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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2018-02-28 22:46:53 +0300
committerKamil Trzciński <ayufan@ayufan.eu>2018-02-28 22:46:53 +0300
commit45d2c31643017807cb3fc66c0be6e9cad9964faf (patch)
treeb3564188de7323969c6ba0a7ec4c86a3b6cce2ac
parent87f11d2cf539d9539b439b54355f0dadaf4ebf76 (diff)
parent4b92efd90cedaa0aff218d11fdce279701128bea (diff)
Merge commit '4b92efd90cedaa0aff218d11fdce279701128bea' into object-storage-ee-to-ce-backport
-rw-r--r--.codeclimate.yml1
-rw-r--r--.gitlab-ci.yml100
-rw-r--r--.rubocop.yml1226
-rw-r--r--.rubocop_todo.yml714
-rw-r--r--.ruby-version2
-rw-r--r--CHANGELOG.md231
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile28
-rw-r--r--Gemfile.lock93
-rw-r--r--PROCESS.md6
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/file_icons.svg1
-rw-r--r--app/assets/images/icon_image_comment.svg1
-rw-r--r--app/assets/images/icon_image_comment@2x.svg1
-rw-r--r--app/assets/images/icons.json2
-rw-r--r--app/assets/images/icons.svg2
-rw-r--r--app/assets/images/illustrations/image_comment_light_cursor.svg1
-rw-r--r--app/assets/images/illustrations/image_comment_light_cursor@2x.svg1
-rw-r--r--app/assets/images/illustrations/job_not_triggered.svg1
-rw-r--r--app/assets/images/illustrations/manual_action.svg1
-rw-r--r--app/assets/images/illustrations/merge_request_changes_empty.svg2
-rw-r--r--app/assets/images/illustrations/multi_file_editor_empty.svg1
-rw-r--r--app/assets/images/illustrations/service_desk_callout.svg1
-rw-r--r--app/assets/images/illustrations/service_desk_empty.svg1
-rw-r--r--app/assets/images/illustrations/wiki_login_empty.svg1
-rw-r--r--app/assets/images/illustrations/wiki_logout_empty.svg1
-rw-r--r--app/assets/images/multi-editor-off.pngbin0 -> 4884 bytes
-rw-r--r--app/assets/images/multi-editor-on.pngbin0 -> 5464 bytes
-rw-r--r--app/assets/javascripts/activities.js10
-rw-r--r--app/assets/javascripts/admin.js117
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/aside.js24
-rw-r--r--app/assets/javascripts/behaviors/copy_as_gfm.js12
-rw-r--r--app/assets/javascripts/behaviors/secret_values.js42
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js3
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js4
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js4
-rw-r--r--app/assets/javascripts/blob/notebook/index.js14
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js16
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js2
-rw-r--r--app/assets/javascripts/boards/components/board_list.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js6
-rw-r--r--app/assets/javascripts/boards/services/board_service.js101
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue20
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js7
-rw-r--r--app/assets/javascripts/commit/image_file.js5
-rw-r--r--app/assets/javascripts/commits.js5
-rw-r--r--app/assets/javascripts/compare.js37
-rw-r--r--app/assets/javascripts/compare_autocomplete.js116
-rw-r--r--app/assets/javascripts/contextual_sidebar.js10
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js8
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.vue7
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue3
-rw-r--r--app/assets/javascripts/diff.js8
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js4
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js3
-rw-r--r--app/assets/javascripts/dispatcher.js67
-rw-r--r--app/assets/javascripts/docs/docs_bundle.js13
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js45
-rw-r--r--app/assets/javascripts/fly_out_nav.js17
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js6
-rw-r--r--app/assets/javascripts/gl_dropdown.js27
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js42
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js57
-rw-r--r--app/assets/javascripts/groups/components/app.vue4
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue16
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue38
-rw-r--r--app/assets/javascripts/groups/components/item_caret.vue15
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue100
-rw-r--r--app/assets/javascripts/groups/components/item_stats_value.vue68
-rw-r--r--app/assets/javascripts/groups/components/item_type_icon.vue13
-rw-r--r--app/assets/javascripts/groups/constants.js6
-rw-r--r--app/assets/javascripts/groups/new_group_child.js5
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js3
-rw-r--r--app/assets/javascripts/helpers/user_feature_helper.js7
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue66
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue (renamed from app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue)0
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue (renamed from app/assets/javascripts/repo/components/commit_sidebar/list_item.vue)0
-rw-r--r--app/assets/javascripts/ide/components/ide.vue95
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue108
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue71
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue108
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue71
-rw-r--r--app/assets/javascripts/ide/components/new_branch_form.vue (renamed from app/assets/javascripts/repo/components/new_branch_form.vue)2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue101
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue (renamed from app/assets/javascripts/repo/components/new_dropdown/modal.vue)32
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue (renamed from app/assets/javascripts/repo/components/new_dropdown/upload.vue)35
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue (renamed from app/assets/javascripts/repo/components/repo_commit_section.vue)61
-rw-r--r--app/assets/javascripts/ide/components/repo_edit_button.vue (renamed from app/assets/javascripts/repo/components/repo_edit_button.vue)8
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue (renamed from app/assets/javascripts/repo/components/repo_editor.vue)48
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue (renamed from app/assets/javascripts/repo/components/repo_file.vue)91
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue (renamed from app/assets/javascripts/repo/components/repo_file_buttons.vue)0
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue (renamed from app/assets/javascripts/repo/components/repo_loading_file.vue)8
-rw-r--r--app/assets/javascripts/ide/components/repo_prev_directory.vue (renamed from app/assets/javascripts/repo/components/repo_prev_directory.vue)8
-rw-r--r--app/assets/javascripts/ide/components/repo_preview.vue (renamed from app/assets/javascripts/repo/components/repo_preview.vue)5
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue (renamed from app/assets/javascripts/repo/components/repo_tab.vue)16
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue (renamed from app/assets/javascripts/repo/components/repo_tabs.vue)0
-rw-r--r--app/assets/javascripts/ide/ide_router.js101
-rw-r--r--app/assets/javascripts/ide/index.js31
-rw-r--r--app/assets/javascripts/ide/lib/common/disposable.js (renamed from app/assets/javascripts/repo/lib/common/disposable.js)0
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js (renamed from app/assets/javascripts/repo/lib/common/model.js)8
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js (renamed from app/assets/javascripts/repo/lib/common/model_manager.js)0
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js (renamed from app/assets/javascripts/repo/lib/decorations/controller.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js (renamed from app/assets/javascripts/repo/lib/diff/controller.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js (renamed from app/assets/javascripts/repo/lib/diff/diff.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js (renamed from app/assets/javascripts/repo/lib/diff/diff_worker.js)0
-rw-r--r--app/assets/javascripts/ide/lib/editor.js (renamed from app/assets/javascripts/repo/lib/editor.js)30
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js (renamed from app/assets/javascripts/repo/lib/editor_options.js)0
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js (renamed from app/assets/javascripts/repo/monaco_loader.js)0
-rw-r--r--app/assets/javascripts/ide/services/index.js (renamed from app/assets/javascripts/repo/services/index.js)7
-rw-r--r--app/assets/javascripts/ide/stores/actions.js183
-rw-r--r--app/assets/javascripts/ide/stores/actions/branch.js43
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js (renamed from app/assets/javascripts/repo/stores/actions/file.js)41
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js27
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js188
-rw-r--r--app/assets/javascripts/ide/stores/getters.js19
-rw-r--r--app/assets/javascripts/ide/stores/index.js (renamed from app/assets/javascripts/repo/stores/index.js)0
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js (renamed from app/assets/javascripts/repo/stores/mutation_types.js)20
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js (renamed from app/assets/javascripts/repo/stores/mutations.js)23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js28
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js (renamed from app/assets/javascripts/repo/stores/mutations/file.js)20
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js (renamed from app/assets/javascripts/repo/stores/mutations/tree.js)9
-rw-r--r--app/assets/javascripts/ide/stores/state.js (renamed from app/assets/javascripts/repo/stores/state.js)21
-rw-r--r--app/assets/javascripts/ide/stores/utils.js (renamed from app/assets/javascripts/repo/stores/utils.js)64
-rw-r--r--app/assets/javascripts/image_diff/helpers/badge_helper.js7
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js7
-rw-r--r--app/assets/javascripts/init_legacy_filters.js2
-rw-r--r--app/assets/javascripts/init_notes.js6
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js5
-rw-r--r--app/assets/javascripts/issue.js15
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue106
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue24
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/index.js7
-rw-r--r--app/assets/javascripts/issue_show/services/index.js21
-rw-r--r--app/assets/javascripts/job.js35
-rw-r--r--app/assets/javascripts/jobs/components/header.vue8
-rw-r--r--app/assets/javascripts/layout_nav.js70
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/cache.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js5
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js228
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js9
-rw-r--r--app/assets/javascripts/lib/utils/tick_formats.js39
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js126
-rw-r--r--app/assets/javascripts/line_highlighter.js2
-rw-r--r--app/assets/javascripts/locale/index.js15
-rw-r--r--app/assets/javascripts/main.js45
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js3
-rw-r--r--app/assets/javascripts/merge_request.js263
-rw-r--r--app/assets/javascripts/merge_request_tabs.js650
-rw-r--r--app/assets/javascripts/milestone_select.js422
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue71
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue76
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue205
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js5
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js52
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js21
-rw-r--r--app/assets/javascripts/namespace_select.js4
-rw-r--r--app/assets/javascripts/new_commit_form.js7
-rw-r--r--app/assets/javascripts/notes.js14
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue (renamed from app/assets/javascripts/notes/components/issue_comment_form.vue)2
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue (renamed from app/assets/javascripts/notes/components/issue_note_body.vue)6
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue (renamed from app/assets/javascripts/notes/components/issue_note_form.vue)0
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue (renamed from app/assets/javascripts/notes/components/issue_discussion.vue)12
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue (renamed from app/assets/javascripts/notes/components/issue_note.vue)13
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue (renamed from app/assets/javascripts/notes/components/issue_notes_app.vue)25
-rw-r--r--app/assets/javascripts/notes/index.js6
-rw-r--r--app/assets/javascripts/notifications_dropdown.js48
-rw-r--r--app/assets/javascripts/notifications_form.js93
-rw-r--r--app/assets/javascripts/pager.js136
-rw-r--r--app/assets/javascripts/pages/users/show/index.js3
-rw-r--r--app/assets/javascripts/performance_bar.js3
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue47
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue41
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue11
-rw-r--r--app/assets/javascripts/pipelines/pipelines_bundle.js3
-rw-r--r--app/assets/javascripts/preview_markdown.js6
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue113
-rw-r--r--app/assets/javascripts/profile/account/index.js8
-rw-r--r--app/assets/javascripts/profile/profile.js21
-rw-r--r--app/assets/javascripts/project.js3
-rw-r--r--app/assets/javascripts/project_find_file.js298
-rw-r--r--app/assets/javascripts/project_variables.js39
-rw-r--r--app/assets/javascripts/projects/project_import_gitlab_project.js4
-rw-r--r--app/assets/javascripts/projects/project_new.js7
-rw-r--r--app/assets/javascripts/render_gfm.js4
-rw-r--r--app/assets/javascripts/render_mermaid.js20
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list.vue89
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue89
-rw-r--r--app/assets/javascripts/repo/components/repo.vue63
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue85
-rw-r--r--app/assets/javascripts/repo/index.js106
-rw-r--r--app/assets/javascripts/repo/stores/actions.js145
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js20
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js162
-rw-r--r--app/assets/javascripts/repo/stores/getters.js40
-rw-r--r--app/assets/javascripts/repo/stores/mutations/branch.js9
-rw-r--r--app/assets/javascripts/right_sidebar.js438
-rw-r--r--app/assets/javascripts/search.js207
-rw-r--r--app/assets/javascripts/search_autocomplete.js769
-rw-r--r--app/assets/javascripts/shortcuts.js15
-rw-r--r--app/assets/javascripts/shortcuts_blob.js6
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js24
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue11
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue9
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js71
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js5
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js4
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js50
-rw-r--r--app/assets/javascripts/single_file_diff.js3
-rw-r--r--app/assets/javascripts/syntax_highlight.js14
-rw-r--r--app/assets/javascripts/todos.js4
-rw-r--r--app/assets/javascripts/tree.js5
-rw-r--r--app/assets/javascripts/users/activity_calendar.js14
-rw-r--r--app/assets/javascripts/users/user_tabs.js6
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue133
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js60
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js20
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue92
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js589
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/memory_graph.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/modal.vue (renamed from app/assets/javascripts/vue_shared/components/popup_dialog.vue)38
-rw-r--r--app/assets/javascripts/vue_shared/components/panel_resizer.vue91
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/image.vue103
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue36
-rw-r--r--app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js36
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js6
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss51
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss320
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/filters.scss59
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/framework/images.scss10
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss6
-rw-r--r--app/assets/stylesheets/framework/layout.scss5
-rw-r--r--app/assets/stylesheets/framework/lists.scss75
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss43
-rw-r--r--app/assets/stylesheets/framework/mobile.scss17
-rw-r--r--app/assets/stylesheets/framework/modal.scss18
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss14
-rw-r--r--app/assets/stylesheets/framework/toggle.scss64
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss14
-rw-r--r--app/assets/stylesheets/framework/wells.scss6
-rw-r--r--app/assets/stylesheets/pages/boards.scss8
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/clusters.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss21
-rw-r--r--app/assets/stylesheets/pages/editor.scss2
-rw-r--r--app/assets/stylesheets/pages/environments.scss88
-rw-r--r--app/assets/stylesheets/pages/events.scss7
-rw-r--r--app/assets/stylesheets/pages/issuable.scss35
-rw-r--r--app/assets/stylesheets/pages/issues.scss2
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/members.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss8
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/notifications.scss4
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss12
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss16
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/assets/stylesheets/pages/repo.scss303
-rw-r--r--app/assets/stylesheets/pages/search.scss21
-rw-r--r--app/assets/stylesheets/pages/settings.scss4
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss3
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/controllers/admin/groups_controller.rb8
-rw-r--r--app/controllers/admin/projects_controller.rb11
-rw-r--r--app/controllers/autocomplete_controller.rb4
-rw-r--r--app/controllers/concerns/boards_responses.rb4
-rw-r--r--app/controllers/concerns/creates_commit.rb26
-rw-r--r--app/controllers/concerns/group_tree.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb23
-rw-r--r--app/controllers/concerns/issuable_collections.rb15
-rw-r--r--app/controllers/concerns/issues_action.rb4
-rw-r--r--app/controllers/concerns/members_presentation.rb11
-rw-r--r--app/controllers/concerns/merge_requests_action.rb3
-rw-r--r--app/controllers/concerns/milestone_actions.rb8
-rw-r--r--app/controllers/concerns/notes_actions.rb32
-rw-r--r--app/controllers/concerns/oauth_applications.rb2
-rw-r--r--app/controllers/concerns/preview_markdown.rb2
-rw-r--r--app/controllers/concerns/renders_commits.rb2
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/spammable_actions.rb24
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb2
-rw-r--r--app/controllers/concerns/with_performance_bar.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/controllers/passwords_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb5
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/controllers/projects/branches_controller.rb8
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb30
-rw-r--r--app/controllers/projects/clusters/user_controller.rb1
-rw-r--r--app/controllers/projects/clusters_controller.rb9
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb25
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb31
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb8
-rw-r--r--app/controllers/projects/runners_controller.rb8
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb10
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb5
-rw-r--r--app/finders/issuable_finder.rb15
-rw-r--r--app/finders/labels_finder.rb19
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb59
-rw-r--r--app/helpers/branches_helper.rb8
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb16
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb5
-rw-r--r--app/helpers/icons_helper.rb7
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb7
-rw-r--r--app/helpers/members_helper.rb7
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/selects_helper.rb1
-rw-r--r--app/helpers/services_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb10
-rw-r--r--app/mailers/emails/notes.rb10
-rw-r--r--app/mailers/notify.rb16
-rw-r--r--app/models/blob.rb12
-rw-r--r--app/models/blob_viewer/dependency_manager.rb13
-rw-r--r--app/models/blob_viewer/package_json.rb18
-rw-r--r--app/models/ci/build.rb12
-rw-r--r--app/models/ci/pipeline.rb17
-rw-r--r--app/models/clusters/applications/helm.rb17
-rw-r--r--app/models/clusters/applications/ingress.rb23
-rw-r--r--app/models/clusters/applications/prometheus.rb26
-rw-r--r--app/models/clusters/cluster.rb7
-rw-r--r--app/models/clusters/concerns/application_core.rb29
-rw-r--r--app/models/commit.rb36
-rw-r--r--app/models/concerns/blocks_json_serialization.rb16
-rw-r--r--app/models/concerns/cache_markdown_field.rb5
-rw-r--r--app/models/concerns/deployment_platform.rb48
-rw-r--r--app/models/concerns/discussion_on_diff.rb10
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/mentionable.rb10
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/concerns/note_on_diff.rb4
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/participable.rb12
-rw-r--r--app/models/concerns/project_features_compatibility.rb1
-rw-r--r--app/models/concerns/relative_positioning.rb26
-rw-r--r--app/models/concerns/resolvable_discussion.rb14
-rw-r--r--app/models/concerns/routable.rb10
-rw-r--r--app/models/concerns/spammable.rb5
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/taskable.rb2
-rw-r--r--app/models/concerns/time_trackable.rb20
-rw-r--r--app/models/diff_discussion.rb20
-rw-r--r--app/models/diff_note.rb6
-rw-r--r--app/models/event.rb13
-rw-r--r--app/models/identity.rb14
-rw-r--r--app/models/issue.rb14
-rw-r--r--app/models/legacy_diff_note.rb6
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/merge_request.rb44
-rw-r--r--app/models/merge_request/metrics.rb10
-rw-r--r--app/models/merge_request_diff.rb6
-rw-r--r--app/models/namespace.rb18
-rw-r--r--app/models/note.rb13
-rw-r--r--app/models/project.rb72
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb28
-rw-r--r--app/models/project_team.rb28
-rw-r--r--app/models/redirect_route.rb28
-rw-r--r--app/models/repository.rb126
-rw-r--r--app/models/route.rb26
-rw-r--r--app/models/service.rb13
-rw-r--r--app/models/user.rb33
-rw-r--r--app/models/user_synced_attributes_metadata.rb10
-rw-r--r--app/policies/ci/pipeline_policy.rb16
-rw-r--r--app/policies/ci/pipeline_schedule_policy.rb10
-rw-r--r--app/policies/group_policy.rb8
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/presenters/group_member_presenter.rb15
-rw-r--r--app/presenters/member_presenter.rb38
-rw-r--r--app/presenters/members_presenter.rb15
-rw-r--r--app/presenters/merge_request_presenter.rb18
-rw-r--r--app/presenters/project_member_presenter.rb15
-rw-r--r--app/serializers/concerns/with_pagination.rb2
-rw-r--r--app/serializers/event_entity.rb4
-rw-r--r--app/serializers/group_child_entity.rb9
-rw-r--r--app/serializers/issuable_entity.rb8
-rw-r--r--app/serializers/issuable_sidebar_entity.rb6
-rw-r--r--app/serializers/issue_entity.rb8
-rw-r--r--app/serializers/job_entity.rb2
-rw-r--r--app/serializers/merge_request_basic_entity.rb1
-rw-r--r--app/serializers/merge_request_metrics_entity.rb6
-rw-r--r--app/serializers/merge_request_serializer.rb6
-rw-r--r--app/serializers/merge_request_widget_entity.rb (renamed from app/serializers/merge_request_entity.rb)46
-rw-r--r--app/services/check_gcp_project_billing_service.rb8
-rw-r--r--app/services/ci/create_pipeline_service.rb27
-rw-r--r--app/services/ci/register_job_service.rb17
-rw-r--r--app/services/clusters/applications/base_helm_service.rb2
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb16
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/files/base_service.rb14
-rw-r--r--app/services/files/delete_service.rb10
-rw-r--r--app/services/files/multi_service.rb20
-rw-r--r--app/services/files/update_service.rb21
-rw-r--r--app/services/git_push_service.rb19
-rw-r--r--app/services/issuable/destroy_service.rb6
-rw-r--r--app/services/issuable_base_service.rb6
-rw-r--r--app/services/members/approve_access_request_service.rb11
-rw-r--r--app/services/members/destroy_service.rb11
-rw-r--r--app/services/merge_request_metrics_service.rb19
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/merge_requests/close_service.rb13
-rw-r--r--app/services/merge_requests/conflicts/list_service.rb2
-rw-r--r--app/services/merge_requests/post_merge_service.rb11
-rw-r--r--app/services/merge_requests/rebase_service.rb30
-rw-r--r--app/services/merge_requests/reopen_service.rb13
-rw-r--r--app/services/merge_requests/working_copy_base_service.rb24
-rw-r--r--app/services/notes/destroy_service.rb4
-rw-r--r--app/services/projects/create_service.rb17
-rw-r--r--app/services/projects/fork_service.rb14
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb4
-rw-r--r--app/services/projects/transfer_service.rb7
-rw-r--r--app/services/projects/unlink_fork_service.rb2
-rw-r--r--app/services/protected_branches/create_service.rb4
-rw-r--r--app/services/quick_actions/interpret_service.rb4
-rw-r--r--app/services/reset_project_cache_service.rb5
-rw-r--r--app/services/search/global_service.rb5
-rw-r--r--app/services/search/group_service.rb1
-rw-r--r--app/services/spam_check_service.rb4
-rw-r--r--app/services/todo_service.rb16
-rw-r--r--app/services/users/destroy_service.rb5
-rw-r--r--app/views/admin/groups/index.html.haml18
-rw-r--r--app/views/admin/system_info/show.html.haml6
-rw-r--r--app/views/admin/users/show.html.haml1
-rw-r--r--app/views/ci/variables/_index.html.haml6
-rw-r--r--app/views/ci/variables/_table.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml6
-rw-r--r--app/views/doorkeeper/applications/show.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml3
-rw-r--r--app/views/groups/edit.html.haml4
-rw-r--r--app/views/help/index.html.haml10
-rw-r--r--app/views/ide/index.html.haml12
-rw-r--r--app/views/layouts/_page.html.haml4
-rw-r--r--app/views/layouts/_recaptcha_verification.html.haml15
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml6
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml14
-rw-r--r--app/views/layouts/nav_only.html.haml13
-rw-r--r--app/views/notify/pipeline_success_email.html.haml2
-rw-r--r--app/views/notify/pipeline_success_email.text.erb4
-rw-r--r--app/views/profiles/accounts/show.html.haml10
-rw-r--r--app/views/profiles/audit_log.html.haml2
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml1
-rw-r--r--app/views/profiles/keys/show.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml6
-rw-r--r--app/views/profiles/preferences/show.html.haml17
-rw-r--r--app/views/profiles/show.html.haml13
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml9
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_last_push.html.haml27
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_merge_request_fast_forward_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_rebase_settings.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml1
-rw-r--r--app/views/projects/blob/_header_content.html.haml3
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml10
-rw-r--r--app/views/projects/blob/show.html.haml15
-rw-r--r--app/views/projects/blob/viewers/_dependency_manager.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_image.html.haml3
-rw-r--r--app/views/projects/branches/_branch.html.haml11
-rw-r--r--app/views/projects/branches/new.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml4
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml4
-rw-r--r--app/views/projects/clusters/_banner.html.haml13
-rw-r--r--app/views/projects/clusters/_cluster.html.haml9
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml6
-rw-r--r--app/views/projects/clusters/_enabled.html.haml15
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml33
-rw-r--r--app/views/projects/clusters/_tabs.html.haml16
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml3
-rw-r--r--app/views/projects/clusters/gcp/_header.html.haml2
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml1
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml2
-rw-r--r--app/views/projects/clusters/index.html.haml16
-rw-r--r--app/views/projects/clusters/show.html.haml7
-rw-r--r--app/views/projects/clusters/user/_form.html.haml3
-rw-r--r--app/views/projects/commit/_limit_exceeded_message.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/compare/_form.html.haml16
-rw-r--r--app/views/projects/compare/index.html.haml18
-rw-r--r--app/views/projects/compare/show.html.haml15
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_replaced_image_diff.html.haml16
-rw-r--r--app/views/projects/diffs/_single_image_diff.html.haml6
-rw-r--r--app/views/projects/edit.html.haml25
-rw-r--r--app/views/projects/environments/metrics.html.haml8
-rw-r--r--app/views/projects/forks/error.html.haml3
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml11
-rw-r--r--app/views/projects/jobs/_empty_state.html.haml17
-rw-r--r--app/views/projects/jobs/show.html.haml76
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml6
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml19
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml50
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml3
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml8
-rw-r--r--app/views/projects/project_members/_team.html.haml10
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml4
-rw-r--r--app/views/projects/services/_deprecated_message.html.haml3
-rw-r--r--app/views/projects/services/_form.html.haml5
-rw-r--r--app/views/projects/services/edit.html.haml2
-rw-r--r--app/views/projects/tags/new.html.haml8
-rw-r--r--app/views/projects/tree/_blob_item.html.haml3
-rw-r--r--app/views/projects/tree/_old_tree_content.html.haml24
-rw-r--r--app/views/projects/tree/_old_tree_header.html.haml64
-rw-r--r--app/views/projects/tree/_tree_content.html.haml29
-rw-r--r--app/views/projects/tree/_tree_header.html.haml78
-rw-r--r--app/views/projects/tree/show.html.haml8
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml11
-rw-r--r--app/views/shared/_field.html.haml11
-rw-r--r--app/views/shared/_label.html.haml33
-rw-r--r--app/views/shared/_recaptcha_form.html.haml20
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml2
-rw-r--r--app/views/shared/_show_aside.html.haml2
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml3
-rw-r--r--app/views/shared/empty_states/_issues.html.haml24
-rw-r--r--app/views/shared/empty_states/_labels.html.haml10
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml17
-rw-r--r--app/views/shared/groups/_dropdown.html.haml5
-rw-r--r--app/views/shared/issuable/_assignees.html.haml18
-rw-r--r--app/views/shared/members/_member.html.haml24
-rw-r--r--app/views/shared/members/_requests.html.haml19
-rw-r--r--app/views/shared/notes/_note.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/repo/_repo.html.haml13
-rw-r--r--app/workers/all_queues.yml101
-rw-r--r--app/workers/background_migration_worker.rb45
-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/check_gcp_project_billing_worker.rb59
-rw-r--r--app/workers/concerns/application_worker.rb24
-rw-r--r--app/workers/concerns/cluster_queue.rb2
-rw-r--r--app/workers/concerns/cronjob_queue.rb3
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb4
-rw-r--r--app/workers/concerns/new_issuable.rb8
-rw-r--r--app/workers/concerns/pipeline_queue.rb10
-rw-r--r--app/workers/concerns/project_import_options.rb23
-rw-r--r--app/workers/concerns/project_start_import.rb1
-rw-r--r--app/workers/concerns/repository_check_queue.rb4
-rw-r--r--app/workers/create_pipeline_worker.rb2
-rw-r--r--app/workers/expire_job_cache_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb2
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_process_worker.rb2
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb2
-rw-r--r--app/workers/rebase_worker.rb12
-rw-r--r--app/workers/repository_fork_worker.rb22
-rw-r--r--app/workers/repository_import_worker.rb19
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb22
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb7
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb16
-rw-r--r--changelogs/unreleased/13634-broadcast-message.yml5
-rw-r--r--changelogs/unreleased/13695-order-contributors-in-api.yml5
-rw-r--r--changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml5
-rw-r--r--changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml5
-rw-r--r--changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml5
-rw-r--r--changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml5
-rw-r--r--changelogs/unreleased/15955-improve-search-query.yml5
-rw-r--r--changelogs/unreleased/16036-ignore-lost-found-folder-during-backup-on-a-volume.yml5
-rw-r--r--changelogs/unreleased/16117-improve-search-for-issues.yml (renamed from changelogs/unreleased/39727-add-axios-to-common.yml)2
-rw-r--r--changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml5
-rw-r--r--changelogs/unreleased/20035-pause-resume-runners.yml5
-rw-r--r--changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml5
-rw-r--r--changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml5
-rw-r--r--changelogs/unreleased/24347-dont-post-system-note-when-branch-creation-fails.yml5
-rw-r--r--changelogs/unreleased/25317-prioritize-author-date-over-commit.yml5
-rw-r--r--changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml4
-rw-r--r--changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml5
-rw-r--r--changelogs/unreleased/31995-project-limit-default-fix.yml5
-rw-r--r--changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml5
-rw-r--r--changelogs/unreleased/32098-pipelines-navigation.yml6
-rw-r--r--changelogs/unreleased/32364-updating-slack-notification-not-working-by-api.yml5
-rw-r--r--changelogs/unreleased/32878-merge-request-from-email.yml5
-rw-r--r--changelogs/unreleased/33028-event-tag-links.yml5
-rw-r--r--changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml5
-rw-r--r--changelogs/unreleased/33609-hide-pagination.yml5
-rw-r--r--changelogs/unreleased/33926-update-issuable-icons.yml5
-rw-r--r--changelogs/unreleased/34534-switch-to-axios.yml5
-rw-r--r--changelogs/unreleased/34600-performance-wiki-pages.yml5
-rw-r--r--changelogs/unreleased/35616-move-k8-to-cluster-page.yml5
-rw-r--r--changelogs/unreleased/36020-private-npm-modules.yml5
-rw-r--r--changelogs/unreleased/36400-trigger-job.yml5
-rw-r--r--changelogs/unreleased/36782-replace-team-user-role-with-add_role-user-in-specs.yml5
-rw-r--r--changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml5
-rw-r--r--changelogs/unreleased/37354-pipelines-update.yml5
-rw-r--r--changelogs/unreleased/37843-ci-trace-ansi-colours-256-bold-have-no-css-due-wrongly-ansi2html-light-color-variant-conversion-feature.yml5
-rw-r--r--changelogs/unreleased/38019-hide-runner-token.yml5
-rw-r--r--changelogs/unreleased/38030-add-graph-value-to-hover.yml5
-rw-r--r--changelogs/unreleased/38075_allow_refernce_integer_labels.yml5
-rw-r--r--changelogs/unreleased/38145_ux_issues_in_system_info_page.yml5
-rw-r--r--changelogs/unreleased/38239-update-toggle-design.yml5
-rw-r--r--changelogs/unreleased/38318-search-merge-requests-with-api.yml5
-rw-r--r--changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml5
-rw-r--r--changelogs/unreleased/38541-cancel-alignment.yml5
-rw-r--r--changelogs/unreleased/38596-fix-backspace-visual-token-clearing.yml5
-rw-r--r--changelogs/unreleased/38822-oauth-search-case-insensitive.yml5
-rw-r--r--changelogs/unreleased/38862-email-notifications-not-sent-as-expected.yml6
-rw-r--r--changelogs/unreleased/38869-templates.yml5
-rw-r--r--changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml5
-rw-r--r--changelogs/unreleased/38893-banzai-upload-filter-relative-urls.yml5
-rw-r--r--changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml5
-rw-r--r--changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml5
-rw-r--r--changelogs/unreleased/39298-list-of-avatars-2.yml5
-rw-r--r--changelogs/unreleased/39335-add-time-spend-to-milestones.yml5
-rw-r--r--changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml5
-rw-r--r--changelogs/unreleased/39367-fix-new-email-session-path.yml5
-rw-r--r--changelogs/unreleased/39436-pages-api-administrative.yml5
-rw-r--r--changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml5
-rw-r--r--changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml5
-rw-r--r--changelogs/unreleased/39601-create-issuable-destroy-service.yml5
-rw-r--r--changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/39608-comment-on-image-discussions-tab-alignment.yml5
-rw-r--r--changelogs/unreleased/3968-protected-branch-is-not-set-for-default-branch-on-import.yml5
-rw-r--r--changelogs/unreleased/39720-group-milestone-sorting.yml5
-rw-r--r--changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml5
-rw-r--r--changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml5
-rw-r--r--changelogs/unreleased/39869_show_closed_status_of_links_to_issues_on_wiki_pages.yml5
-rw-r--r--changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml6
-rw-r--r--changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml5
-rw-r--r--changelogs/unreleased/39957-redirect-to-gpc-page-if-users-try-to-create-a-cluster-but-the-account-is-not-enabled.yml5
-rw-r--r--changelogs/unreleased/39977-gitlab-shell-default-timeout.yml5
-rw-r--r--changelogs/unreleased/40016-log-header.yml5
-rw-r--r--changelogs/unreleased/40031-include-assset_sync-gem.yml5
-rw-r--r--changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml5
-rw-r--r--changelogs/unreleased/40063-markdown-editor-improvements.yml5
-rw-r--r--changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml5
-rw-r--r--changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml5
-rw-r--r--changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml5
-rw-r--r--changelogs/unreleased/40190-fix-slash-commands-dropdown-description-mis-alignement-on-firefox.yml5
-rw-r--r--changelogs/unreleased/40228-verify-integrity-of-repositories.yml5
-rw-r--r--changelogs/unreleased/40274-user-settings-breadcrumbs.yml5
-rw-r--r--changelogs/unreleased/40286-hide-full-namespace-groups-tree.yml6
-rw-r--r--changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml5
-rw-r--r--changelogs/unreleased/40301-rebase.yml5
-rw-r--r--changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml6
-rw-r--r--changelogs/unreleased/40418-migrate-existing-data-from-kubernetesservice-to-clusters-platforms-kubernetes.yml5
-rw-r--r--changelogs/unreleased/40453-fix-api-endpoints-to-edit-wiki-pages-where-project-belongs-to-a-group.yml5
-rw-r--r--changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml5
-rw-r--r--changelogs/unreleased/40509_sorting_tags_api.yml5
-rw-r--r--changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml5
-rw-r--r--changelogs/unreleased/40533-groups-tree-updates.yml6
-rw-r--r--changelogs/unreleased/40549-render-emoj-in-groups-overview.yml5
-rw-r--r--changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml6
-rw-r--r--changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml5
-rw-r--r--changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml5
-rw-r--r--changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml5
-rw-r--r--changelogs/unreleased/40622-use-left-right-and-max-count.yml6
-rw-r--r--changelogs/unreleased/40711-fix-forking-hashed-projects.yml5
-rw-r--r--changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml5
-rw-r--r--changelogs/unreleased/40770-doc-elasticsearch.yml5
-rw-r--r--changelogs/unreleased/40780-choose-file.yml5
-rw-r--r--changelogs/unreleased/4080-align-retry-btn.yml5
-rw-r--r--changelogs/unreleased/40871-todo-notification-count-shows-notification-without-having-a-todo.yml5
-rw-r--r--changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml5
-rw-r--r--changelogs/unreleased/41016-import-gitlab-shell-projects.yml6
-rw-r--r--changelogs/unreleased/41053-extend-cluster-applications-to-allow-install-to-prometheus.yml5
-rw-r--r--changelogs/unreleased/41054-disable-creation-of-new-kubernetes-integrations.yml6
-rw-r--r--changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml5
-rw-r--r--changelogs/unreleased/41244-issue-board-shortcut-working-while-no-issues.yml5
-rw-r--r--changelogs/unreleased/41249-clearing-the-cache.yml5
-rw-r--r--changelogs/unreleased/41268-bump-ruby-to-2-3-6.yml5
-rw-r--r--changelogs/unreleased/41424-gitlab-rake-gitlab-import-repos-schedules-an-import.yml5
-rw-r--r--changelogs/unreleased/41468-error-500-trying-to-view-a-merge-request-json-undefined-method-binary-for-nil-nilclass.yml5
-rw-r--r--changelogs/unreleased/41754-update-scss-lint-to-0-56-0.yml5
-rw-r--r--changelogs/unreleased/ac-autodevopfix-kubectl-version.yml5
-rw-r--r--changelogs/unreleased/add-tcp-check-rake-task.yml5
-rw-r--r--changelogs/unreleased/add_project_ci_config_path_leading_slash_validation.yml6
-rw-r--r--changelogs/unreleased/admin-welcome-new-group-link.yml5
-rw-r--r--changelogs/unreleased/an-gitaly-timeouts.yml5
-rw-r--r--changelogs/unreleased/anchor-issue-references.yml6
-rw-r--r--changelogs/unreleased/api-domains-expose-project_id.yml5
-rw-r--r--changelogs/unreleased/brand_header_change.yml5
-rw-r--r--changelogs/unreleased/bump_mysql_gem.yml5
-rw-r--r--changelogs/unreleased/bvl-circuitbreaker-keys-set.yml5
-rw-r--r--changelogs/unreleased/bvl-circuitbreaker-process.yml5
-rw-r--r--changelogs/unreleased/bvl-delete-empty-fork-networks.yml5
-rw-r--r--changelogs/unreleased/bvl-double-fork.yml5
-rw-r--r--changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml5
-rw-r--r--changelogs/unreleased/bvl-fork-public-project-to-private-namespace.yml5
-rw-r--r--changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml5
-rw-r--r--changelogs/unreleased/bvl-subgroup-in-dropdowns.yml5
-rw-r--r--changelogs/unreleased/change-issues-closed-at-background-migration.yml5
-rw-r--r--changelogs/unreleased/cleanup-issues-schema.yml5
-rw-r--r--changelogs/unreleased/commit-title-wrapping.yml5
-rw-r--r--changelogs/unreleased/conditionally-eager-load-event-target-authors.yml5
-rw-r--r--changelogs/unreleased/da-handle-hashed-storage-repos-using-repo-import-task.yml5
-rw-r--r--changelogs/unreleased/delay-background-migrations.yml5
-rw-r--r--changelogs/unreleased/deploy-keys-loading-icon.yml5
-rw-r--r--changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml5
-rw-r--r--changelogs/unreleased/dm-diff-note-for-line-performance.yml5
-rw-r--r--changelogs/unreleased/dm-search-pattern.yml5
-rw-r--r--changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml5
-rw-r--r--changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml5
-rw-r--r--changelogs/unreleased/feature-40842-provide-oracles-webgate-cookies-to-jira-requests.yml6
-rw-r--r--changelogs/unreleased/feature-api_runners_online.yml5
-rw-r--r--changelogs/unreleased/feature-custom-text-for-new-projects.yml5
-rw-r--r--changelogs/unreleased/feature-disable-password-authentication.yml5
-rw-r--r--changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml5
-rw-r--r--changelogs/unreleased/feature_add_mermaid.yml5
-rw-r--r--changelogs/unreleased/fix-abuse-reports-link-url.yml5
-rw-r--r--changelogs/unreleased/fix-activity-inline-event-line-height.yml5
-rw-r--r--changelogs/unreleased/fix-create-mr-from-issue-with-template.yml5
-rw-r--r--changelogs/unreleased/fix-docs-help-shortcut.yml (renamed from changelogs/unreleased/40508-snippets-zen-mode.yml)2
-rw-r--r--changelogs/unreleased/fix-filter-by-my-reaction.yml5
-rw-r--r--changelogs/unreleased/fix-gb-fix-import-export-restoring-associations.yml6
-rw-r--r--changelogs/unreleased/fix-last-push-event-widget-layout.yml5
-rw-r--r--changelogs/unreleased/fix-move-2fa-disable-button.yml5
-rw-r--r--changelogs/unreleased/fix-new-project-guidelines-styling.yml5
-rw-r--r--changelogs/unreleased/fix-onion-skin-reenter.yml5
-rw-r--r--changelogs/unreleased/fix-profile-settings-content-width.yml5
-rw-r--r--changelogs/unreleased/fix-profile-settings-sidebar-heading.yml5
-rw-r--r--changelogs/unreleased/fix-protected-branches-descriptions.yml5
-rw-r--r--changelogs/unreleased/fix-remove-unnecessary-sidebar-element-alignment.yml5
-rw-r--r--changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml5
-rw-r--r--changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml6
-rw-r--r--changelogs/unreleased/fix-todos-last-page.yml5
-rw-r--r--changelogs/unreleased/fix_build_count_in_pipeline_success_maild.yml5
-rw-r--r--changelogs/unreleased/fj-40053-error-500-members-list.yml5
-rw-r--r--changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml5
-rw-r--r--changelogs/unreleased/fj-40407-missing-order-paginate.yml5
-rw-r--r--changelogs/unreleased/fj-40752-forks-api-not-using-services.yml5
-rw-r--r--changelogs/unreleased/group-new-miletone-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/hashed-storage-attachments-migration-path.yml5
-rw-r--r--changelogs/unreleased/improved-changes-dropdown.yml5
-rw-r--r--changelogs/unreleased/index-namespaces-lower-name.yml5
-rw-r--r--changelogs/unreleased/issue-description-field-typo.yml5
-rw-r--r--changelogs/unreleased/issue_39238.yml5
-rw-r--r--changelogs/unreleased/issue_40500.yml5
-rw-r--r--changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml5
-rw-r--r--changelogs/unreleased/jivl-activate-repo-cookie-preferences.yml5
-rw-r--r--changelogs/unreleased/jivl-fix-import-project-url-bug.yml5
-rw-r--r--changelogs/unreleased/jramsay-4012-i18n-compare.yml5
-rw-r--r--changelogs/unreleased/jramsay-41590-add-readme-case.yml5
-rw-r--r--changelogs/unreleased/ldap_username_attributes.yml5
-rw-r--r--changelogs/unreleased/lfs-badge.yml5
-rw-r--r--changelogs/unreleased/merge-request-lock-icon-size-fix.yml5
-rw-r--r--changelogs/unreleased/merge-requests-schema-cleanup.yml5
-rw-r--r--changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml5
-rw-r--r--changelogs/unreleased/mk-add-user-rate-limits.yml6
-rw-r--r--changelogs/unreleased/mk-no-op-delete-conflicting-redirects.yml6
-rw-r--r--changelogs/unreleased/multiple-clusters-single-list.yml5
-rw-r--r--changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml6
-rw-r--r--changelogs/unreleased/osw-introduce-merge-request-statistics.yml5
-rw-r--r--changelogs/unreleased/outdated-browser-position-fix.yml5
-rw-r--r--changelogs/unreleased/patch-24.yml5
-rw-r--r--changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml5
-rw-r--r--changelogs/unreleased/perform-sql-matching-of-tags.yml5
-rw-r--r--changelogs/unreleased/protected-branches-names.yml5
-rw-r--r--changelogs/unreleased/remove-incorrect-guidance.yml6
-rw-r--r--changelogs/unreleased/remove-links-mr-empty-state.yml5
-rw-r--r--changelogs/unreleased/remove-tabindexes-from-tag-form.yml5
-rw-r--r--changelogs/unreleased/sh-add-schedule-pipeline-run-now.yml5
-rw-r--r--changelogs/unreleased/sh-catch-invalid-uri-markdown.yml5
-rw-r--r--changelogs/unreleased/sh-fix-root-ref-repository.yml5
-rw-r--r--changelogs/unreleased/sh-make-kib-human.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-commit-stats.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-groups-api.yml5
-rw-r--r--changelogs/unreleased/sh-validate-path-project-import.yml5
-rw-r--r--changelogs/unreleased/show-inline-edit-btn.yml5
-rw-r--r--changelogs/unreleased/show_proper_labels_in_board_issue_sidebar_when_issue_is_closed.yml5
-rw-r--r--changelogs/unreleased/skip_confirmation_user_API.yml7
-rw-r--r--changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml5
-rw-r--r--changelogs/unreleased/tc-correct-email-in-reply-to.yml5
-rw-r--r--changelogs/unreleased/text-utils.yml5
-rw-r--r--changelogs/unreleased/throttle-touching-of-objects.yml5
-rw-r--r--changelogs/unreleased/tm-feature-list-runners-jobs-api.yml5
-rw-r--r--changelogs/unreleased/tm-feature-namespace-by-id-api.yml5
-rw-r--r--changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml6
-rw-r--r--changelogs/unreleased/update-merge-worker-metrics.yml5
-rw-r--r--changelogs/unreleased/update-redis-rack.yml (renamed from changelogs/unreleased/fl-upgrade-svg.yml)2
-rw-r--r--changelogs/unreleased/update_mr_changes_empty_page.yml5
-rw-r--r--changelogs/unreleased/use-count_commits-directly.yml5
-rw-r--r--changelogs/unreleased/use-merge-requests-diff-id-column.yml5
-rw-r--r--changelogs/unreleased/user-agent-gke-api.yml5
-rw-r--r--changelogs/unreleased/winh-modal-target-id.yml5
-rw-r--r--changelogs/unreleased/winh-subgroups-api.yml5
-rw-r--r--changelogs/unreleased/winh-translate-contributors-page-dates.yml5
-rw-r--r--changelogs/unreleased/zj-commit-show-n-1.yml5
-rw-r--r--config.ru5
-rw-r--r--config/application.rb6
-rw-r--r--config/gitlab.yml.example1
-rw-r--r--config/initializers/active_record_data_types.rb5
-rw-r--r--config/initializers/active_record_schema_ignore_tables.rb2
-rw-r--r--config/initializers/asset_sync.rb31
-rw-r--r--config/initializers/fix_local_cache_middleware.rb2
-rw-r--r--config/initializers/flipper.rb24
-rw-r--r--config/initializers/forbid_sidekiq_in_transactions.rb8
-rw-r--r--config/initializers/peek.rb2
-rw-r--r--config/initializers/rspec_profiling.rb4
-rw-r--r--config/initializers/rugged_use_gitlab_git_attributes.rb5
-rw-r--r--config/initializers/sidekiq.rb18
-rw-r--r--config/no_todos_messages.yml6
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/project.rb10
-rw-r--r--config/sidekiq_queues.yml5
-rw-r--r--config/webpack.config.js24
-rw-r--r--db/fixtures/development/04_project.rb3
-rw-r--r--db/fixtures/development/06_teams.rb2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb4
-rw-r--r--db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb1
-rw-r--r--db/migrate/20160610301627_remove_notification_level_from_users.rb1
-rw-r--r--db/migrate/20160621123729_add_rebase_commit_sha_to_merge_requests.rb22
-rw-r--r--db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb1
-rw-r--r--db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb1
-rw-r--r--db/migrate/20160729173930_remove_project_id_from_spam_logs.rb1
-rw-r--r--db/migrate/20160831223750_remove_features_enabled_from_projects.rb1
-rw-r--r--db/migrate/20160913162434_remove_projects_pushes_since_gc.rb1
-rw-r--r--db/migrate/20161018024550_remove_priority_from_labels.rb1
-rw-r--r--db/migrate/20161201160452_migrate_project_statistics.rb1
-rw-r--r--db/migrate/20170222143500_remove_old_project_id_columns.rb1
-rw-r--r--db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb1
-rw-r--r--db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb1
-rw-r--r--db/migrate/20171019141859_fix_dev_timezone_schema.rb25
-rw-r--r--db/migrate/20171106135924_issues_milestone_id_foreign_key.rb1
-rw-r--r--db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb16
-rw-r--r--db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb6
-rw-r--r--db/migrate/20171127151038_add_events_related_columns_to_merge_request_metrics.rb37
-rw-r--r--db/migrate/20171204204233_add_permanent_to_redirect_route.rb18
-rw-r--r--db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb19
-rw-r--r--db/migrate/20171212203433_create_clusters_applications_prometheus.rb18
-rw-r--r--db/migrate/20171216111734_clean_up_for_members.rb31
-rw-r--r--db/migrate/20171216112339_add_foreign_key_for_members.rb21
-rw-r--r--db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb30
-rw-r--r--db/migrate/20171222183504_add_jobs_cache_index_to_project.rb13
-rw-r--r--db/migrate/20171229225929_change_user_project_limit_not_null_and_remove_default.rb38
-rw-r--r--db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests_ce.rb15
-rw-r--r--db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb48
-rw-r--r--db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb28
-rw-r--r--db/post_migrate/20171106154015_remove_issues_branch_name.rb (renamed from db/migrate/20171106154015_remove_issues_branch_name.rb)1
-rw-r--r--db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb151
-rw-r--r--db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb68
-rw-r--r--db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb16
-rw-r--r--db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb29
-rw-r--r--db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb45
-rw-r--r--db/schema.rb33
-rw-r--r--doc/administration/auth/README.md1
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/gitlab_ou.png (renamed from doc/articles/how_to_configure_ldap_gitlab_ce/img/gitlab_ou.png)bin27877 -> 27877 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/ldap_ou.gif (renamed from doc/articles/how_to_configure_ldap_gitlab_ce/img/ldap_ou.gif)bin222162 -> 222162 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/user_auth.gif (renamed from doc/articles/how_to_configure_ldap_gitlab_ce/img/user_auth.gif)bin110971 -> 110971 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md267
-rw-r--r--doc/administration/auth/ldap.md3
-rw-r--r--doc/administration/high_availability/README.md1
-rw-r--r--doc/administration/high_availability/gitlab.md23
-rw-r--r--doc/administration/index.md37
-rw-r--r--doc/administration/integration/plantuml.md18
-rw-r--r--doc/administration/monitoring/index.md9
-rw-r--r--doc/administration/monitoring/performance/index.md72
-rw-r--r--doc/administration/monitoring/performance/introduction.md71
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md3
-rw-r--r--doc/administration/operations.md8
-rw-r--r--doc/administration/operations/index.md16
-rw-r--r--doc/administration/raketasks/check.md13
-rw-r--r--doc/administration/raketasks/maintenance.md19
-rw-r--r--doc/administration/reply_by_email.md8
-rw-r--r--doc/api/boards.md115
-rw-r--r--doc/api/commits.md1
-rw-r--r--doc/api/groups.md22
-rw-r--r--doc/api/issues.md45
-rw-r--r--doc/api/merge_requests.md45
-rw-r--r--doc/api/pages_domains.md1
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/api/repository_files.md1
-rw-r--r--doc/api/runners.md40
-rw-r--r--doc/api/services.md327
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/tags.md6
-rw-r--r--doc/articles/artifactory_and_gitlab/index.md279
-rw-r--r--doc/articles/how_to_configure_ldap_gitlab_ce/index.md268
-rw-r--r--doc/articles/index.md11
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/index.md2
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/docker/using_docker_build.md15
-rw-r--r--doc/ci/docker/using_docker_images.md61
-rw-r--r--doc/ci/examples/README.md33
-rw-r--r--doc/ci/examples/artifactory_and_gitlab/index.md282
-rw-r--r--doc/ci/examples/browser_performance.md50
-rw-r--r--doc/ci/examples/code_climate.md3
-rw-r--r--doc/ci/ssh_keys/README.md249
-rw-r--r--doc/ci/variables/README.md13
-rw-r--r--doc/customization/issue_closing.md4
-rw-r--r--doc/customization/welcome_message.md2
-rw-r--r--doc/development/README.md10
-rw-r--r--doc/development/automatic_ce_ee_merge.md93
-rw-r--r--doc/development/doc_styleguide.md4
-rw-r--r--doc/development/ee_features.md8
-rw-r--r--doc/development/fe_guide/axios.md2
-rw-r--r--doc/development/fe_guide/dropdowns.md12
-rw-r--r--doc/development/fe_guide/style_guide_js.md122
-rw-r--r--doc/development/gitaly.md23
-rw-r--r--doc/development/gotchas.md4
-rw-r--r--doc/development/i18n/externalization.md18
-rw-r--r--doc/development/i18n/index.md1
-rw-r--r--doc/development/limit_ee_conflicts.md347
-rw-r--r--doc/development/module_with_instance_variables.md242
-rw-r--r--doc/development/performance.md2
-rw-r--r--doc/development/sidekiq_style_guide.md59
-rw-r--r--doc/development/testing_guide/best_practices.md14
-rw-r--r--doc/development/testing_guide/end_to_end_tests.md80
-rw-r--r--doc/development/testing_guide/index.md10
-rw-r--r--doc/development/testing_guide/testing_levels.md3
-rw-r--r--doc/development/utilities.md92
-rw-r--r--doc/development/ux_guide/components.md18
-rw-r--r--doc/development/ux_guide/copy.md12
-rw-r--r--doc/development/ux_guide/img/james-mackey.pngbin11147 -> 32552 bytes
-rw-r--r--doc/development/ux_guide/img/karolina-plaskaty.pngbin33498 -> 29553 bytes
-rw-r--r--doc/development/ux_guide/img/matthieu-poirier.pngbin0 -> 32819 bytes
-rw-r--r--doc/development/ux_guide/img/nazim-ramesh.pngbin31163 -> 27475 bytes
-rw-r--r--doc/development/ux_guide/users.md291
-rw-r--r--doc/development/what_requires_downtime.md57
-rw-r--r--doc/development/writing_documentation.md169
-rw-r--r--doc/install/installation.md14
-rw-r--r--doc/install/kubernetes/index.md4
-rw-r--r--doc/integration/omniauth.md12
-rw-r--r--doc/monitoring/performance/introduction.md2
-rw-r--r--doc/operations/README.md2
-rw-r--r--doc/topics/authentication/index.md3
-rw-r--r--doc/topics/autodevops/img/auto_devops_settings.pngbin95233 -> 0 bytes
-rw-r--r--doc/topics/autodevops/index.md50
-rw-r--r--doc/topics/git/index.md4
-rw-r--r--doc/topics/git/troubleshooting_git.md82
-rw-r--r--doc/update/10.1-to-10.2.md4
-rw-r--r--doc/update/10.2-to-10.3.md360
-rw-r--r--doc/update/10.3-to-10.4.md360
-rw-r--r--doc/user/discussions/img/commit_comment_mr_context.pngbin0 -> 25854 bytes
-rw-r--r--doc/user/discussions/img/commit_comment_mr_discussions_tab.pngbin0 -> 15139 bytes
-rw-r--r--doc/user/discussions/img/merge_request_commits_tab.pngbin0 -> 12792 bytes
-rw-r--r--doc/user/discussions/index.md47
-rw-r--r--doc/user/group/img/transfer_project_to_other_group.pngbin66460 -> 0 bytes
-rw-r--r--doc/user/group/index.md56
-rw-r--r--doc/user/markdown.md11
-rw-r--r--doc/user/profile/index.md79
-rw-r--r--doc/user/project/clusters/index.md1
-rw-r--r--doc/user/project/index.md32
-rw-r--r--doc/user/project/integrations/jira.md8
-rw-r--r--doc/user/project/integrations/kubernetes.md5
-rw-r--r--doc/user/project/integrations/project_services.md2
-rw-r--r--doc/user/project/integrations/samples/prometheus.yml2
-rw-r--r--doc/user/project/merge_requests/fast_forward_merge.md4
-rw-r--r--doc/user/project/merge_requests/img/create_from_email.pngbin0 -> 152975 bytes
-rw-r--r--doc/user/project/merge_requests/img/ff_merge_mr.pngbin21380 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/ff_merge_rebase.pngbin0 -> 26945 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff_file_navigation.pngbin76052 -> 244736 bytes
-rw-r--r--doc/user/project/merge_requests/index.md10
-rw-r--r--doc/user/project/milestones/img/progress.pngbin23491 -> 0 bytes
-rw-r--r--doc/user/project/milestones/img/sidebar.pngbin0 -> 89947 bytes
-rw-r--r--doc/user/project/milestones/index.md10
-rw-r--r--doc/user/project/pipelines/settings.md2
-rw-r--r--doc/user/project/quick_actions.md2
-rw-r--r--doc/user/project/repository/gpg_signed_commits/index.md7
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/user/project/settings/index.md73
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md8
-rw-r--r--features/explore/groups.feature105
-rw-r--r--features/invites.feature45
-rw-r--r--features/project/ff_merge_requests.feature17
-rw-r--r--features/steps/explore/groups.rb88
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/invites.rb80
-rw-r--r--features/steps/profile/profile.rb2
-rw-r--r--features/steps/project/deploy_keys.rb4
-rw-r--r--features/steps/project/ff_merge_requests.rb22
-rw-r--r--features/steps/project/fork.rb4
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/source/browse_files.rb13
-rw-r--r--features/steps/project/source/markdown_render.rb2
-rw-r--r--features/steps/shared/builds.rb2
-rw-r--r--features/steps/shared/group.rb2
-rw-r--r--features/steps/shared/issuable.rb2
-rw-r--r--features/steps/shared/project.rb21
-rw-r--r--features/support/capybara.rb3
-rw-r--r--features/support/env.rb6
-rw-r--r--lib/after_commit_queue.rb10
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/boards.rb84
-rw-r--r--lib/api/boards_responses.rb50
-rw-r--r--lib/api/entities.rb27
-rw-r--r--lib/api/helpers.rb27
-rw-r--r--lib/api/helpers/internal_helpers.rb16
-rw-r--r--lib/api/internal.rb16
-rw-r--r--lib/api/issues.rb17
-rw-r--r--lib/api/labels.rb4
-rw-r--r--lib/api/members.rb4
-rw-r--r--lib/api/merge_requests.rb13
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/services.rb180
-rw-r--r--lib/api/tags.rb7
-rw-r--r--lib/api/time_tracking_endpoints.rb4
-rw-r--r--lib/api/v3/labels.rb2
-rw-r--r--lib/api/v3/time_tracking_endpoints.rb4
-rw-r--r--lib/backup/files.rb6
-rw-r--r--lib/banzai/filter/mermaid_filter.rb11
-rw-r--r--lib/banzai/filter/relative_link_filter.rb48
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb1
-rw-r--r--lib/banzai/filter/upload_link_filter.rb60
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/extracts_path.rb16
-rw-r--r--lib/feature.rb9
-rw-r--r--lib/gitlab/action_rate_limiter.rb47
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_type_change.rb54
-rw-r--r--lib/gitlab/background_migration/copy_column.rb39
-rw-r--r--lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb36
-rw-r--r--lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb16
-rw-r--r--lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb135
-rw-r--r--lib/gitlab/bare_repository_import/importer.rb6
-rw-r--r--lib/gitlab/bare_repository_import/repository.rb37
-rw-r--r--lib/gitlab/cache/request_cache.rb6
-rw-r--r--lib/gitlab/checks/project_moved.rb69
-rw-r--r--lib/gitlab/ci/ansi2html.rb2
-rw-r--r--lib/gitlab/ci/charts.rb2
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb12
-rw-r--r--lib/gitlab/ci/config/entry/node.rb4
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/base.rb7
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb42
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb61
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb12
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb12
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/repository.rb7
-rw-r--r--lib/gitlab/conflict/file_collection.rb13
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb4
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/issue_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/merge_request_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb4
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb8
-rw-r--r--lib/gitlab/cycle_analytics/staging_event_fetcher.rb4
-rw-r--r--lib/gitlab/database/migration_helpers.rb127
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb4
-rw-r--r--lib/gitlab/diff/file.rb35
-rw-r--r--lib/gitlab/ee_compat_check.rb6
-rw-r--r--lib/gitlab/email/handler/create_merge_request_handler.rb4
-rw-r--r--lib/gitlab/emoji.rb10
-rw-r--r--lib/gitlab/encoding_helper.rb36
-rw-r--r--lib/gitlab/exclusive_lease.rb11
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/blob.rb35
-rw-r--r--lib/gitlab/git/commit.rb20
-rw-r--r--lib/gitlab/git/commit_stats.rb9
-rw-r--r--lib/gitlab/git/conflict/file.rb4
-rw-r--r--lib/gitlab/git/conflict/resolution.rb15
-rw-r--r--lib/gitlab/git/conflict/resolver.rb117
-rw-r--r--lib/gitlab/git/gitlab_projects.rb294
-rw-r--r--lib/gitlab/git/index.rb12
-rw-r--r--lib/gitlab/git/operation_service.rb7
-rw-r--r--lib/gitlab/git/popen.rb12
-rw-r--r--lib/gitlab/git/remote_mirror.rb75
-rw-r--r--lib/gitlab/git/repository.rb294
-rw-r--r--lib/gitlab/git/repository_mirroring.rb41
-rw-r--r--lib/gitlab/git/storage/checker.rb24
-rw-r--r--lib/gitlab/git_access.rb17
-rw-r--r--lib/gitlab/gitaly_client.rb16
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb50
-rw-r--r--lib/gitlab/gitaly_client/conflicts_service.rb95
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb29
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb10
-rw-r--r--lib/gitlab/gitaly_client/remote_service.rb41
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb28
-rw-r--r--lib/gitlab/gitaly_client/util.rb8
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb30
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb2
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/identifier.rb5
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/command_line_util.rb7
-rw-r--r--lib/gitlab/import_export/import_export.yml4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb18
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb2
-rw-r--r--lib/gitlab/import_sources.rb1
-rw-r--r--lib/gitlab/kubernetes/helm.rb90
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb42
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb53
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb69
-rw-r--r--lib/gitlab/ldap/adapter.rb2
-rw-r--r--lib/gitlab/ldap/config.rb2
-rw-r--r--lib/gitlab/ldap/person.rb36
-rw-r--r--lib/gitlab/ldap/user.rb4
-rw-r--r--lib/gitlab/metrics/influx_db.rb3
-rw-r--r--lib/gitlab/metrics/method_call.rb29
-rw-r--r--lib/gitlab/metrics/prometheus.rb17
-rw-r--r--lib/gitlab/metrics/samplers/influx_sampler.rb24
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb29
-rw-r--r--lib/gitlab/metrics/subscribers/rails_cache.rb2
-rw-r--r--lib/gitlab/metrics/system.rb16
-rw-r--r--lib/gitlab/metrics/transaction.rb8
-rw-r--r--lib/gitlab/o_auth/provider.rb12
-rw-r--r--lib/gitlab/o_auth/user.rb38
-rw-r--r--lib/gitlab/search_results.rb20
-rw-r--r--lib/gitlab/seeder.rb2
-rw-r--r--lib/gitlab/setup_helper.rb61
-rw-r--r--lib/gitlab/shell.rb89
-rw-r--r--lib/gitlab/sidekiq_config.rb37
-rw-r--r--lib/gitlab/sidekiq_versioning.rb25
-rw-r--r--lib/gitlab/sidekiq_versioning/manager.rb12
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_base.rb14
-rw-r--r--lib/gitlab/tcp_checker.rb45
-rw-r--r--lib/gitlab/utils/strong_memoize.rb20
-rw-r--r--lib/gitlab/view/presenter/factory.rb2
-rw-r--r--lib/gitlab/visibility_level.rb12
-rw-r--r--lib/google_api/cloud_platform/client.rb18
-rw-r--r--lib/tasks/gettext.rake1
-rw-r--r--lib/tasks/gitlab/check.rake41
-rw-r--r--lib/tasks/gitlab/git.rake35
-rw-r--r--lib/tasks/gitlab/gitaly.rake57
-rw-r--r--lib/tasks/gitlab/task_helpers.rb19
-rw-r--r--lib/tasks/gitlab/tcp_check.rake20
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--locale/bg/gitlab.po573
-rw-r--r--locale/de/gitlab.po573
-rw-r--r--locale/eo/gitlab.po561
-rw-r--r--locale/es/gitlab.po561
-rw-r--r--locale/fr/gitlab.po613
-rw-r--r--locale/it/gitlab.po981
-rw-r--r--locale/ja/gitlab.po560
-rw-r--r--locale/ko/gitlab.po560
-rw-r--r--locale/nl_NL/gitlab.po565
-rw-r--r--locale/pl_PL/gitlab.po562
-rw-r--r--locale/pt_BR/gitlab.po685
-rw-r--r--locale/ru/gitlab.po712
-rw-r--r--locale/uk/gitlab.po780
-rw-r--r--locale/zh_CN/gitlab.po632
-rw-r--r--locale/zh_HK/gitlab.po560
-rw-r--r--locale/zh_TW/gitlab.po722
-rw-r--r--package.json13
-rw-r--r--qa/Dockerfile2
-rw-r--r--qa/Gemfile12
-rw-r--r--qa/Gemfile.lock84
-rw-r--r--qa/README.md9
-rw-r--r--qa/qa.rb62
-rw-r--r--qa/qa/factory/base.rb35
-rw-r--r--qa/qa/factory/dependency.rb38
-rw-r--r--qa/qa/factory/product.rb26
-rw-r--r--qa/qa/factory/repository/push.rb40
-rw-r--r--qa/qa/factory/resource/deploy_key.rb31
-rw-r--r--qa/qa/factory/resource/group.rb35
-rw-r--r--qa/qa/factory/resource/project.rb31
-rw-r--r--qa/qa/factory/resource/sandbox.rb34
-rw-r--r--qa/qa/factory/settings/hashed_storage.rb22
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/base.rb16
-rw-r--r--qa/qa/page/group/show.rb8
-rw-r--r--qa/qa/page/main/entry.rb26
-rw-r--r--qa/qa/page/main/login.rb8
-rw-r--r--qa/qa/page/mattermost/login.rb8
-rw-r--r--qa/qa/page/menu/admin.rb (renamed from qa/qa/page/admin/menu.rb)4
-rw-r--r--qa/qa/page/menu/main.rb (renamed from qa/qa/page/main/menu.rb)4
-rw-r--r--qa/qa/page/menu/side.rb29
-rw-r--r--qa/qa/page/project/settings/common.rb17
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb27
-rw-r--r--qa/qa/page/project/settings/repository.rb17
-rw-r--r--qa/qa/runtime/browser.rb139
-rw-r--r--qa/qa/runtime/env.rb15
-rw-r--r--qa/qa/runtime/scenario.rb8
-rw-r--r--qa/qa/runtime/user.rb11
-rw-r--r--qa/qa/scenario/entrypoint.rb1
-rw-r--r--qa/qa/scenario/gitlab/admin/hashed_storage.rb25
-rw-r--r--qa/qa/scenario/gitlab/group/create.rb27
-rw-r--r--qa/qa/scenario/gitlab/project/create.rb42
-rw-r--r--qa/qa/scenario/gitlab/sandbox/prepare.rb28
-rw-r--r--qa/qa/specs/config.rb63
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb6
-rw-r--r--qa/qa/specs/features/mattermost/group_create_spec.rb4
-rw-r--r--qa/qa/specs/features/mattermost/login_spec.rb25
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb22
-rw-r--r--qa/qa/specs/features/project/create_spec.rb4
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb4
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb28
-rw-r--r--qa/qa/specs/runner.rb2
-rw-r--r--qa/spec/factory/base_spec.rb88
-rw-r--r--qa/spec/factory/dependency_spec.rb59
-rw-r--r--qa/spec/factory/product_spec.rb44
-rw-r--r--qa/spec/runtime/env_spec.rb64
-rw-r--r--rubocop/cop/active_record_dependent.rb26
-rw-r--r--rubocop/cop/active_record_serialize.rb18
-rw-r--r--rubocop/cop/custom_error_class.rb64
-rw-r--r--rubocop/cop/gem_fetcher.rb37
-rw-r--r--rubocop/cop/gitlab/module_with_instance_variables.rb63
-rw-r--r--rubocop/cop/in_batches.rb16
-rw-r--r--rubocop/cop/include_sidekiq_worker.rb29
-rw-r--r--rubocop/cop/line_break_after_guard_clauses.rb100
-rw-r--r--rubocop/cop/migration/add_column.rb2
-rw-r--r--rubocop/cop/migration/add_concurrent_foreign_key.rb2
-rw-r--r--rubocop/cop/migration/add_concurrent_index.rb2
-rw-r--r--rubocop/cop/migration/add_index.rb2
-rw-r--r--rubocop/cop/migration/add_timestamps.rb2
-rw-r--r--rubocop/cop/migration/datetime.rb4
-rw-r--r--rubocop/cop/migration/hash_index.rb2
-rw-r--r--rubocop/cop/migration/remove_column.rb30
-rw-r--r--rubocop/cop/migration/remove_concurrent_index.rb2
-rw-r--r--rubocop/cop/migration/remove_index.rb2
-rw-r--r--rubocop/cop/migration/reversible_add_column_with_default.rb4
-rw-r--r--rubocop/cop/migration/safer_boolean_column.rb4
-rw-r--r--rubocop/cop/migration/timestamps.rb2
-rw-r--r--rubocop/cop/migration/update_column_in_batches.rb2
-rw-r--r--rubocop/cop/migration/update_large_table.rb4
-rw-r--r--rubocop/cop/polymorphic_associations.rb23
-rw-r--r--rubocop/cop/project_path_helper.rb2
-rw-r--r--rubocop/cop/redirect_with_status.rb44
-rw-r--r--rubocop/cop/rspec/env_assignment.rb5
-rw-r--r--rubocop/cop/rspec/single_line_hook.rb38
-rw-r--r--rubocop/cop/rspec/verbose_include_metadata.rb74
-rw-r--r--rubocop/cop/sidekiq_options_queue.rb27
-rw-r--r--rubocop/migration_helpers.rb6
-rw-r--r--rubocop/model_helpers.rb11
-rw-r--r--rubocop/rubocop.rb16
-rwxr-xr-xscripts/add-code-formatters18
-rw-r--r--scripts/create_mysql_user.sh2
-rwxr-xr-xscripts/gitaly-test-spawn3
-rwxr-xr-xscripts/lint-changelog-yaml4
-rw-r--r--scripts/pre-commit18
-rwxr-xr-xscripts/static-analysis2
-rwxr-xr-xscripts/trigger-build-omnibus17
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb6
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb4
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard_controller_spec.rb2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb19
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/notification_settings_controller_spec.rb2
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb8
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb12
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb18
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb2
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb18
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb28
-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.rb4
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb2
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb4
-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.rb2
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb53
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb2
-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/creations_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb76
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb14
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb67
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb13
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb46
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb2
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb38
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb49
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb2
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb4
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb2
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb6
-rw-r--r--spec/controllers/uploads_controller_spec.rb8
-rw-r--r--spec/controllers/users_controller_spec.rb4
-rw-r--r--spec/factories/abuse_reports.rb2
-rw-r--r--spec/factories/appearances.rb4
-rw-r--r--spec/factories/application_settings.rb2
-rw-r--r--spec/factories/award_emoji.rb2
-rw-r--r--spec/factories/boards.rb2
-rw-r--r--spec/factories/broadcast_messages.rb2
-rw-r--r--spec/factories/chat_names.rb2
-rw-r--r--spec/factories/chat_teams.rb2
-rw-r--r--spec/factories/ci/build_trace_section_names.rb2
-rw-r--r--spec/factories/ci/builds.rb27
-rw-r--r--spec/factories/ci/group_variables.rb2
-rw-r--r--spec/factories/ci/job_artifacts.rb2
-rw-r--r--spec/factories/ci/pipeline_schedule.rb2
-rw-r--r--spec/factories/ci/pipeline_schedule_variables.rb2
-rw-r--r--spec/factories/ci/pipeline_variables.rb2
-rw-r--r--spec/factories/ci/pipelines.rb2
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/ci/runners.rb2
-rw-r--r--spec/factories/ci/stages.rb2
-rw-r--r--spec/factories/ci/trigger_requests.rb2
-rw-r--r--spec/factories/ci/triggers.rb2
-rw-r--r--spec/factories/ci/variables.rb2
-rw-r--r--spec/factories/clusters/applications/helm.rb7
-rw-r--r--spec/factories/clusters/applications/ingress.rb35
-rw-r--r--spec/factories/clusters/clusters.rb2
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb2
-rw-r--r--spec/factories/clusters/providers/gcp.rb2
-rw-r--r--spec/factories/commit_statuses.rb2
-rw-r--r--spec/factories/commits.rb21
-rw-r--r--spec/factories/container_repositories.rb2
-rw-r--r--spec/factories/conversational_development_index_metrics.rb2
-rw-r--r--spec/factories/deploy_keys_projects.rb2
-rw-r--r--spec/factories/deployments.rb2
-rw-r--r--spec/factories/emails.rb2
-rw-r--r--spec/factories/environments.rb2
-rw-r--r--spec/factories/events.rb2
-rw-r--r--spec/factories/file_uploaders.rb2
-rw-r--r--spec/factories/fork_network_members.rb2
-rw-r--r--spec/factories/fork_networks.rb2
-rw-r--r--spec/factories/forked_project_links.rb2
-rw-r--r--spec/factories/gitaly/commit.rb2
-rw-r--r--spec/factories/gitaly/commit_author.rb2
-rw-r--r--spec/factories/gpg_key_subkeys.rb2
-rw-r--r--spec/factories/gpg_keys.rb2
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/group_custom_attributes.rb2
-rw-r--r--spec/factories/group_members.rb2
-rw-r--r--spec/factories/groups.rb2
-rw-r--r--spec/factories/identities.rb2
-rw-r--r--spec/factories/instance_configuration.rb2
-rw-r--r--spec/factories/issues.rb3
-rw-r--r--spec/factories/keys.rb65
-rw-r--r--spec/factories/label_links.rb2
-rw-r--r--spec/factories/label_priorities.rb2
-rw-r--r--spec/factories/labels.rb2
-rw-r--r--spec/factories/lfs_objects.rb2
-rw-r--r--spec/factories/lfs_objects_projects.rb2
-rw-r--r--spec/factories/lists.rb2
-rw-r--r--spec/factories/merge_requests.rb2
-rw-r--r--spec/factories/merge_requests_closing_issues.rb2
-rw-r--r--spec/factories/milestones.rb2
-rw-r--r--spec/factories/namespaces.rb2
-rw-r--r--spec/factories/notes.rb2
-rw-r--r--spec/factories/notification_settings.rb2
-rw-r--r--spec/factories/oauth_access_grants.rb2
-rw-r--r--spec/factories/oauth_access_tokens.rb2
-rw-r--r--spec/factories/oauth_applications.rb2
-rw-r--r--spec/factories/pages_domains.rb2
-rw-r--r--spec/factories/personal_access_tokens.rb2
-rw-r--r--spec/factories/project_auto_devops.rb2
-rw-r--r--spec/factories/project_custom_attributes.rb2
-rw-r--r--spec/factories/project_group_links.rb2
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/project_members.rb2
-rw-r--r--spec/factories/project_statistics.rb2
-rw-r--r--spec/factories/project_wikis.rb2
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/factories/protected_branches.rb2
-rw-r--r--spec/factories/protected_tags.rb2
-rw-r--r--spec/factories/releases.rb2
-rw-r--r--spec/factories/sent_notifications.rb2
-rw-r--r--spec/factories/sequences.rb2
-rw-r--r--spec/factories/service_hooks.rb2
-rw-r--r--spec/factories/services.rb3
-rw-r--r--spec/factories/snippets.rb2
-rw-r--r--spec/factories/spam_logs.rb2
-rw-r--r--spec/factories/subscriptions.rb2
-rw-r--r--spec/factories/system_hooks.rb2
-rw-r--r--spec/factories/system_note_metadata.rb2
-rw-r--r--spec/factories/timelogs.rb4
-rw-r--r--spec/factories/todos.rb2
-rw-r--r--spec/factories/trending_project.rb2
-rw-r--r--spec/factories/u2f_registrations.rb2
-rw-r--r--spec/factories/uploads.rb2
-rw-r--r--spec/factories/user_agent_details.rb2
-rw-r--r--spec/factories/user_custom_attributes.rb2
-rw-r--r--spec/factories/users.rb6
-rw-r--r--spec/factories/web_hook_log.rb2
-rw-r--r--spec/factories/wiki_directories.rb2
-rw-r--r--spec/factories/wiki_pages.rb2
-rw-r--r--spec/factories_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb6
-rw-r--r--spec/features/admin/admin_runners_spec.rb14
-rw-r--r--spec/features/admin/admin_system_info_spec.rb12
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb4
-rw-r--r--spec/features/atom/dashboard_spec.rb2
-rw-r--r--spec/features/atom/issues_spec.rb2
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/auto_deploy_spec.rb4
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb6
-rw-r--r--spec/features/boards/issue_ordering_spec.rb2
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb36
-rw-r--r--spec/features/boards/modal_filter_spec.rb6
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/calendar_spec.rb2
-rw-r--r--spec/features/commits_spec.rb10
-rw-r--r--spec/features/copy_as_gfm_spec.rb96
-rw-r--r--spec/features/cycle_analytics_spec.rb2
-rw-r--r--spec/features/dashboard/activity_spec.rb7
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb4
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb4
-rw-r--r--spec/features/dashboard/groups_list_spec.rb8
-rw-r--r--spec/features/dashboard/issues_spec.rb2
-rw-r--r--spec/features/dashboard/milestones_spec.rb2
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/projects_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb8
-rw-r--r--spec/features/dashboard/todos/todos_sorting_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb2
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb4
-rw-r--r--spec/features/explore/groups_spec.rb87
-rw-r--r--spec/features/global_search_spec.rb2
-rw-r--r--spec/features/group_variables_spec.rb2
-rw-r--r--spec/features/groups/labels/user_sees_links_to_issuables.rb15
-rw-r--r--spec/features/groups/members/manage_members.rb21
-rw-r--r--spec/features/groups/show_spec.rb16
-rw-r--r--spec/features/help_pages_spec.rb26
-rw-r--r--spec/features/invites_spec.rb97
-rw-r--r--spec/features/issues/award_emoji_spec.rb2
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb4
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb8
-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.rb7
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/form_spec.rb4
-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.rb6
-rw-r--r--spec/features/issues/keyboard_shortcut_spec.rb36
-rw-r--r--spec/features/issues/move_spec.rb10
-rw-r--r--spec/features/issues/notes_on_issues_spec.rb2
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/issues/todo_spec.rb2
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb12
-rw-r--r--spec/features/issues_spec.rb1007
-rw-r--r--spec/features/markdown_spec.rb2
-rw-r--r--spec/features/merge_requests/assign_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb2
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb2
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb4
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb2
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb2
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb6
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb2
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb2
-rw-r--r--spec/features/merge_requests/form_spec.rb6
-rw-r--r--spec/features/merge_requests/image_diff_notes_spec.rb (renamed from spec/features/merge_requests/image_diff_notes.rb)59
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb2
-rw-r--r--spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb2
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb8
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb2
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb2
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb2
-rw-r--r--spec/features/merge_requests/target_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb15
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb10
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb2
-rw-r--r--spec/features/merge_requests/widget_spec.rb2
-rw-r--r--spec/features/merge_requests/wip_message_spec.rb2
-rw-r--r--spec/features/milestone_spec.rb6
-rw-r--r--spec/features/password_reset_spec.rb19
-rw-r--r--spec/features/profile_spec.rb4
-rw-r--r--spec/features/profiles/keys_spec.rb1
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb8
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb12
-rw-r--r--spec/features/projects/activity/rss_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/badges/list_spec.rb2
-rw-r--r--spec/features/projects/blobs/edit_spec.rb8
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb4
-rw-r--r--spec/features/projects/clusters/applications_spec.rb2
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb155
-rw-r--r--spec/features/projects/clusters/interchangeability_spec.rb2
-rw-r--r--spec/features/projects/clusters/user_spec.rb8
-rw-r--r--spec/features/projects/clusters_spec.rb25
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/commits/rss_spec.rb2
-rw-r--r--spec/features/projects/compare_spec.rb2
-rw-r--r--spec/features/projects/deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/developer_views_empty_project_instructions_spec.rb2
-rw-r--r--spec/features/projects/edit_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb2
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb6
-rw-r--r--spec/features/projects/files/browse_files_spec.rb2
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb2
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb8
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/guest_navigation_menu_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin343232 -> 343092 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb12
-rw-r--r--spec/features/projects/issues/rss_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb30
-rw-r--r--spec/features/projects/labels/subscription_spec.rb2
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb2
-rw-r--r--spec/features/projects/labels/user_sees_links_to_issuables.rb75
-rw-r--r--spec/features/projects/main/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/main/rss_spec.rb2
-rw-r--r--spec/features/projects/members/anonymous_user_sees_members_spec.rb2
-rw-r--r--spec/features/projects/members/group_members_spec.rb2
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb2
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb2
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb2
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/list_spec.rb2
-rw-r--r--spec/features/projects/pages_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb6
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb36
-rw-r--r--spec/features/projects/ref_switcher_spec.rb78
-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.rb2
-rw-r--r--spec/features/projects/services/user_activates_slack_slash_command_spec.rb2
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb2
-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/tags/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb28
-rw-r--r--spec/features/projects/tree/create_file_spec.rb18
-rw-r--r--spec/features/projects/tree/rss_spec.rb2
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb28
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb9
-rw-r--r--spec/features/projects/user_browses_files_spec.rb2
-rw-r--r--spec/features/projects/user_creates_directory_spec.rb4
-rw-r--r--spec/features/projects/user_creates_files_spec.rb6
-rw-r--r--spec/features/projects/user_deletes_files_spec.rb4
-rw-r--r--spec/features/projects/user_edits_files_spec.rb8
-rw-r--r--spec/features/projects/user_replaces_files_spec.rb4
-rw-r--r--spec/features/projects/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb2
-rw-r--r--spec/features/projects_spec.rb6
-rw-r--r--spec/features/runners_spec.rb20
-rw-r--r--spec/features/signed_commits_spec.rb6
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb2
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb2
-rw-r--r--spec/features/tags/master_updates_tag_spec.rb2
-rw-r--r--spec/features/tags/master_views_tags_spec.rb5
-rw-r--r--spec/features/triggers_spec.rb6
-rw-r--r--spec/features/u2f_spec.rb2
-rw-r--r--spec/features/variables_spec.rb6
-rw-r--r--spec/finders/access_requests_finder_spec.rb4
-rw-r--r--spec/finders/group_descendants_finder_spec.rb35
-rw-r--r--spec/finders/group_projects_finder_spec.rb4
-rw-r--r--spec/finders/issues_finder_spec.rb6
-rw-r--r--spec/finders/labels_finder_spec.rb14
-rw-r--r--spec/finders/merge_requests_finder_spec.rb8
-rw-r--r--spec/finders/move_to_project_finder_spec.rb34
-rw-r--r--spec/finders/notes_finder_spec.rb6
-rw-r--r--spec/finders/personal_projects_finder_spec.rb2
-rw-r--r--spec/finders/snippets_finder_spec.rb4
-rw-r--r--spec/finders/todos_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/contributor.json18
-rw-r--r--spec/fixtures/api/schemas/contributors.json4
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json1
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_metrics.json21
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json (renamed from spec/fixtures/api/schemas/entities/merge_request.json)22
-rw-r--r--spec/fixtures/api/schemas/entities/user.json17
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/board.json86
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/boards.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/basic.json2
-rw-r--r--spec/fixtures/emails/valid_new_merge_request.eml2
-rw-r--r--spec/fixtures/emails/valid_new_merge_request_no_description.eml18
-rw-r--r--spec/fixtures/markdown.md.erb17
-rw-r--r--spec/helpers/labels_helper_spec.rb63
-rw-r--r--spec/helpers/markup_helper_spec.rb2
-rw-r--r--spec/helpers/members_helper_spec.rb8
-rw-r--r--spec/helpers/notes_helper_spec.rb22
-rw-r--r--spec/helpers/preferences_helper_spec.rb74
-rw-r--r--spec/helpers/runners_helper_spec.rb6
-rw-r--r--spec/helpers/tree_helper_spec.rb1
-rw-r--r--spec/javascripts/activities_spec.js5
-rw-r--r--spec/javascripts/behaviors/secret_values_spec.js146
-rw-r--r--spec/javascripts/blob/notebook/index_spec.js48
-rw-r--r--spec/javascripts/boards/board_blank_state_spec.js23
-rw-r--r--spec/javascripts/boards/board_card_spec.js13
-rw-r--r--spec/javascripts/boards/board_list_spec.js14
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js23
-rw-r--r--spec/javascripts/boards/boards_store_spec.js16
-rw-r--r--spec/javascripts/boards/components/board_spec.js3
-rw-r--r--spec/javascripts/boards/issue_card_spec.js3
-rw-r--r--spec/javascripts/boards/issue_spec.js4
-rw-r--r--spec/javascripts/boards/list_spec.js22
-rw-r--r--spec/javascripts/boards/mock_data.js35
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js5
-rw-r--r--spec/javascripts/clusters/services/mock_data.js6
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js7
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js3
-rw-r--r--spec/javascripts/commits_spec.js1
-rw-r--r--spec/javascripts/datetime_utility_spec.js186
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js3
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js24
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml4
-rw-r--r--spec/javascripts/fly_out_nav_spec.js4
-rw-r--r--spec/javascripts/gl_dropdown_spec.js6
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js12
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_spec.js26
-rw-r--r--spec/javascripts/groups/components/app_spec.js6
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js6
-rw-r--r--spec/javascripts/groups/components/item_actions_spec.js30
-rw-r--r--spec/javascripts/groups/components/item_caret_spec.js10
-rw-r--r--spec/javascripts/groups/components/item_stats_spec.js55
-rw-r--r--spec/javascripts/groups/components/item_stats_value_spec.js81
-rw-r--r--spec/javascripts/groups/components/item_type_icon_spec.js8
-rw-r--r--spec/javascripts/groups/mock_data.js22
-rw-r--r--spec/javascripts/helpers/locale_helper.js11
-rw-r--r--spec/javascripts/image_diff/helpers/badge_helper_spec.js9
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js177
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js30
-rw-r--r--spec/javascripts/issue_show/mock_data.js10
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/job_spec.js26
-rw-r--r--spec/javascripts/jobs/header_spec.js3
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js10
-rw-r--r--spec/javascripts/lib/utils/tick_formats_spec.js40
-rw-r--r--spec/javascripts/line_highlighter_spec.js3
-rw-r--r--spec/javascripts/locale/index_spec.js35
-rw-r--r--spec/javascripts/merge_request_notes_spec.js4
-rw-r--r--spec/javascripts/merge_request_spec.js3
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js50
-rw-r--r--spec/javascripts/monitoring/graph/deployment_spec.js121
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js114
-rw-r--r--spec/javascripts/monitoring/graph_spec.js14
-rw-r--r--spec/javascripts/monitoring/mock_data.js10
-rw-r--r--spec/javascripts/notes/components/comment_form_spec.js (renamed from spec/javascripts/notes/components/issue_comment_form_spec.js)2
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js (renamed from spec/javascripts/notes/components/issue_note_app_spec.js)81
-rw-r--r--spec/javascripts/notes/components/note_body_spec.js (renamed from spec/javascripts/notes/components/issue_note_body_spec.js)2
-rw-r--r--spec/javascripts/notes/components/note_form_spec.js (renamed from spec/javascripts/notes/components/issue_note_form_spec.js)2
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js (renamed from spec/javascripts/notes/components/issue_discussion_spec.js)4
-rw-r--r--spec/javascripts/notes/components/noteable_note_spec.js (renamed from spec/javascripts/notes/components/issue_note_spec.js)17
-rw-r--r--spec/javascripts/notes/mock_data.js332
-rw-r--r--spec/javascripts/notes_spec.js16
-rw-r--r--spec/javascripts/pager_spec.js15
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js31
-rw-r--r--spec/javascripts/pipelines/nav_controls_spec.js27
-rw-r--r--spec/javascripts/profile/account/components/delete_account_modal_spec.js8
-rw-r--r--spec/javascripts/projects/project_new_spec.js32
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js4
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_item_spec.js2
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_spec.js29
-rw-r--r--spec/javascripts/repo/components/ide_context_bar_spec.js49
-rw-r--r--spec/javascripts/repo/components/ide_repo_tree_spec.js (renamed from spec/javascripts/repo/components/repo_sidebar_spec.js)29
-rw-r--r--spec/javascripts/repo/components/ide_side_bar_spec.js43
-rw-r--r--spec/javascripts/repo/components/ide_spec.js (renamed from spec/javascripts/repo/components/repo_spec.js)16
-rw-r--r--spec/javascripts/repo/components/new_branch_form_spec.js4
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js30
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js149
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js81
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js38
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js8
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js6
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js4
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js28
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js5
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js4
-rw-r--r--spec/javascripts/repo/components/repo_preview_spec.js4
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js10
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js4
-rw-r--r--spec/javascripts/repo/helpers.js5
-rw-r--r--spec/javascripts/repo/lib/common/disposable_spec.js2
-rw-r--r--spec/javascripts/repo/lib/common/model_manager_spec.js4
-rw-r--r--spec/javascripts/repo/lib/common/model_spec.js4
-rw-r--r--spec/javascripts/repo/lib/decorations/controller_spec.js8
-rw-r--r--spec/javascripts/repo/lib/diff/controller_spec.js12
-rw-r--r--spec/javascripts/repo/lib/diff/diff_spec.js2
-rw-r--r--spec/javascripts/repo/lib/editor_options_spec.js2
-rw-r--r--spec/javascripts/repo/lib/editor_spec.js4
-rw-r--r--spec/javascripts/repo/monaco_loader_spec.js2
-rw-r--r--spec/javascripts/repo/stores/actions/branch_spec.js20
-rw-r--r--spec/javascripts/repo/stores/actions/file_spec.js75
-rw-r--r--spec/javascripts/repo/stores/actions/tree_spec.js325
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js102
-rw-r--r--spec/javascripts/repo/stores/getters_spec.js38
-rw-r--r--spec/javascripts/repo/stores/mutations/branch_spec.js6
-rw-r--r--spec/javascripts/repo/stores/mutations/file_spec.js4
-rw-r--r--spec/javascripts/repo/stores/mutations/tree_spec.js4
-rw-r--r--spec/javascripts/repo/stores/mutations_spec.js40
-rw-r--r--spec/javascripts/repo/stores/utils_spec.js43
-rw-r--r--spec/javascripts/right_sidebar_spec.js5
-rw-r--r--spec/javascripts/search_autocomplete_spec.js7
-rw-r--r--spec/javascripts/sidebar/sidebar_assignees_spec.js45
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js5
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js9
-rw-r--r--spec/javascripts/syntax_highlight_spec.js74
-rw-r--r--spec/javascripts/test_bundle.js12
-rw-r--r--spec/javascripts/todos_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js14
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js115
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js10
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js17
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js9
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js33
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/expand_button_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/file_icon_spec.js83
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js81
-rw-r--r--spec/javascripts/vue_shared/components/modal_spec.js66
-rw-r--r--spec/javascripts/vue_shared/components/panel_resizer_spec.js59
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js6
-rw-r--r--spec/javascripts/vue_shared/components/toggle_button_spec.js14
-rw-r--r--spec/lib/banzai/filter/mermaid_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb86
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb133
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb6
-rw-r--r--spec/lib/gitlab/action_rate_limiter_spec.rb29
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb27
-rw-r--r--spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb13
-rw-r--r--spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb19
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb19
-rw-r--r--spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb124
-rw-r--r--spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb17
-rw-r--r--spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb13
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb40
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb129
-rw-r--r--spec/lib/gitlab/checks/project_moved_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb57
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb185
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/status/build/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/external/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/external/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/stage/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb6
-rw-r--r--spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb14
-rw-r--r--spec/lib/gitlab/cycle_analytics/permissions_spec.rb8
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb101
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb25
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb12
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb4
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb32
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb15
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb19
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb6
-rw-r--r--spec/lib/gitlab/git/gitlab_projects_spec.rb337
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb352
-rw-r--r--spec/lib/gitlab/git_access_spec.rb70
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb90
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb47
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/identifier_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project.json770
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb26
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb (renamed from spec/lib/gitlab/kubernetes/helm_spec.rb)49
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb111
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb86
-rw-r--r--spec/lib/gitlab/ldap/adapter_spec.rb10
-rw-r--r--spec/lib/gitlab/ldap/person_spec.rb73
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb15
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb43
-rw-r--r--spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb23
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb23
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb8
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb70
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb2
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb8
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb23
-rw-r--r--spec/lib/gitlab/search_results_spec.rb42
-rw-r--r--spec/lib/gitlab/shell_spec.rb175
-rw-r--r--spec/lib/gitlab/sidekiq_config_spec.rb23
-rw-r--r--spec/lib/gitlab/sidekiq_versioning/manager_spec.rb22
-rw-r--r--spec/lib/gitlab/sidekiq_versioning_spec.rb44
-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/tcp_checker_spec.rb32
-rw-r--r--spec/lib/gitlab/user_access_spec.rb36
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb12
-rw-r--r--spec/lib/gitlab/view/presenter/factory_spec.rb8
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb27
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb24
-rw-r--r--spec/mailers/notify_spec.rb48
-rw-r--r--spec/migrations/clean_up_for_members_spec.rb78
-rw-r--r--spec/migrations/delete_conflicting_redirect_routes_spec.rb22
-rw-r--r--spec/migrations/issues_moved_to_id_foreign_key_spec.rb25
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb43
-rw-r--r--spec/migrations/migrate_kubernetes_service_to_new_clusters_architectures_spec.rb312
-rw-r--r--spec/migrations/migrate_stage_id_reference_in_background_spec.rb6
-rw-r--r--spec/migrations/migrate_stages_statuses_spec.rb6
-rw-r--r--spec/migrations/normalize_ldap_extern_uids_spec.rb6
-rw-r--r--spec/migrations/remove_assignee_id_from_issue_spec.rb37
-rw-r--r--spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb12
-rw-r--r--spec/migrations/schedule_merge_request_diff_migrations_spec.rb6
-rw-r--r--spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb6
-rw-r--r--spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb6
-rw-r--r--spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb24
-rw-r--r--spec/migrations/track_untracked_uploads_spec.rb12
-rw-r--r--spec/models/ability_spec.rb6
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb47
-rw-r--r--spec/models/ci/build_spec.rb69
-rw-r--r--spec/models/ci/pipeline_spec.rb18
-rw-r--r--spec/models/ci/runner_spec.rb28
-rw-r--r--spec/models/ci/trigger_spec.rb2
-rw-r--r--spec/models/clusters/applications/helm_spec.rb12
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb102
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb16
-rw-r--r--spec/models/clusters/cluster_spec.rb10
-rw-r--r--spec/models/commit_spec.rb44
-rw-r--r--spec/models/concerns/blocks_json_serialization_spec.rb17
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb20
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb73
-rw-r--r--spec/models/concerns/issuable_spec.rb4
-rw-r--r--spec/models/concerns/mentionable_spec.rb4
-rw-r--r--spec/models/concerns/milestoneish_spec.rb8
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb2
-rw-r--r--spec/models/deployment_spec.rb8
-rw-r--r--spec/models/diff_discussion_spec.rb48
-rw-r--r--spec/models/diff_note_spec.rb16
-rw-r--r--spec/models/event_spec.rb20
-rw-r--r--spec/models/generic_commit_status_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb4
-rw-r--r--spec/models/identity_spec.rb27
-rw-r--r--spec/models/issue_collection_spec.rb2
-rw-r--r--spec/models/issue_spec.rb40
-rw-r--r--spec/models/member_spec.rb8
-rw-r--r--spec/models/members/project_member_spec.rb8
-rw-r--r--spec/models/merge_request/metrics_spec.rb15
-rw-r--r--spec/models/merge_request_spec.rb126
-rw-r--r--spec/models/namespace_spec.rb50
-rw-r--r--spec/models/note_spec.rb24
-rw-r--r--spec/models/project_feature_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb20
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb101
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb2
-rw-r--r--spec/models/project_spec.rb132
-rw-r--r--spec/models/project_team_spec.rb4
-rw-r--r--spec/models/repository_spec.rb233
-rw-r--r--spec/models/route_spec.rb104
-rw-r--r--spec/models/service_spec.rb26
-rw-r--r--spec/models/user_spec.rb107
-rw-r--r--spec/policies/ci/build_policy_spec.rb4
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb92
-rw-r--r--spec/policies/ci/trigger_policy_spec.rb10
-rw-r--r--spec/policies/issue_policy_spec.rb12
-rw-r--r--spec/policies/project_snippet_policy_spec.rb6
-rw-r--r--spec/presenters/group_member_presenter_spec.rb138
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb71
-rw-r--r--spec/presenters/project_member_presenter_spec.rb138
-rw-r--r--spec/requests/api/access_requests_spec.rb4
-rw-r--r--spec/requests/api/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/boards_spec.rb179
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/environments_spec.rb2
-rw-r--r--spec/requests/api/files_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb64
-rw-r--r--spec/requests/api/internal_spec.rb64
-rw-r--r--spec/requests/api/issues_spec.rb34
-rw-r--r--spec/requests/api/jobs_spec.rb6
-rw-r--r--spec/requests/api/labels_spec.rb2
-rw-r--r--spec/requests/api/members_spec.rb4
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb38
-rw-r--r--spec/requests/api/notes_spec.rb6
-rw-r--r--spec/requests/api/pages_domains_spec.rb1
-rw-r--r--spec/requests/api/pipelines_spec.rb4
-rw-r--r--spec/requests/api/project_hooks_spec.rb6
-rw-r--r--spec/requests/api/project_milestones_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb10
-rw-r--r--spec/requests/api/repositories_spec.rb22
-rw-r--r--spec/requests/api/services_spec.rb10
-rw-r--r--spec/requests/api/tags_spec.rb38
-rw-r--r--spec/requests/api/todos_spec.rb6
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/v3/boards_spec.rb4
-rw-r--r--spec/requests/api/v3/commits_spec.rb8
-rw-r--r--spec/requests/api/v3/deployments_spec.rb2
-rw-r--r--spec/requests/api/v3/environments_spec.rb2
-rw-r--r--spec/requests/api/v3/files_spec.rb2
-rw-r--r--spec/requests/api/v3/groups_spec.rb2
-rw-r--r--spec/requests/api/v3/issues_spec.rb8
-rw-r--r--spec/requests/api/v3/labels_spec.rb2
-rw-r--r--spec/requests/api/v3/members_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb12
-rw-r--r--spec/requests/api/v3/milestones_spec.rb8
-rw-r--r--spec/requests/api/v3/notes_spec.rb6
-rw-r--r--spec/requests/api/v3/pipelines_spec.rb4
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb6
-rw-r--r--spec/requests/api/v3/projects_spec.rb10
-rw-r--r--spec/requests/api/v3/services_spec.rb4
-rw-r--r--spec/requests/api/v3/todos_spec.rb4
-rw-r--r--spec/requests/api/wikis_spec.rb30
-rw-r--r--spec/requests/git_http_spec.rb24
-rw-r--r--spec/requests/lfs_http_spec.rb30
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb2
-rw-r--r--spec/rubocop/cop/active_record_dependent_spec.rb33
-rw-r--r--spec/rubocop/cop/active_record_serialize_spec.rb33
-rw-r--r--spec/rubocop/cop/custom_error_class_spec.rb111
-rw-r--r--spec/rubocop/cop/gem_fetcher_spec.rb46
-rw-r--r--spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb157
-rw-r--r--spec/rubocop/cop/in_batches_spec.rb19
-rw-r--r--spec/rubocop/cop/include_sidekiq_worker_spec.rb33
-rw-r--r--spec/rubocop/cop/line_break_after_guard_clauses_spec.rb160
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb6
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_index_spec.rb6
-rw-r--r--spec/rubocop/cop/migration/add_timestamps_spec.rb12
-rw-r--r--spec/rubocop/cop/migration/datetime_spec.rb16
-rw-r--r--spec/rubocop/cop/migration/hash_index_spec.rb8
-rw-r--r--spec/rubocop/cop/migration/remove_column_spec.rb68
-rw-r--r--spec/rubocop/cop/migration/remove_concurrent_index_spec.rb6
-rw-r--r--spec/rubocop/cop/migration/remove_index_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb6
-rw-r--r--spec/rubocop/cop/migration/safer_boolean_column_spec.rb10
-rw-r--r--spec/rubocop/cop/migration/timestamps_spec.rb12
-rw-r--r--spec/rubocop/cop/migration/update_column_in_batches_spec.rb8
-rw-r--r--spec/rubocop/cop/migration/update_large_table_spec.rb10
-rw-r--r--spec/rubocop/cop/polymorphic_associations_spec.rb33
-rw-r--r--spec/rubocop/cop/project_path_helper_spec.rb6
-rw-r--r--spec/rubocop/cop/redirect_with_status_spec.rb86
-rw-r--r--spec/rubocop/cop/rspec/env_assignment_spec.rb6
-rw-r--r--spec/rubocop/cop/rspec/single_line_hook_spec.rb66
-rw-r--r--spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb53
-rw-r--r--spec/rubocop/cop/sidekiq_options_queue_spec.rb28
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb4
-rw-r--r--spec/serializers/event_entity_spec.rb13
-rw-r--r--spec/serializers/group_child_entity_spec.rb12
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb48
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb (renamed from spec/serializers/merge_request_entity_spec.rb)120
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb6
-rw-r--r--spec/services/boards/issues/create_service_spec.rb2
-rw-r--r--spec/services/boards/issues/list_service_spec.rb2
-rw-r--r--spec/services/boards/issues/move_service_spec.rb2
-rw-r--r--spec/services/boards/lists/create_service_spec.rb2
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb2
-rw-r--r--spec/services/check_gcp_project_billing_service_spec.rb31
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb31
-rw-r--r--spec/services/ci/register_job_service_spec.rb40
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb6
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb4
-rw-r--r--spec/services/clusters/applications/install_service_spec.rb12
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb2
-rw-r--r--spec/services/create_deployment_service_spec.rb2
-rw-r--r--spec/services/delete_branch_service_spec.rb2
-rw-r--r--spec/services/discussions/resolve_service_spec.rb2
-rw-r--r--spec/services/files/delete_service_spec.rb64
-rw-r--r--spec/services/files/multi_service_spec.rb144
-rw-r--r--spec/services/files/update_service_spec.rb2
-rw-r--r--spec/services/git_push_service_spec.rb12
-rw-r--r--spec/services/git_tag_push_service_spec.rb2
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb8
-rw-r--r--spec/services/issuable/destroy_service_spec.rb16
-rw-r--r--spec/services/issues/build_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb6
-rw-r--r--spec/services/issues/create_service_spec.rb18
-rw-r--r--spec/services/issues/move_service_spec.rb28
-rw-r--r--spec/services/issues/reopen_service_spec.rb4
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb12
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb2
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb2
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb6
-rw-r--r--spec/services/members/create_service_spec.rb2
-rw-r--r--spec/services/members/destroy_service_spec.rb4
-rw-r--r--spec/services/merge_requests/assign_issues_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb2
-rw-r--r--spec/services/merge_requests/close_service_spec.rb19
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb26
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb22
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb12
-rw-r--r--spec/services/merge_requests/create_service_spec.rb18
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb8
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb15
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb134
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb4
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb19
-rw-r--r--spec/services/merge_requests/update_service_spec.rb10
-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/notes/create_service_spec.rb2
-rw-r--r--spec/services/notes/destroy_service_spec.rb16
-rw-r--r--spec/services/notes/post_process_service_spec.rb2
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb10
-rw-r--r--spec/services/notes/update_service_spec.rb6
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb6
-rw-r--r--spec/services/projects/fork_service_spec.rb17
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb8
-rw-r--r--spec/services/projects/transfer_service_spec.rb12
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb20
-rw-r--r--spec/services/projects/update_service_spec.rb2
-rw-r--r--spec/services/protected_branches/create_service_spec.rb16
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb12
-rw-r--r--spec/services/reset_project_cache_service_spec.rb28
-rw-r--r--spec/services/search/snippet_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb36
-rw-r--r--spec/services/todo_service_spec.rb33
-rw-r--r--spec/services/update_merge_request_metrics_service_spec.rb42
-rw-r--r--spec/services/users/destroy_service_spec.rb17
-rw-r--r--spec/services/users/keys_count_service_spec.rb10
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/api/boards_shared_examples.rb180
-rw-r--r--spec/support/api/milestones_shared_examples.rb4
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb6
-rw-r--r--spec/support/api/v3/time_tracking_shared_examples.rb6
-rw-r--r--spec/support/background_migrations_matchers.rb15
-rw-r--r--spec/support/batch_loader.rb5
-rw-r--r--spec/support/capybara.rb3
-rw-r--r--spec/support/cluster_application_spec.rb105
-rw-r--r--spec/support/cookie_helper.rb4
-rw-r--r--spec/support/factory_girl.rb2
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb2
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb6
-rw-r--r--spec/support/markdown_feature.rb6
-rw-r--r--spec/support/mentionable_shared_examples.rb2
-rw-r--r--spec/support/reference_parser_shared_examples.rb2
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/services_shared_context.rb8
-rw-r--r--spec/support/shared_examples/requests/api/issuable_participants_examples.rb29
-rw-r--r--spec/support/stub_configuration.rb3
-rw-r--r--spec/support/stub_env.rb2
-rw-r--r--spec/support/test_env.rb13
-rw-r--r--spec/support/updating_mentions_shared_examples.rb2
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb38
-rw-r--r--spec/uploaders/records_uploads_spec.rb2
-rw-r--r--spec/views/events/event/_push.html.haml_spec.rb55
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb2
-rw-r--r--spec/views/projects/jobs/show.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tree/_blob_item.html.haml_spec.rb40
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb1
-rw-r--r--spec/views/shared/notes/_form.html.haml_spec.rb2
-rw-r--r--spec/workers/background_migration_worker_spec.rb23
-rw-r--r--spec/workers/check_gcp_project_billing_worker_spec.rb61
-rw-r--r--spec/workers/concerns/application_worker_spec.rb8
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb2
-rw-r--r--spec/workers/concerns/cronjob_queue_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/queue_spec.rb2
-rw-r--r--spec/workers/concerns/pipeline_queue_spec.rb11
-rw-r--r--spec/workers/concerns/project_import_options_spec.rb40
-rw-r--r--spec/workers/concerns/repository_check_queue_spec.rb2
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb31
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/rebase_worker_spec.rb27
-rw-r--r--spec/workers/repository_fork_worker_spec.rb29
-rw-r--r--spec/workers/repository_import_worker_spec.rb24
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb39
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb6
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb2
-rw-r--r--vendor/Dockerfile/CONTRIBUTING.md25
-rw-r--r--vendor/Dockerfile/LICENSE16
-rw-r--r--vendor/gitignore/Eagle.gitignore10
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore3
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore1
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore21
-rw-r--r--vendor/gitignore/Go.gitignore4
-rw-r--r--vendor/gitignore/Haskell.gitignore1
-rw-r--r--vendor/gitignore/Jekyll.gitignore1
-rw-r--r--vendor/gitignore/Node.gitignore2
-rw-r--r--vendor/gitignore/ROS.gitignore2
-rw-r--r--vendor/gitignore/Rails.gitignore4
-rw-r--r--vendor/gitignore/Symfony.gitignore4
-rw-r--r--vendor/gitignore/TeX.gitignore6
-rw-r--r--vendor/gitignore/Umbraco.gitignore7
-rw-r--r--vendor/gitignore/Unity.gitignore13
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore2
-rw-r--r--vendor/gitignore/VisualStudio.gitignore22
-rw-r--r--vendor/gitignore/WordPress.gitignore1
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml120
-rw-r--r--vendor/gitlab-ci-yml/CONTRIBUTING.md19
-rw-r--r--vendor/gitlab-ci-yml/Chef.gitlab-ci.yml51
-rw-r--r--vendor/gitlab-ci-yml/Go.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/LICENSE16
-rw-r--r--vendor/gitlab-ci-yml/Mono.gitlab-ci.yml42
-rw-r--r--vendor/gitlab-ci-yml/Rust.gitlab-ci.yml2
-rw-r--r--vendor/licenses.csv1108
-rw-r--r--vendor/project_templates/express.tar.gzbin5651 -> 5614 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25065 -> 25007 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50845 -> 50945 bytes
-rw-r--r--vendor/prometheus/values.yaml134
-rw-r--r--yarn.lock135
2209 files changed, 43171 insertions, 19752 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 42afed54371..d4905856e72 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -16,6 +16,7 @@ engines:
enabled: true
rubocop:
enabled: true
+ channel: "gitlab-rubocop-0-52"
ratings:
paths:
- Gemfile.lock
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 19540e4331e..4f47d3f0171 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,9 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-62.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
+
+.dedicated-runner: &dedicated-runner
+ retry: 1
+ tags:
+ - gitlab-org
.default-cache: &default-cache
key: "ruby-235-with-yarn"
@@ -42,11 +47,6 @@ stages:
- post-cleanup
# Predefined scopes
-.dedicated-runner: &dedicated-runner
- retry: 1
- tags:
- - gitlab-org
-
.tests-metadata-state: &tests-metadata-state
<<: *dedicated-runner
variables:
@@ -76,10 +76,19 @@ stages:
except:
- /(^docs[\/-].*|.*-docs$)/
+.except-qa: &except-qa
+ except:
+ - /(^qa[\/-].*|.*-qa$)/
+
+.except-docs-and-qa: &except-docs-and-qa
+ except:
+ - /(^docs[\/-].*|.*-docs$)/
+ - /(^qa[\/-].*|.*-qa$)/
+
.rspec-metadata: &rspec-metadata
<<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@@ -116,8 +125,8 @@ stages:
.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@@ -156,6 +165,7 @@ stages:
# Trigger a package build in omnibus-gitlab repository
#
package-qa:
+ <<: *dedicated-runner
image: ruby:2.4-alpine
before_script: []
stage: build
@@ -169,6 +179,8 @@ package-qa:
# Review docs base
.review-docs: &review-docs
+ <<: *dedicated-runner
+ <<: *except-qa
image: ruby:2.4-alpine
before_script:
- gem install gitlab --no-doc
@@ -213,7 +225,7 @@ review-docs-cleanup:
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
<<: *tests-metadata-state
- <<: *except-docs
+ <<: *except-docs-and-qa
stage: prepare
cache:
key: tests_metadata
@@ -265,6 +277,7 @@ flaky-examples-check:
except:
- master
- /(^docs[\/-].*|.*-docs$)/
+ - /(^qa[\/-].*|.*-qa$)/
artifacts:
expire_in: 30d
paths:
@@ -275,9 +288,9 @@ flaky-examples-check:
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env:
- <<: *use-pg
<<: *dedicated-runner
<<: *except-docs
+ <<: *use-pg
stage: prepare
cache:
<<: *default-cache
@@ -366,18 +379,18 @@ spinach-mysql 3 4: *spinach-metadata-mysql
SETUP_DB: "false"
.rake-exec: &rake-exec
- <<: *ruby-static-analysis
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
+ <<: *ruby-static-analysis
stage: test
script:
- bundle exec rake $CI_JOB_NAME
static-analysis:
- <<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
+ <<: *ruby-static-analysis
stage: test
script:
- scripts/static-analysis
@@ -387,6 +400,7 @@ static-analysis:
# - Make sure cURL examples in API docs use the full switches
docs lint:
<<: *dedicated-runner
+ <<: *except-qa
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
cache: {}
@@ -409,6 +423,7 @@ downtime_check:
- tags
- /^[\d-]+-stable(-ee)?$/
- /(^docs[\/-].*|.*-docs$)/
+ - /(^qa[\/-].*|.*-qa$)/
ee_compat_check:
<<: *rake-exec
@@ -416,6 +431,7 @@ ee_compat_check:
- master
- tags
- /^[\d-]+-stable(-ee)?/
+ - /^security-/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
retry: 0
@@ -429,7 +445,7 @@ ee_compat_check:
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
script:
@@ -443,10 +459,16 @@ db:migrate:reset-mysql:
<<: *db-migrate-reset
<<: *use-mysql
+db:check-schema-pg:
+ <<: *db-migrate-reset
+ <<: *use-pg
+ script:
+ - source scripts/schema_changed.sh
+
.migration-paths: &migration-paths
<<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
stage: test
variables:
SETUP_DB: "false"
@@ -472,7 +494,7 @@ migration:path-mysql:
.db-rollback: &db-rollback
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
script:
@@ -487,43 +509,39 @@ db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
-.db-seed_fu: &db-seed_fu
+.gitlab-setup: &gitlab-setup
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
variables:
SIZE: "1"
SETUP_DB: "false"
CREATE_DB_USER: "true"
+ FIXTURE_PATH: db/fixtures/development
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- - bundle exec rake db:setup db:seed_fu
+ - scripts/gitaly-test-spawn
+ - force=yes bundle exec rake gitlab:setup
artifacts:
when: on_failure
expire_in: 1d
paths:
- log/development.log
-db:seed_fu-pg:
- <<: *db-seed_fu
+gitlab:setup-pg:
+ <<: *gitlab-setup
<<: *use-pg
-db:seed_fu-mysql:
- <<: *db-seed_fu
+gitlab:setup-mysql:
+ <<: *gitlab-setup
<<: *use-mysql
-db:check-schema-pg:
- <<: *db-migrate-reset
- <<: *use-pg
- script:
- - source scripts/schema_changed.sh
-
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
dependencies: []
@@ -544,10 +562,10 @@ gitlab:assets:compile:
- webpack-report/
karma:
- <<: *use-pg
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
+ <<: *use-pg
stage: test
variables:
BABEL_ENV: "coverage"
@@ -579,13 +597,22 @@ codequality:
script:
- cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-v2 analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml
artifacts:
paths: [codeclimate.json]
+sast:
+ image: registry.gitlab.com/gitlab-org/gl-sast:latest
+ before_script: []
+ script:
+ - /app/bin/run .
+ artifacts:
+ paths: [gl-sast-report.json]
+
qa:internal:
+ <<: *dedicated-runner
<<: *except-docs
stage: test
variables:
@@ -598,7 +625,7 @@ qa:internal:
coverage:
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: post-test
services: []
@@ -617,7 +644,7 @@ coverage:
lint:javascript:report:
<<: *dedicated-runner
- <<: *except-docs
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: post-test
dependencies:
@@ -675,8 +702,9 @@ cache gems:
- master@gitlab-org/gitlab-ee
gitlab_git_test:
+ <<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
variables:
SETUP_DB: "false"
script:
diff --git a/.rubocop.yml b/.rubocop.yml
index c427f219a0d..9adc2fae7a8 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,1226 +1,34 @@
-require:
- - rubocop-rspec
- - rubocop-gitlab-security
- - ./rubocop/rubocop
+inherit_gem:
+ gitlab-styles:
+ - rubocop-default.yml
inherit_from: .rubocop_todo.yml
+require: ./rubocop/rubocop
AllCops:
- TargetRubyVersion: 2.3
TargetRailsVersion: 4.2
- # Cop names are not d§splayed in offense messages by default. Change behavior
- # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
- # option.
- DisplayCopNames: true
- # Style guide URLs are not displayed in offense messages by default. Change
- # behavior by overriding DisplayStyleGuide, or by giving the
- # -S/--display-style-guide option.
- DisplayStyleGuide: false
- # Exclude some GitLab files
Exclude:
- 'vendor/**/*'
- 'node_modules/**/*'
- 'db/*'
- 'db/fixtures/**/*'
+ - 'db/geo/*'
- 'tmp/**/*'
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
-# Gems in consecutive lines should be alphabetically sorted
-Bundler/OrderedGems:
- Enabled: false
+# Gitlab ###################################################################
-# Layout ######################################################################
-
-# Check indentation of private/protected visibility modifiers.
-Layout/AccessModifierIndentation:
- Enabled: true
-
-# Align the elements of an array literal if they span more than one line.
-Layout/AlignArray:
- Enabled: true
-
-# Align the elements of a hash literal if they span more than one line.
-Layout/AlignHash:
- Enabled: true
-
-# Here we check if the parameters on a multi-line method call or
-# definition are aligned.
-Layout/AlignParameters:
- Enabled: false
-
-# Put end statement of multiline block on its own line.
-Layout/BlockEndNewline:
- Enabled: true
-
-# Indentation of when in a case/when/[else/]end.
-Layout/CaseIndentation:
- Enabled: true
-
-# Indentation of comments.
-Layout/CommentIndentation:
- Enabled: true
-
-# Multi-line method chaining should be done with leading dots.
-Layout/DotPosition:
- Enabled: true
- EnforcedStyle: leading
-
-# Align elses and elsifs correctly.
-Layout/ElseAlignment:
- Enabled: true
-
-# Add an empty line after magic comments to separate them from code.
-Layout/EmptyLineAfterMagicComment:
- Enabled: false
-
-# Use empty lines between defs.
-Layout/EmptyLineBetweenDefs:
- Enabled: true
-
-# Don't use several empty lines in a row.
-Layout/EmptyLines:
- Enabled: true
-
-# Keep blank lines around access modifiers.
-Layout/EmptyLinesAroundAccessModifier:
- Enabled: true
-
-# Keeps track of empty lines around block bodies.
-Layout/EmptyLinesAroundBlockBody:
- Enabled: true
-
-# Keeps track of empty lines around class bodies.
-Layout/EmptyLinesAroundClassBody:
- Enabled: true
-
-# Keeps track of empty lines around exception handling keywords.
-Layout/EmptyLinesAroundExceptionHandlingKeywords:
- Enabled: false
-
-# Keeps track of empty lines around method bodies.
-Layout/EmptyLinesAroundMethodBody:
- Enabled: true
-
-# Keeps track of empty lines around module bodies.
-Layout/EmptyLinesAroundModuleBody:
- Enabled: true
-
-# Use Unix-style line endings.
-Layout/EndOfLine:
- Enabled: true
-
-# Checks for a line break before the first parameter in a multi-line method
-# parameter definition.
-Layout/FirstMethodParameterLineBreak:
- Enabled: true
-
-# Keep indentation straight.
-Layout/IndentationConsistency:
- Enabled: true
-
-# Use 2 spaces for indentation.
-Layout/IndentationWidth:
- Enabled: true
-
-# Checks the indentation of the first line of the right-hand-side of a
-# multi-line assignment.
-Layout/IndentAssignment:
- Enabled: true
-
-# This cops checks the indentation of the here document bodies.
-Layout/IndentHeredoc:
- Enabled: false
-
-# Comments should start with a space.
-Layout/LeadingCommentSpace:
- Enabled: true
-
-# Checks that the closing brace in an array literal is either on the same line
-# as the last array element, or a new line.
-Layout/MultilineArrayBraceLayout:
- Enabled: true
- EnforcedStyle: symmetrical
-
-# Ensures newlines after multiline block do statements.
-Layout/MultilineBlockLayout:
- Enabled: true
-
-# Checks that the closing brace in a hash literal is either on the same line as
-# the last hash element, or a new line.
-Layout/MultilineHashBraceLayout:
- Enabled: true
- EnforcedStyle: symmetrical
-
-# Checks that the closing brace in a method call is either on the same line as
-# the last method argument, or a new line.
-Layout/MultilineMethodCallBraceLayout:
- Enabled: false
- EnforcedStyle: symmetrical
-
-# Checks indentation of method calls with the dot operator that span more than
-# one line.
-Layout/MultilineMethodCallIndentation:
- Enabled: false
-
-# Checks that the closing brace in a method definition is symmetrical with
-# respect to the opening brace and the method parameters.
-Layout/MultilineMethodDefinitionBraceLayout:
- Enabled: false
-
-# Checks indentation of binary operations that span more than one line.
-Layout/MultilineOperationIndentation:
- Enabled: true
- EnforcedStyle: indented
-
-# Use spaces after colons.
-Layout/SpaceAfterColon:
- Enabled: true
-
-# Use spaces after commas.
-Layout/SpaceAfterComma:
- Enabled: true
-
-# Do not put a space between a method name and the opening parenthesis in a
-# method definition.
-Layout/SpaceAfterMethodName:
- Enabled: true
-
-# Tracks redundant space after the ! operator.
-Layout/SpaceAfterNot:
- Enabled: true
-
-# Use spaces after semicolons.
-Layout/SpaceAfterSemicolon:
- Enabled: true
-
-# Use space around equals in parameter default
-Layout/SpaceAroundEqualsInParameterDefault:
- Enabled: true
-
-# Use a space around keywords if appropriate.
-Layout/SpaceAroundKeyword:
- Enabled: true
-
-# Use a single space around operators.
-Layout/SpaceAroundOperators:
- Enabled: true
-
-# Checks that block braces have or don't have a space before the opening
-# brace depending on configuration.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: space, no_space
-Layout/SpaceBeforeBlockBraces:
- Enabled: true
-
-# No spaces before commas.
-Layout/SpaceBeforeComma:
- Enabled: true
-
-# Checks for missing space between code and a comment on the same line.
-Layout/SpaceBeforeComment:
- Enabled: true
-
-# No spaces before semicolons.
-Layout/SpaceBeforeSemicolon:
- Enabled: true
-
-# Checks for spaces inside square brackets.
-Layout/SpaceInsideBrackets:
- Enabled: true
-
-# Use spaces inside hash literal braces - or don't.
-Layout/SpaceInsideHashLiteralBraces:
- Enabled: true
-
-# No spaces inside range literals.
-Layout/SpaceInsideRangeLiteral:
- Enabled: true
-
-# Checks for padding/surrounding spaces inside string interpolation.
-Layout/SpaceInsideStringInterpolation:
- EnforcedStyle: no_space
- Enabled: true
-
-# No hard tabs.
-Layout/Tab:
- Enabled: true
-
-# Checks trailing blank lines and final newline.
-Layout/TrailingBlankLines:
- Enabled: true
-
-# Avoid trailing whitespace.
-Layout/TrailingWhitespace:
- Enabled: true
-
-# Style #######################################################################
-
-# Check the naming of accessor methods for get_/set_.
-Style/AccessorMethodName:
- Enabled: false
-
-# Use alias_method instead of alias.
-Style/Alias:
- EnforcedStyle: prefer_alias_method
- Enabled: true
-
-# Whether `and` and `or` are banned only in conditionals (conditionals)
-# or completely (always).
-Style/AndOr:
- Enabled: true
-
-# Use `Array#join` instead of `Array#*`.
-Style/ArrayJoin:
- Enabled: true
-
-# Use only ascii symbols in comments.
-Style/AsciiComments:
- Enabled: true
-
-# Use only ascii symbols in identifiers.
-Style/AsciiIdentifiers:
- Enabled: true
-
-# Checks for uses of Module#attr.
-Style/Attr:
- Enabled: true
-
-# Avoid the use of BEGIN blocks.
-Style/BeginBlock:
- Enabled: true
-
-# Do not use block comments.
-Style/BlockComments:
- Enabled: true
-
-# Avoid using {...} for multi-line blocks (multiline chaining is # always
-# ugly). Prefer {...} over do...end for single-line blocks.
-Style/BlockDelimiters:
- Enabled: true
-
- # This cop checks for braces around the last parameter in a method call
-# if the last parameter is a hash.
-Style/BracesAroundHashParameters:
- Enabled: false
-
-# This cop checks for uses of the case equality operator(===).
-Style/CaseEquality:
- Enabled: false
-
-# Checks for uses of character literals.
-Style/CharacterLiteral:
- Enabled: true
-
-# Use CamelCase for classes and modules.'
-Style/ClassAndModuleCamelCase:
- Enabled: true
-
-# Checks style of children classes and modules.
-Style/ClassAndModuleChildren:
- Enabled: false
-
-# Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.
-Style/ClassCheck:
- Enabled: true
-
-# Use self when defining module/class methods.
-Style/ClassMethods:
- Enabled: true
-
-# Avoid the use of class variables.
-Style/ClassVars:
- Enabled: true
-
-# This cop checks for methods invoked via the :: operator instead
-# of the . operator (like FileUtils::rmdir instead of FileUtils.rmdir).
-Style/ColonMethodCall:
- Enabled: true
-
-# This cop checks that comment annotation keywords are written according
-# to guidelines.
-Style/CommentAnnotation:
- Enabled: false
-
-# Check for `if` and `case` statements where each branch is used for
-# assignment to the same variable when using the return of the
-# condition can be used instead.
-Style/ConditionalAssignment:
- Enabled: true
-
-# Constants should use SCREAMING_SNAKE_CASE.
-Style/ConstantName:
- Enabled: true
-
-# Use def with parentheses when there are arguments.
-Style/DefWithParentheses:
- Enabled: true
-
-# Document classes and non-namespace modules.
-Style/Documentation:
- Enabled: false
-
-# This cop checks for uses of double negation (!!) to convert something
-# to a boolean value. As this is both cryptic and usually redundant, it
-# should be avoided.
-Style/DoubleNegation:
- Enabled: false
-
-# Avoid the use of END blocks.
-Style/EndBlock:
- Enabled: true
-
-# Favor the use of Fixnum#even? && Fixnum#odd?
-Style/EvenOdd:
- Enabled: true
-
-# Use snake_case for source file names.
-Style/FileName:
- Enabled: true
-
-# Checks for flip flops.
-Style/FlipFlop:
- Enabled: true
-
-# Checks use of for or each in multiline loops.
-Style/For:
- Enabled: true
-
-# Use a consistent style for format string tokens.
-Style/FormatStringToken:
- Enabled: false
-
-# Checks if there is a magic comment to enforce string literals
-Style/FrozenStringLiteralComment:
- Enabled: false
-
-# Do not introduce global variables.
-Style/GlobalVars:
- Enabled: true
- Exclude:
- - 'lib/backup/**/*'
- - 'lib/tasks/**/*'
-
-# Prefer Ruby 1.9 hash syntax `{ a: 1, b: 2 }`
-# over 1.8 syntax `{ :a => 1, :b => 2 }`.
-Style/HashSyntax:
- Enabled: true
-
-# Checks that conditional statements do not have an identical line at the
-# end of each branch, which can validly be moved out of the conditional.
-Style/IdenticalConditionalBranches:
- Enabled: true
-
-# Do not use if x; .... Use the ternary operator instead.
-Style/IfWithSemicolon:
- Enabled: true
-
-# Use Kernel#loop for infinite loops.
-Style/InfiniteLoop:
- Enabled: true
-
-# Use the inverse method instead of `!.method`
-# if an inverse method is defined.
-Style/InverseMethods:
- Enabled: false
-
-# Use lambda.call(...) instead of lambda.(...).
-Style/LambdaCall:
- Enabled: true
-
-# Checks if the method definitions have or don't have parentheses.
-Style/MethodDefParentheses:
- Enabled: true
-
-# Use the configured style when naming methods.
-Style/MethodName:
- Enabled: true
-
-# Checks for usage of `extend self` in modules.
-Style/ModuleFunction:
- Enabled: false
-
-# Avoid multi-line chains of blocks.
-Style/MultilineBlockChain:
- Enabled: true
-
-# Do not use then for multi-line if/unless.
-Style/MultilineIfThen:
- Enabled: true
-
-# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
-Style/MultilineTernaryOperator:
- Enabled: true
-
-# Avoid comparing a variable with multiple items in a conditional,
-# use Array#include? instead.
-Style/MultipleComparison:
- Enabled: false
-
-# This cop checks whether some constant value isn't a
-# mutable literal (e.g. array or hash).
-Style/MutableConstant:
- Enabled: true
- Exclude:
- - 'db/migrate/**/*'
- - 'db/post_migrate/**/*'
-
-# Favor unless over if for negative conditions (or control flow or).
-Style/NegatedIf:
- Enabled: true
-
-# Avoid using nested modifiers.
-Style/NestedModifier:
- Enabled: true
-
-# Use one expression per branch in a ternary operator.
-Style/NestedTernaryOperator:
- Enabled: true
-
-# Prefer x.nil? to x == nil.
-Style/NilComparison:
- Enabled: true
-
-# Checks for redundant nil checks.
-Style/NonNilCheck:
- Enabled: true
-
-# Use ! instead of not.
-Style/Not:
- Enabled: true
-
-# Add underscores to large numeric literals to improve their readability.
-Style/NumericLiterals:
- Enabled: false
-
-# Favor the ternary operator(?:) over if/then/else/end constructs.
-Style/OneLineConditional:
- Enabled: true
-
-# When defining binary operators, name the argument other.
-Style/OpMethod:
- Enabled: true
-
-# Don't use parentheses around the condition of an if/unless/while.
-Style/ParenthesesAroundCondition:
- Enabled: true
-
-# This cop (by default) checks for uses of methods Hash#has_key? and
-# Hash#has_value? where it enforces Hash#key? and Hash#value?
-# It is configurable to enforce the inverse, using `verbose` method
-# names also.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: short, verbose
-Style/PreferredHashMethods:
- Enabled: false
-
-# Checks for an obsolete RuntimeException argument in raise/fail.
-Style/RedundantException:
- Enabled: true
-
-# Checks for parentheses that seem not to serve any purpose.
-Style/RedundantParentheses:
- Enabled: true
-
-# Don't use semicolons to terminate expressions.
-Style/Semicolon:
- Enabled: true
-
-# Checks for proper usage of fail and raise.
-Style/SignalException:
- EnforcedStyle: only_raise
- Enabled: true
-
-# Check for the usage of parentheses around stabby lambda arguments.
-Style/StabbyLambdaParentheses:
- EnforcedStyle: require_parentheses
- Enabled: true
-
-# Checks if uses of quotes match the configured preference.
-Style/StringLiterals:
- Enabled: false
-
-# Checks if configured preferred methods are used over non-preferred.
-Style/StringMethods:
- PreferredMethods:
- intern: to_sym
- Enabled: true
-
-# Use %i or %I for arrays of symbols.
-Style/SymbolArray:
- Enabled: false
-
-# This cop checks for trailing comma in array and hash literals.
-Style/TrailingCommaInLiteral:
- Enabled: true
- EnforcedStyleForMultiline: no_comma
-
-# This cop checks for trailing comma in argument lists.
-Style/TrailingCommaInArguments:
- Enabled: true
- EnforcedStyleForMultiline: no_comma
-
-# Checks for %W when interpolation is not needed.
-Style/UnneededCapitalW:
- Enabled: true
-
-# Checks for %q/%Q when single quotes or double quotes would do.
-Style/UnneededPercentQ:
- Enabled: false
-
-# Don't interpolate global, instance and class variables directly in strings.
-Style/VariableInterpolation:
- Enabled: true
-
-# Use the configured style when naming variables.
-Style/VariableName:
- EnforcedStyle: snake_case
- Enabled: true
-
-# Use the configured style when numbering variables.
-Style/VariableNumber:
- Enabled: false
-
-# Use when x then ... for one-line cases.
-Style/WhenThen:
- Enabled: true
-
-# Checks for redundant do after while or until.
-Style/WhileUntilDo:
- Enabled: true
-
-# Favor modifier while/until usage when you have a single-line body.
-Style/WhileUntilModifier:
- Enabled: true
-
-# Use %w or %W for arrays of words.
-Style/WordArray:
- Enabled: true
-
-# Do not use literals as the first operand of a comparison.
-Style/YodaCondition:
- Enabled: false
-
-# Use `proc` instead of `Proc.new`.
-Style/Proc:
- Enabled: true
-
-# Use `spam?` instead of `is_spam?`
-# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
-# NamePrefix: is_, has_, have_
-# NamePrefixBlacklist: is_, has_, have_
-# NameWhitelist: is_a?
-Style/PredicateName:
- Enabled: true
- NamePrefixBlacklist: is_
- Exclude:
- - 'spec/**/*'
- - 'features/**/*'
-
-# Metrics #####################################################################
-
-# A calculated magnitude based on number of assignments,
-# branches, and conditions.
-Metrics/AbcSize:
- Enabled: true
- Max: 54.28
-
-# This cop checks if the length of a block exceeds some maximum value.
-Metrics/BlockLength:
- Enabled: false
-
-# Avoid excessive block nesting.
-Metrics/BlockNesting:
- Enabled: true
- Max: 4
-
-# Avoid classes longer than 100 lines of code.
-Metrics/ClassLength:
- Enabled: false
-
-# A complexity metric that is strongly correlated to the number
-# of test cases needed to validate a method.
-Metrics/CyclomaticComplexity:
- Enabled: true
- Max: 13
-
-# Limit lines to 80 characters.
-Metrics/LineLength:
- Enabled: false
-
-# Avoid methods longer than 10 lines of code.
-Metrics/MethodLength:
- Enabled: false
-
-# Avoid modules longer than 100 lines of code.
-Metrics/ModuleLength:
- Enabled: false
-
-# Avoid parameter lists longer than three or four parameters.
-Metrics/ParameterLists:
- Enabled: true
- Max: 8
-
-# A complexity metric geared towards measuring complexity for a human reader.
-Metrics/PerceivedComplexity:
- Enabled: true
- Max: 14
-
-# Lint ########################################################################
-
-# Checks for ambiguous block association with method when param passed without
-# parentheses.
-Lint/AmbiguousBlockAssociation:
- Enabled: false
-
-# Checks for ambiguous operators in the first argument of a method invocation
-# without parentheses.
-Lint/AmbiguousOperator:
- Enabled: true
-
-# This cop checks for ambiguous regexp literals in the first argument of
-# a method invocation without parentheses.
-Lint/AmbiguousRegexpLiteral:
- Enabled: false
-
-# This cop checks for assignments in the conditions of
-# if/while/until.
-Lint/AssignmentInCondition:
- Enabled: false
-
-# Align block ends correctly.
-Lint/BlockAlignment:
- Enabled: true
-
-# Default values in optional keyword arguments and optional ordinal arguments
-# should not refer back to the name of the argument.
-Lint/CircularArgumentReference:
- Enabled: true
-
-# Checks for condition placed in a confusing position relative to the keyword.
-Lint/ConditionPosition:
- Enabled: true
-
-# Check for debugger calls.
-Lint/Debugger:
- Enabled: true
-
-# Align ends corresponding to defs correctly.
-Lint/DefEndAlignment:
- Enabled: true
-
-# Check for deprecated class method calls.
-Lint/DeprecatedClassMethods:
- Enabled: true
-
-# Check for immutable argument given to each_with_object.
-Lint/EachWithObjectArgument:
- Enabled: true
-
-# Check for odd code arrangement in an else block.
-Lint/ElseLayout:
- Enabled: true
-
-# Checks for empty ensure block.
-Lint/EmptyEnsure:
- Enabled: true
-
-# Checks for the presence of `when` branches without a body.
-Lint/EmptyWhen:
- Enabled: true
-
-# Align ends correctly.
-Lint/EndAlignment:
- Enabled: true
-
-# END blocks should not be placed inside method definitions.
-Lint/EndInMethod:
- Enabled: true
-
-# Do not use return in an ensure block.
-Lint/EnsureReturn:
- Enabled: true
-
-# Catches floating-point literals too large or small for Ruby to represent.
-Lint/FloatOutOfRange:
- Enabled: true
-
-# The number of parameters to format/sprint must match the fields.
-Lint/FormatParameterMismatch:
- Enabled: true
-
-# This cop checks for *rescue* blocks with no body.
-Lint/HandleExceptions:
- Enabled: false
-
-# Checks for adjacent string literals on the same line, which could better be
-# represented as a single string literal.
-Lint/ImplicitStringConcatenation:
- Enabled: true
-
-# Checks for attempts to use `private` or `protected` to set the visibility
-# of a class method, which does not work.
-Lint/IneffectiveAccessModifier:
- Enabled: false
-
-# Checks for invalid character literals with a non-escaped whitespace
-# character.
-Lint/InvalidCharacterLiteral:
- Enabled: true
-
-# Checks of literals used in conditions.
-Lint/LiteralInCondition:
- Enabled: true
-
-# Checks for literals used in interpolation.
-Lint/LiteralInInterpolation:
- Enabled: true
-
-# This cop checks for uses of *begin...end while/until something*.
-Lint/Loop:
- Enabled: false
-
-# Do not use nested method definitions.
-Lint/NestedMethodDefinition:
- Enabled: true
-
-# Do not omit the accumulator when calling `next` in a `reduce`/`inject` block.
-Lint/NextWithoutAccumulator:
- Enabled: true
-
-# Checks for method calls with a space before the opening parenthesis.
-Lint/ParenthesesAsGroupedExpression:
- Enabled: true
-
-# Checks for `rand(1)` calls. Such calls always return `0` and most likely
-# a mistake.
-Lint/RandOne:
- Enabled: true
-
-# Use parentheses in the method call to avoid confusion about precedence.
-Lint/RequireParentheses:
- Enabled: true
-
-# Avoid rescuing the Exception class.
-Lint/RescueException:
- Enabled: true
-
-# Checks for the order which exceptions are rescued to avoid rescueing a less specific exception before a more specific exception.
-Lint/ShadowedException:
- Enabled: false
-
-# This cop looks for use of the same name as outer local variables
-# for block arguments or block local variables.
-Lint/ShadowingOuterLocalVariable:
- Enabled: false
-
-# Checks for Object#to_s usage in string interpolation.
-Lint/StringConversionInInterpolation:
- Enabled: true
-
-# Do not use prefix `_` for a variable that is used.
-Lint/UnderscorePrefixedVariableName:
- Enabled: true
-
-# This cop checks for using Fixnum or Bignum constant
-Lint/UnifiedInteger:
- Enabled: true
-
-# Checks for rubocop:disable comments that can be removed.
-# Note: this cop is not disabled when disabling all cops.
-# It must be explicitly disabled.
-Lint/UnneededDisable:
- Enabled: false
-
-# This cop checks for unneeded usages of splat expansion
-Lint/UnneededSplatExpansion:
- Enabled: false
-
-# Unreachable code.
-Lint/UnreachableCode:
- Enabled: true
-
-# This cop checks for unused block arguments.
-Lint/UnusedBlockArgument:
- Enabled: false
-
-# This cop checks for unused method arguments.
-Lint/UnusedMethodArgument:
- Enabled: false
-
-# Checks for useless access modifiers.
-Lint/UselessAccessModifier:
- Enabled: true
-
-# Checks for useless assignment to a local variable.
-Lint/UselessAssignment:
- Enabled: true
-
-# Checks for comparison of something with itself.
-Lint/UselessComparison:
- Enabled: true
-
-# Checks for useless `else` in `begin..end` without `rescue`.
-Lint/UselessElseWithoutRescue:
- Enabled: true
-
-# Checks for useless setter call to a local variable.
-Lint/UselessSetterCall:
- Enabled: true
-
-# Possible use of operator/literal/variable in void context.
-Lint/Void:
- Enabled: true
-
-# Performance #################################################################
-
-# Use `caller(n..n)` instead of `caller`.
-Performance/Caller:
- Enabled: false
-
-# Use `casecmp` rather than `downcase ==`.
-Performance/Casecmp:
- Enabled: true
-
-# Use `str.{start,end}_with?(x, ..., y, ...)` instead of
-# `str.{start,end}_with?(x, ...) || str.{start,end}_with?(y, ...)`.
-Performance/DoubleStartEndWith:
- Enabled: true
-
-# Use `strip` instead of `lstrip.rstrip`.
-Performance/LstripRstrip:
- Enabled: true
-
-# Use `Range#cover?` instead of `Range#include?`.
-Performance/RangeInclude:
- Enabled: true
-
-# This cop identifies the use of a `&block` parameter and `block.call`
-# where `yield` would do just as well.
-Performance/RedundantBlockCall:
- Enabled: true
-
-# This cop identifies use of `Regexp#match` or `String#match in a context
-# where the integral return value of `=~` would do just as well.
-Performance/RedundantMatch:
- Enabled: true
-
-# This cop identifies places where `Hash#merge!` can be replaced by
-# `Hash#[]=`.
-Performance/RedundantMerge:
- Enabled: true
- MaxKeyValuePairs: 1
-
-# Use `sort` instead of `sort_by { |x| x }`.
-Performance/RedundantSortBy:
- Enabled: true
-
-# Use `start_with?` instead of a regex match anchored to the beginning of a
-# string.
-Performance/StartWith:
- Enabled: true
-
-# Use `tr` instead of `gsub` when you are replacing the same number of
-# characters. Use `delete` instead of `gsub` when you are deleting
-# characters.
-Performance/StringReplacement:
- Enabled: true
-
-# Checks for `.times.map` calls.
-Performance/TimesMap:
- Enabled: true
-
-# Security ####################################################################
-
-# This cop checks for the use of JSON class methods which have potential
-# security issues.
-Security/JSONLoad:
- Enabled: true
-
-# This cop checks for the use of *Kernel#eval*.
-Security/Eval:
- Enabled: true
-
-# Rails #######################################################################
-
-# Enables Rails cops.
-Rails:
- Enabled: true
-
-# Enforces consistent use of action filter methods.
-Rails/ActionFilter:
- Enabled: true
- EnforcedStyle: action
-
-# Check that models subclass ApplicationRecord.
-Rails/ApplicationRecord:
- Enabled: false
-
-# Enforce using `blank?` and `present?`.
-Rails/Blank:
- Enabled: false
-
-# Checks the correct usage of date aware methods, such as `Date.today`,
-# `Date.current`, etc.
-Rails/Date:
- Enabled: false
-
-# Prefer delegate method for delegations.
-# Disabled per https://gitlab.com/gitlab-org/gitlab-ce/issues/35869
-Rails/Delegate:
- Enabled: false
-
-# This cop checks dynamic `find_by_*` methods.
-Rails/DynamicFindBy:
- Enabled: false
-
-# This cop enforces that 'exit' calls are not used within a rails app.
-Rails/Exit:
- Enabled: true
- Exclude:
- - lib/gitlab/upgrader.rb
- - 'lib/backup/**/*'
-
-# Prefer `find_by` over `where.first`.
-Rails/FindBy:
- Enabled: true
-
-# Prefer `all.find_each` over `all.find`.
-Rails/FindEach:
- Enabled: true
-
-# Prefer has_many :through to has_and_belongs_to_many.
-Rails/HasAndBelongsToMany:
- Enabled: true
-
-# This cop is used to identify usages of http methods like `get`, `post`,
-# `put`, `patch` without the usage of keyword arguments in your tests and
-# change them to use keyword args.
-Rails/HttpPositionalArguments:
- Enabled: false
-
-# Checks for calls to puts, print, etc.
-Rails/Output:
- Enabled: true
- Exclude:
- - lib/gitlab/seeder.rb
- - lib/gitlab/upgrader.rb
- - 'lib/backup/**/*'
- - 'lib/tasks/**/*'
-
-# This cop checks for the use of output safety calls like html_safe and
-# raw.
-Rails/OutputSafety:
- Enabled: false
-
-# Checks for incorrect grammar when using methods like `3.day.ago`.
-Rails/PluralizationGrammar:
- Enabled: true
-
-# Enforce using `blank?` and `present?`.
-Rails/Present:
- Enabled: false
-
-# Checks for `read_attribute(:attr)` and `write_attribute(:attr, val)`.
-Rails/ReadWriteAttribute:
- Enabled: false
-
-# Do not assign relative date to constants.
-Rails/RelativeDateConstant:
- Enabled: false
-
-# Checks the arguments of ActiveRecord scopes.
-Rails/ScopeArgs:
- Enabled: true
-
-# This cop checks for the use of Time methods without zone.
-Rails/TimeZone:
- Enabled: false
-
-# This cop checks for the use of old-style attribute validation macros.
-Rails/Validation:
- Enabled: true
-
-# RSpec #######################################################################
-
-# Check that instances are not being stubbed globally.
-RSpec/AnyInstance:
- Enabled: false
-
-# Check for expectations where `be(...)` can replace `eql(...)`.
-RSpec/BeEql:
- Enabled: true
-
-# We don't enforce this as we use this technique in a few places.
-RSpec/BeforeAfterAll:
- Enabled: false
-
-# Check that the first argument to the top level describe is the tested class or
-# module.
-RSpec/DescribeClass:
- Enabled: false
-
-# Checks that the second argument to `describe` specifies a method.
-RSpec/DescribeMethod:
- Enabled: false
-
-# Avoid describing symbols.
-RSpec/DescribeSymbol:
- Enabled: true
-
-# Checks that tests use `described_class`.
-RSpec/DescribedClass:
- Enabled: true
-
-# Checks if an example group does not include any tests.
-RSpec/EmptyExampleGroup:
- Enabled: true
- CustomIncludeMethods:
- - run_permission_checks
-
-# Checks for long example.
-RSpec/ExampleLength:
- Enabled: false
- Max: 5
-
-# Do not use should when describing your tests.
-RSpec/ExampleWording:
- Enabled: false
- CustomTransform:
- be: is
- have: has
- not: does not
- IgnoredWords: []
-
-# Checks for `expect(...)` calls containing literal values.
-RSpec/ExpectActual:
- Enabled: true
-
-# Checks for opportunities to use `expect { … }.to output`.
-RSpec/ExpectOutput:
- Enabled: true
-
-# Checks the file and folder naming of the spec file.
-RSpec/FilePath:
- Enabled: true
- IgnoreMethods: true
- Exclude:
- - 'qa/**/*'
- - 'spec/javascripts/fixtures/*'
- - 'spec/requests/api/v3/*'
-
-# Checks if there are focused specs.
-RSpec/Focus:
- Enabled: true
-
-# Checks the arguments passed to `before`, `around`, and `after`.
-RSpec/HookArgument:
- Enabled: true
- EnforcedStyle: implicit
-
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: is_expected, should
-RSpec/ImplicitExpect:
- Enabled: true
- EnforcedStyle: is_expected
-
-# Checks for the usage of instance variables.
-RSpec/InstanceVariable:
- Enabled: false
-
-# Checks for `subject` definitions that come after `let` definitions.
-RSpec/LeadingSubject:
- Enabled: false
-
-# Checks unreferenced `let!` calls being used for test setup.
-RSpec/LetSetup:
- Enabled: false
-
-# Check that chains of messages are not being stubbed.
-RSpec/MessageChain:
- Enabled: false
-
-# Checks that message expectations are set using spies.
-RSpec/MessageSpies:
- Enabled: false
-
-# Checks for multiple top-level describes.
-RSpec/MultipleDescribes:
- Enabled: false
-
-# Checks if examples contain too many `expect` calls.
-RSpec/MultipleExpectations:
- Enabled: false
-
-# Checks for explicitly referenced test subjects.
-RSpec/NamedSubject:
- Enabled: false
-
-# Checks for nested example groups.
-RSpec/NestedGroups:
- Enabled: false
-
-# Enforces the usage of the same method on all negative message expectations.
-RSpec/NotToNot:
- EnforcedStyle: not_to
- Enabled: true
-
-# Check for repeated description strings in example groups.
-RSpec/RepeatedDescription:
- Enabled: false
-
-# Ensure RSpec hook blocks are always multi-line.
-RSpec/SingleLineHook:
- Enabled: true
- Exclude:
- - 'spec/factories/*'
- - 'spec/requests/api/v3/*'
-
-# Checks for stubbed test subjects.
-RSpec/SubjectStub:
- Enabled: false
-
-# Prefer using verifying doubles over normal doubles.
-RSpec/VerifiedDoubles:
- Enabled: false
-
-# GitlabSecurity ##############################################################
-
-GitlabSecurity/DeepMunge:
- Enabled: true
- Exclude:
- - 'lib/**/*.rake'
- - 'spec/**/*'
-
-# To be enabled by https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13610
-GitlabSecurity/JsonSerialization:
- Enabled: false
-
-GitlabSecurity/PublicSend:
- Enabled: true
- Exclude:
- - 'config/**/*'
- - 'db/**/*'
- - 'features/**/*'
- - 'lib/**/*.rake'
- - 'qa/**/*'
- - 'spec/**/*'
-
-GitlabSecurity/RedirectToParamsUpdate:
- Enabled: true
- Exclude:
- - 'lib/**/*.rake'
- - 'spec/**/*'
-
-GitlabSecurity/SqlInjection:
- Enabled: true
- Exclude:
- - 'lib/**/*.rake'
- - 'spec/**/*'
-
-GitlabSecurity/SystemCommandInjection:
- Enabled: true
+Gitlab/ModuleWithInstanceVariables:
+ Enable: true
Exclude:
- - 'lib/**/*.rake'
- - 'spec/**/*'
+ # We ignore Rails helpers right now because it's hard to workaround it
+ - app/helpers/**/*_helper.rb
+ - ee/app/helpers/**/*_helper.rb
+ # We ignore Rails mailers right now because it's hard to workaround it
+ - app/mailers/emails/**/*.rb
+ - ee/**/emails/**/*.rb
+ # We ignore spec helpers because it usually doesn't matter
+ - spec/support/**/*.rb
+ - features/steps/**/*.rb
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index cdf97d1d842..085dc153596 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,53 +1,111 @@
# This configuration was generated by
-# `rubocop --auto-gen-config --exclude-limit 0`
-# on 2017-07-10 01:48:30 +0900 using RuboCop version 0.49.1.
+# `rubocop --auto-gen-config`
+# on 2017-12-14 12:04:26 +0100 using RuboCop version 0.52.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 181
+# Offense count: 174
+Capybara/CurrentPathExpectation:
+ Enabled: false
+
+# Offense count: 951
+Capybara/FeatureMethods:
+ Enabled: false
+
+# Offense count: 24
+FactoryBot/DynamicAttributeDefinedStatically:
+ Exclude:
+ - 'spec/factories/broadcast_messages.rb'
+ - 'spec/factories/ci/builds.rb'
+ - 'spec/factories/ci/runners.rb'
+ - 'spec/factories/clusters/applications/helm.rb'
+ - 'spec/factories/clusters/applications/ingress.rb'
+ - 'spec/factories/clusters/platforms/kubernetes.rb'
+ - 'spec/factories/emails.rb'
+ - 'spec/factories/gpg_keys.rb'
+ - 'spec/factories/group_members.rb'
+ - 'spec/factories/merge_requests.rb'
+ - 'spec/factories/notes.rb'
+ - 'spec/factories/oauth_access_grants.rb'
+ - 'spec/factories/project_members.rb'
+ - 'spec/factories/todos.rb'
+ - 'spec/factories/uploads.rb'
+
+# Offense count: 65
+# Cop supports --auto-correct.
+Layout/EmptyLinesAroundArguments:
+ Enabled: false
+
+# Offense count: 249
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Enabled: false
-# Offense count: 119
+# Offense count: 82
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Layout/IndentArray:
Enabled: false
-# Offense count: 208
+# Offense count: 239
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Layout/IndentHash:
Enabled: false
-# Offense count: 8
+# Offense count: 15
# Cop supports --auto-correct.
-# Configuration parameters: AllowForAlignment.
-Layout/SpaceBeforeFirstArg:
- Enabled: false
+# Configuration parameters: .
+# SupportedStyles: space, no_space
+# SupportedStylesForEmptyBraces: space, no_space
+Layout/SpaceBeforeBlockBraces:
+ EnforcedStyle: space
+ EnforcedStyleForEmptyBraces: space
-# Offense count: 64
+# Offense count: 11
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+# Configuration parameters: AllowForAlignment.
+Layout/SpaceBeforeFirstArg:
+ Exclude:
+ - 'config/routes/project.rb'
+ - 'db/migrate/20170506185517_add_foreign_key_pipeline_schedules_and_pipelines.rb'
+ - 'features/steps/project/source/browse_files.rb'
+ - 'features/steps/project/source/markdown_render.rb'
+ - 'lib/api/runners.rb'
+ - 'spec/features/search/user_uses_search_filters_spec.rb'
+ - 'spec/routing/project_routing_spec.rb'
+ - 'spec/services/system_note_service_spec.rb'
+
+# Offense count: 93
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space
Layout/SpaceInLambdaLiteral:
Enabled: false
-# Offense count: 256
+# Offense count: 1
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
+# SupportedStyles: space, no_space, compact
+# SupportedStylesForEmptyBrackets: space, no_space
+Layout/SpaceInsideArrayLiteralBrackets:
+ Exclude:
+ - 'spec/lib/gitlab/import_export/relation_factory_spec.rb'
+
+# Offense count: 323
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 135
+# Offense count: 146
# Cop supports --auto-correct.
Layout/SpaceInsideParens:
Enabled: false
@@ -55,178 +113,535 @@ Layout/SpaceInsideParens:
# Offense count: 14
# Cop supports --auto-correct.
Layout/SpaceInsidePercentLiteralDelimiters:
+ Exclude:
+ - 'lib/gitlab/git_access.rb'
+ - 'lib/gitlab/health_checks/fs_shards_check.rb'
+ - 'spec/lib/gitlab/health_checks/fs_shards_check_spec.rb'
+
+# Offense count: 25
+Lint/DuplicateMethods:
+ Exclude:
+ - 'app/models/application_setting.rb'
+ - 'app/models/commit.rb'
+ - 'app/models/note.rb'
+ - 'app/services/merge_requests/merge_service.rb'
+ - 'lib/bitbucket/representation/repo.rb'
+ - 'lib/declarative_policy/base.rb'
+ - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
+ - 'lib/gitlab/cycle_analytics/base_event_fetcher.rb'
+ - 'lib/gitlab/diff/formatters/base_formatter.rb'
+ - 'lib/gitlab/git/blob.rb'
+ - 'lib/gitlab/git/repository.rb'
+ - 'lib/gitlab/git/tree.rb'
+ - 'lib/gitlab/git/wiki_page.rb'
+ - 'lib/gitlab/ldap/person.rb'
+ - 'lib/gitlab/o_auth/user.rb'
+
+# Offense count: 4
+Lint/InterpolationCheck:
+ Exclude:
+ - 'spec/features/issues/filtered_search/filter_issues_spec.rb'
+ - 'spec/features/users_spec.rb'
+ - 'spec/services/quick_actions/interpret_service_spec.rb'
+
+# Offense count: 198
+# Configuration parameters: MaximumRangeSize.
+Lint/MissingCopEnableDirective:
+ Enabled: false
+
+# Offense count: 2
+Lint/NestedPercentLiteral:
+ Exclude:
+ - 'lib/gitlab/git/repository.rb'
+ - 'spec/support/email_format_shared_examples.rb'
+
+# Offense count: 1
+Lint/ReturnInVoidContext:
+ Exclude:
+ - 'app/models/project.rb'
+
+# Offense count: 1
+# Configuration parameters: IgnoreImplicitReferences.
+Lint/ShadowedArgument:
+ Exclude:
+ - 'lib/gitlab/database/sha_attribute.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Lint/UnneededRequireStatement:
+ Exclude:
+ - 'db/post_migrate/20161221153951_rename_reserved_project_names.rb'
+ - 'db/post_migrate/20170313133418_rename_more_reserved_project_names.rb'
+ - 'lib/declarative_policy.rb'
+
+# Offense count: 9
+Lint/UriEscapeUnescape:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/models/project_services/drone_ci_service.rb'
+ - 'spec/lib/google_api/auth_spec.rb'
+ - '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: 2
+Naming/ConstantName:
+ Exclude:
+ - 'lib/gitlab/import_sources.rb'
+ - 'lib/gitlab/ssh_public_key.rb'
+
+# Offense count: 11
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: lowercase, uppercase
+Naming/HeredocDelimiterCase:
+ Exclude:
+ - 'spec/lib/gitlab/diff/parser_spec.rb'
+ - 'spec/lib/json_web_token/rsa_token_spec.rb'
+ - 'spec/models/commit_spec.rb'
+ - 'spec/support/repo_helpers.rb'
+ - 'spec/support/seed_repo.rb'
+
+# Offense count: 101
+# Configuration parameters: Blacklist.
+# Blacklist: END, (?-mix:EO[A-Z]{1})
+Naming/HeredocDelimiterNaming:
+ Enabled: false
+
+# Offense count: 28
+# Cop supports --auto-correct.
+# Configuration parameters: AutoCorrect.
+Performance/HashEachMethods:
+ Enabled: false
+
+# Offense count: 1
+Performance/UnfreezeString:
+ Exclude:
+ - 'features/steps/project/commits/commits.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Performance/UriDefaultParser:
+ Exclude:
+ - 'lib/gitlab/url_sanitizer.rb'
+
+# Offense count: 3745
+# Configuration parameters: Prefixes.
+# Prefixes: when, with, without
+RSpec/ContextWording:
Enabled: false
-# Offense count: 272
+# Offense count: 291
RSpec/EmptyLineAfterFinalLet:
Enabled: false
-# Offense count: 181
+# Offense count: 180
RSpec/EmptyLineAfterSubject:
Enabled: false
-# Offense count: 9
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+# Offense count: 220
+RSpec/ExpectInHook:
+ Enabled: false
+
+# Offense count: 7
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: implicit, each, example
+RSpec/HookArgument:
+ Exclude:
+ - 'spec/spec_helper.rb'
+ - 'spec/support/carrierwave.rb'
+ - 'spec/support/db_cleaner.rb'
+ - 'spec/support/gitaly.rb'
+ - 'spec/support/setup_builds_storage.rb'
+
+# Offense count: 19
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: it_behaves_like, it_should_behave_like
RSpec/ItBehavesLike:
- Enabled: false
+ Exclude:
+ - 'spec/lib/gitlab/git/commit_spec.rb'
+ - 'spec/lib/gitlab/git/repository_spec.rb'
+ - 'spec/lib/gitlab/shell_spec.rb'
+ - 'spec/services/notification_service_spec.rb'
+ - 'spec/workers/git_garbage_collect_worker_spec.rb'
-# Offense count: 4
+# Offense count: 5
RSpec/IteratedExpectation:
- Enabled: false
+ Exclude:
+ - 'spec/features/admin/admin_settings_spec.rb'
+ - 'spec/features/merge_requests/diff_notes_resolve_spec.rb'
+ - 'spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb'
+ - 'spec/lib/gitlab/gitlab_import/client_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/client_spec.rb'
+
+# Offense count: 75
+RSpec/LetBeforeExamples:
+ Exclude:
+ - 'spec/controllers/projects/commit_controller_spec.rb'
+ - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/user_reference_filter_spec.rb'
+ - 'spec/lib/gitlab/email/handler/create_issue_handler_spec.rb'
+ - 'spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb'
+ - 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
+ - 'spec/models/commit_range_spec.rb'
+ - 'spec/models/milestone_spec.rb'
+ - 'spec/models/project_services/packagist_service_spec.rb'
+ - 'spec/models/repository_spec.rb'
+ - 'spec/rubocop/cop/migration/update_column_in_batches_spec.rb'
+ - 'spec/serializers/pipeline_details_entity_spec.rb'
+ - 'spec/views/ci/lints/show.html.haml_spec.rb'
-# Offense count: 2
+# Offense count: 1
+RSpec/MultipleSubjects:
+ Exclude:
+ - 'spec/services/merge_requests/create_from_issue_service_spec.rb'
+
+# Offense count: 4
RSpec/OverwritingSetup:
+ Exclude:
+ - 'spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb'
+ - 'spec/models/email_spec.rb'
+ - 'spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
+ - 'spec/services/notes/quick_actions_service_spec.rb'
+
+# Offense count: 917
+# Configuration parameters: Strict, EnforcedStyle.
+# SupportedStyles: inflected, explicit
+RSpec/PredicateMatcher:
Enabled: false
-# Offense count: 36
+# Offense count: 35
RSpec/RepeatedExample:
Enabled: false
-# Offense count: 86
+# Offense count: 132
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: and_return, block
+RSpec/ReturnFromStub:
+ Enabled: false
+
+# Offense count: 105
RSpec/ScatteredLet:
Enabled: false
-# Offense count: 20
+# Offense count: 22
RSpec/ScatteredSetup:
- Enabled: false
+ Exclude:
+ - 'spec/controllers/projects/templates_controller_spec.rb'
+ - '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
RSpec/SharedContext:
+ Exclude:
+ - 'spec/features/admin/admin_groups_spec.rb'
+
+# Offense count: 90
+RSpec/SingleLineHook:
+ Enabled: false
+
+# Offense count: 5
+RSpec/VoidExpect:
+ Exclude:
+ - 'spec/features/projects/artifacts/download_spec.rb'
+ - 'spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb'
+ - 'spec/models/ci/group_spec.rb'
+ - 'spec/models/ci/runner_spec.rb'
+ - 'spec/services/users/destroy_service_spec.rb'
+
+# Offense count: 40
+# Configuration parameters: Include.
+# Include: db/migrate/*.rb
+Rails/CreateTableWithTimestamps:
Enabled: false
-# Offense count: 115
+# Offense count: 149
Rails/FilePath:
Enabled: false
+# Offense count: 119
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/HasManyOrHasOneDependent:
+ Enabled: false
+
+# Offense count: 113
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/InverseOf:
+ Enabled: false
+
+# Offense count: 48
+# Configuration parameters: Include.
+# Include: app/controllers/**/*.rb
+Rails/LexicallyScopedActionFilter:
+ Enabled: false
+
+# Offense count: 14
+# Cop supports --auto-correct.
+Rails/Presence:
+ Exclude:
+ - 'app/controllers/projects/blob_controller.rb'
+ - 'app/models/ci/pipeline.rb'
+ - 'app/models/clusters/platforms/kubernetes.rb'
+ - 'app/models/concerns/mentionable.rb'
+ - 'app/models/concerns/token_authenticatable.rb'
+ - 'app/models/project_services/hipchat_service.rb'
+ - 'app/models/project_services/irker_service.rb'
+ - 'app/models/project_services/jira_service.rb'
+ - 'app/models/project_services/kubernetes_service.rb'
+ - 'app/models/project_services/packagist_service.rb'
+ - 'app/models/wiki_page.rb'
+ - 'lib/gitlab/git/hook.rb'
+ - 'lib/gitlab/github_import/importer/releases_importer.rb'
+
+# Offense count: 14
+# Cop supports --auto-correct.
+Rails/RedundantReceiverInWithOptions:
+ Exclude:
+ - 'config/initializers/doorkeeper_openid_connect.rb'
+
# Offense count: 2
# Configuration parameters: Include.
# Include: db/migrate/*.rb
Rails/ReversibleMigration:
- Enabled: false
+ Exclude:
+ - 'db/migrate/20160824103857_drop_unused_ci_tables.rb'
-# Offense count: 336
+# Offense count: 430
# Configuration parameters: Blacklist.
# Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
Rails/SkipsModelValidations:
Enabled: false
-# Offense count: 11
-# Cop supports --auto-correct.
-Security/YAMLLoad:
- Enabled: false
+# Offense count: 1
+# Configuration parameters: Environments.
+# Environments: development, test, production
+Rails/UnknownEnv:
+ Exclude:
+ - 'db/migrate/20171124125748_populate_missing_merge_request_statuses.rb'
-# Offense count: 58
+# Offense count: 13
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+Security/YAMLLoad:
+ Exclude:
+ - 'config/initializers/carrierwave.rb'
+ - 'lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb'
+ - 'lib/gitlab/redis/wrapper.rb'
+ - 'lib/system_check/incoming_email/imap_authentication_check.rb'
+ - 'spec/config/mail_room_spec.rb'
+ - 'spec/initializers/secret_token_spec.rb'
+ - 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
+ - 'spec/models/clusters/platforms/kubernetes_spec.rb'
+ - 'spec/models/project_services/kubernetes_service_spec.rb'
+
+# Offense count: 63
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 6
-# Cop supports --auto-correct.
-Style/EachWithObject:
+# Offense count: 5
+Style/CommentedKeyword:
+ Exclude:
+ - 'lib/tasks/gitlab/backup.rake'
+ - 'spec/tasks/gitlab/backup_rake_spec.rb'
+
+# Offense count: 30
+Style/DateTime:
Enabled: false
-# Offense count: 31
+# Offense count: 1
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/Dir:
+ Exclude:
+ - 'qa/qa.rb'
+
+# Offense count: 9
+# Cop supports --auto-correct.
+Style/EachWithObject:
+ Exclude:
+ - 'config/initializers/gollum.rb'
+ - 'lib/expand_variables.rb'
+ - 'lib/gitlab/ci/ansi2html.rb'
+ - 'lib/gitlab/ee_compat_check.rb'
+ - 'lib/gitlab/hook_data/issuable_builder.rb'
+ - 'lib/gitlab/i18n/po_linter.rb'
+ - 'lib/gitlab/import_export/members_mapper.rb'
+ - 'lib/gitlab/import_export/relation_factory.rb'
+ - 'scripts/static-analysis'
+
+# Offense count: 24
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: empty, nil, both
Style/EmptyElse:
Enabled: false
-# Offense count: 9
+# Offense count: 14
+# Cop supports --auto-correct.
+Style/EmptyLambdaParameter:
+ Exclude:
+ - 'app/models/ci/build.rb'
+ - 'app/models/ci/runner.rb'
+
+# Offense count: 12
# Cop supports --auto-correct.
Style/EmptyLiteral:
- Enabled: false
+ Exclude:
+ - 'features/steps/project/commits/commits.rb'
+ - 'lib/gitlab/fogbugz_import/importer.rb'
+ - 'lib/gitlab/git/diff_collection.rb'
+ - 'lib/gitlab/gitaly_client.rb'
+ - 'scripts/trigger-build-omnibus'
+ - 'spec/features/merge_requests/versions_spec.rb'
+ - 'spec/helpers/merge_requests_helper_spec.rb'
+ - 'spec/lib/gitlab/request_context_spec.rb'
+ - 'spec/lib/gitlab/workhorse_spec.rb'
+ - 'spec/requests/api/jobs_spec.rb'
+ - 'spec/support/chat_slash_commands_shared_examples.rb'
-# Offense count: 78
+# Offense count: 98
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Enabled: false
# Offense count: 23
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/Encoding:
+ Enabled: false
+
+# Offense count: 2
+Style/EvalWithLocation:
+ Exclude:
+ - 'app/models/service.rb'
+
+# Offense count: 52
+# Cop supports --auto-correct.
+# Configuration parameters: Autocorrect, EnforcedStyle.
+# SupportedStyles: module_function, extend_self
+Style/ExtendSelf:
+ Enabled: false
+
+# Offense count: 34
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Enabled: false
-# Offense count: 301
+# Offense count: 371
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 18
+# Offense count: 21
Style/IfInsideElse:
Enabled: false
-# Offense count: 182
+# Offense count: 781
# Cop supports --auto-correct.
-# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
Enabled: false
-# Offense count: 52
+# Offense count: 71
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: line_count_dependent, lambda, literal
Style/Lambda:
Enabled: false
-# Offense count: 6
+# Offense count: 11
# Cop supports --auto-correct.
Style/LineEndConcatenation:
- Enabled: false
+ Exclude:
+ - 'app/helpers/tree_helper.rb'
+ - 'spec/features/issuables/markdown_references_spec.rb'
+ - 'spec/lib/gitlab/checks/project_moved_spec.rb'
+ - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
+ - 'spec/lib/gitlab/incoming_email_spec.rb'
-# Offense count: 40
+# Offense count: 39
# Cop supports --auto-correct.
Style/MethodCallWithoutArgsParentheses:
Enabled: false
-# Offense count: 13
+# Offense count: 17
Style/MethodMissing:
Enabled: false
+# Offense count: 7
+Style/MixinUsage:
+ Exclude:
+ - 'features/support/env.rb'
+ - 'spec/factories/ci/builds.rb'
+ - 'spec/factories/ci/job_artifacts.rb'
+ - 'spec/factories/lfs_objects.rb'
+ - 'spec/factories/notes.rb'
+ - 'spec/lib/gitlab/import_export/project_tree_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/version_checker_spec.rb'
+
# Offense count: 6
# Cop supports --auto-correct.
Style/MultilineIfModifier:
- Enabled: false
+ Exclude:
+ - 'app/helpers/snippets_helper.rb'
+ - 'app/models/project_wiki.rb'
+ - 'app/services/ci/process_pipeline_service.rb'
+ - 'app/services/create_deployment_service.rb'
+ - 'lib/api/commit_statuses.rb'
+ - 'lib/gitlab/ci/trace.rb'
-# Offense count: 26
+# Offense count: 23
# Cop supports --auto-correct.
+# Configuration parameters: Whitelist.
+# Whitelist: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with
Style/NestedParenthesizedCalls:
Enabled: false
# Offense count: 20
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
+# Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Enabled: false
-# Offense count: 45
+# Offense count: 58
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
+# Configuration parameters: EnforcedOctalStyle.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
-# Offense count: 98
+# Offense count: 112
# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
+# Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Enabled: false
-# Offense count: 42
+# Offense count: 4
+# Cop supports --auto-correct.
+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
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
-# Offense count: 800
+# Offense count: 891
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@@ -235,106 +650,194 @@ Style/PercentLiteralDelimiters:
# Offense count: 15
# Cop supports --auto-correct.
Style/PerlBackrefs:
- Enabled: false
-
-# Offense count: 58
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+ Exclude:
+ - 'app/controllers/projects/application_controller.rb'
+ - 'app/helpers/submodule_helper.rb'
+ - 'lib/backup/manager.rb'
+ - 'lib/banzai/filter/abstract_reference_filter.rb'
+ - 'lib/banzai/filter/autolink_filter.rb'
+ - 'lib/banzai/filter/emoji_filter.rb'
+ - 'lib/banzai/filter/gollum_tags_filter.rb'
+ - 'lib/expand_variables.rb'
+ - 'lib/gitlab/diff/highlight.rb'
+ - 'lib/gitlab/search_results.rb'
+ - 'lib/gitlab/sherlock/query.rb'
+
+# Offense count: 82
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, exploded
Style/RaiseArgs:
Enabled: false
-# Offense count: 6
+# Offense count: 8
# Cop supports --auto-correct.
Style/RedundantBegin:
- Enabled: false
+ Exclude:
+ - 'app/controllers/projects/clusters/gcp_controller.rb'
+ - 'app/models/merge_request.rb'
+ - 'app/services/projects/import_service.rb'
+ - 'lib/api/branches.rb'
+ - 'lib/gitlab/current_settings.rb'
+ - 'lib/gitlab/git/commit.rb'
+ - 'lib/gitlab/health_checks/base_abstract_check.rb'
+ - 'lib/tasks/gitlab/task_helpers.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/RedundantConditional:
+ Exclude:
+ - 'lib/system_check/helpers.rb'
-# Offense count: 37
+# Offense count: 58
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
-# Offense count: 14
+# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: AllowMultipleReturnValues.
Style/RedundantReturn:
- Enabled: false
-
-# Offense count: 406
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/concerns/issuable_actions.rb'
+ - 'app/controllers/groups/application_controller.rb'
+ - 'app/controllers/omniauth_callbacks_controller.rb'
+ - 'app/controllers/profiles/keys_controller.rb'
+ - 'app/controllers/projects/application_controller.rb'
+ - 'app/services/access_token_validation_service.rb'
+ - 'lib/gitlab/utils.rb'
+ - 'lib/google_api/auth.rb'
+
+# Offense count: 454
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 115
+# Offense count: 140
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
+# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
-# Offense count: 29
+# Offense count: 35
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
+# Offense count: 105
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: implicit, explicit
+Style/RescueStandardError:
+ Enabled: false
+
+# Offense count: 91
+# Cop supports --auto-correct.
+# Configuration parameters: ConvertCodeThatCanStartToReturnNil.
+Style/SafeNavigation:
+ Enabled: false
+
# Offense count: 8
# Cop supports --auto-correct.
Style/SelfAssignment:
- Enabled: false
+ Exclude:
+ - 'app/models/concerns/bulk_member_access_load.rb'
+ - 'app/serializers/base_serializer.rb'
+ - 'app/services/notification_service.rb'
+ - 'lib/api/runners.rb'
+ - 'spec/features/merge_requests/diff_notes_resolve_spec.rb'
+ - 'spec/features/projects/clusters/interchangeability_spec.rb'
+ - 'spec/support/import_export/configuration_helper.rb'
# Offense count: 50
# Cop supports --auto-correct.
# Configuration parameters: AllowIfMethodIsEmpty.
Style/SingleLineMethods:
- Enabled: false
+ Exclude:
+ - 'lib/gitlab/ci/ansi2html.rb'
-# Offense count: 64
+# Offense count: 66
# Cop supports --auto-correct.
-# Configuration parameters: SupportedStyles.
+# Configuration parameters: .
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
-# Offense count: 44
+# Offense count: 1
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/StderrPuts:
+ Exclude:
+ - 'config/initializers/rspec_profiling.rb'
+
+# Offense count: 45
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 84
+# Offense count: 99
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
Style/SymbolProc:
Enabled: false
-# Offense count: 8
+# Offense count: 9
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
+# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
- Enabled: false
+ Exclude:
+ - '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'
# Offense count: 17
# Cop supports --auto-correct.
# Configuration parameters: AllowNamedUnderscoreVariables.
Style/TrailingUnderscoreVariable:
- Enabled: false
+ Exclude:
+ - 'app/controllers/admin/background_jobs_controller.rb'
+ - 'app/controllers/invites_controller.rb'
+ - 'app/helpers/tab_helper.rb'
+ - 'lib/backup/manager.rb'
+ - 'lib/gitlab/logger.rb'
+ - 'lib/gitlab/upgrader.rb'
+ - 'lib/system_check/app/migrations_are_up_check.rb'
+ - 'lib/system_check/incoming_email/mail_room_running_check.rb'
+ - 'lib/tasks/gitlab/check.rake'
+ - 'lib/tasks/gitlab/task_helpers.rb'
+ - 'spec/lib/gitlab/etag_caching/middleware_spec.rb'
+ - 'spec/services/quick_actions/interpret_service_spec.rb'
-# Offense count: 4
+# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist.
# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym
Style/TrivialAccessors:
- Enabled: false
+ Exclude:
+ - 'app/models/external_issue.rb'
+ - 'app/serializers/base_serializer.rb'
+ - 'lib/gitlab/ldap/person.rb'
+ - 'lib/system_check/base_check.rb'
-# Offense count: 5
+# Offense count: 4
# Cop supports --auto-correct.
Style/UnlessElse:
- Enabled: false
+ Exclude:
+ - 'lib/backup/manager.rb'
+ - 'lib/gitlab/project_search_results.rb'
+ - 'lib/tasks/gitlab/check.rake'
+ - 'spec/features/issues/award_emoji_spec.rb'
-# Offense count: 28
+# Offense count: 30
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
@@ -342,4 +845,19 @@ Style/UnneededInterpolation:
# Offense count: 11
# Cop supports --auto-correct.
Style/ZeroLengthPredicate:
- Enabled: false
+ Exclude:
+ - 'app/models/deploy_key.rb'
+ - 'app/models/network/commit.rb'
+ - 'app/models/network/graph.rb'
+ - 'app/models/project_services/asana_service.rb'
+ - 'app/services/boards/create_service.rb'
+ - 'app/services/merge_requests/conflicts/list_service.rb'
+ - 'lib/declarative_policy/dsl.rb'
+ - 'lib/extracts_path.rb'
+ - 'lib/gitlab/git/repository.rb'
+
+# Offense count: 22050
+# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
+# URISchemes: http, https
+Metrics/LineLength:
+ Max: 1310
diff --git a/.ruby-version b/.ruby-version
index cc6c9a491e0..e75da3e63d6 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.3.5
+2.3.6
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6088a1b3515..26580e7183f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,215 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.3.3 (2018-01-02)
+
+### Fixed (3 changes)
+
+- Fix links to old commits in merge request comments.
+- Fix 404 errors after a user edits an issue description and solves the reCAPTCHA.
+- Gracefully handle orphaned write deploy keys in /internal/post_receive.
+
+
+## 10.3.2 (2017-12-28)
+
+### Fixed (1 change)
+
+- Fix migration for removing orphaned issues.moved_to_id values in MySQL and PostgreSQL.
+
+
+## 10.3.1 (2017-12-27)
+
+### Fixed (3 changes)
+
+- Don't link LFS objects to a project when unlinking forks when they were already linked. !16006
+- Execute project hooks and services after commit when moving an issue.
+- Fix Error 500s with anonymous clones for a project that has moved.
+
+### Changed (1 change)
+
+- Reduce the number of buckets in gitlab_cache_operation_duration_seconds metric. !15881
+
+
+## 10.3.0 (2017-12-22)
+
+### Security (1 change, 1 of them is from the community)
+
+- Upgrade jQuery to 2.2.4. !15570 (Takuya Noguchi)
+
+### Fixed (55 changes, 8 of them are from the community)
+
+- Fail jobs if its dependency is missing. !14009
+- Fix errors when selecting numeric-only labels in the labels autocomplete selector. !14607 (haseebeqx)
+- Fix pipeline status transition for single manual job. This would also fix pipeline duration becuse it is depending on status transition. !15251
+- Fix acceptance of username for Mattermost service update. !15275
+- Set the default gitlab-shell timeout to 3 hours. !15292
+- Make sure a user can add projects to subgroups they have access to. !15294
+- OAuth identity lookups case-insensitive. !15312
+- Fix filter by my reaction is not working. !15345 (Hiroyuki Sato)
+- Avoid deactivation when pipeline schedules execute a branch includes `[ci skip]` comment. !15405
+- Add recaptcha modal to issue updates detected as spam. !15408
+- Fix item name and namespace text overflow in Projects dropdown. !15451
+- Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive'. !15493
+- Fix commits page throwing 500 when the multi-file editor was enabled. !15502
+- Fix Issue comment submit button being disabled when pasting content from another GFM note. !15530
+- Reenable Prometheus metrics, add more control over Prometheus method instrumentation. !15558
+- Fix broadcast message not showing up on login page. !15578
+- Initializes the branches dropdown when the 'Start new pipeline' failed due to validation errors. !15588 (Christiaan Van den Poel)
+- Fix merge requests where the source or target branch name matches a tag name. !15591
+- Create a fork network for forks with a deleted source. !15595
+- Fix search results when a filename would contain a special character. !15606 (haseebeqx)
+- Strip leading & trailing whitespaces in CI/CD secret variable keys. !15615
+- Correctly link to a forked project from the new fork page. !15653
+- Fix the fork project functionality for projects with hashed storage. !15671
+- Added default order to UsersFinder. !15679
+- Fix graph notes number duplication. !15696 (Vladislav Kaverin)
+- Fix updateEndpoint undefined error for issue_show app root. !15698
+- Change boards page boards_data absolute urls to paths. !15703
+- Using appropiate services in the API for managing forks. !15709
+- Confirming email with invalid token should no longer generate an error. !15726
+- fix #39233 - 500 in merge request. !15774 (Martin Nowak)
+- Use Markdown styling for new project guidelines. !15785 (Markus Koller)
+- Fix error during schema dump. !15866
+- Fix broken illustration images for monitoring page empty states. !15889
+- Make sure user email is read only when synced with LDAP. !15915
+- Fixed outdated browser flash positioning.
+- Fix gitlab:import:repos Rake task moving repositories into the wrong location.
+- Gracefully handle case when repository's root ref does not exist.
+- Fix GitHub importer using removed interface.
+- Align retry button with job title with new grid size.
+- Fixed admin welcome screen new group path.
+- Fix related branches/Merge requests failing to load when the hostname setting is changed.
+- Init zen mode in snippets pages.
+- Remove extra margin from wordmark in header.
+- Fixed long commit links not wrapping correctly.
+- Fixed deploy keys remove button loading state not resetting.
+- Use app host instead of asset host when rendering image blob or diff.
+- Hide log size for mobile screens.
+- Fix sending notification emails to users with the mention level set who were mentioned in an issue or merge request description.
+- Changed validation error message on wrong milestone dates. (Xurxo Méndez Pérez)
+- Fix access to the final page of todos.
+- Fixed new group milestone breadcrumbs.
+- Fix image diff notification email from showing wrong content.
+- Fixed merge request lock icon size.
+- Make sure head pippeline always corresponds with the head sha of an MR.
+- Prevent 500 error when inspecting job after trigger was removed.
+
+### Changed (14 changes, 2 of them are from the community)
+
+- Only owner or master can erase jobs. !15216
+- Allow password authentication to be disabled entirely. !15223 (Markus Koller)
+- Add the option to automatically run a pipeline after updating AutoDevOps settings. !15380
+- Add total_time_spent to the `changes` hash in issuable Webhook payloads. !15381
+- Monitor NFS shards for circuitbreaker in a separate process. !15426
+- Add inline editing to issues on mobile. !15438
+- Add custom brand text on new project pages. !15541 (Markus Koller)
+- Show only group name by default and put full namespace in tooltip in Groups tree. !15650
+- Use custom user agent header in all GCP API requests. !15705
+- Changed the deploy markers on the prometheus dashboard to be more verbose. !38032
+- Animate contextual sidebar on collapse/expand.
+- Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma in :cartwheel_tone4:.
+- When a custom header logo is present, don't show GitLab type logo.
+- Improved diff changed files dropdown design.
+
+### Performance (19 changes)
+
+- Add timeouts for Gitaly calls. !15047
+- Performance issues when loading large number of wiki pages. !15276
+- Add performance logging to UpdateMergeRequestsWorker. !15360
+- Keep track of all circuitbreaker keys in a set. !15613
+- Improve the performance for counting commits. !15628
+- Reduce requests for project forks on show page of projects that have forks. !15663
+- Perform SQL matching of Build&Runner tags to greatly speed-up job picking.
+- Only load branch names for protected branch checks.
+- Optimize API /groups/:id/projects by preloading associations.
+- Remove allocation tracking code from InfluxDB sampler for performance.
+- Throttle the number of UPDATEs triggered by touch.
+- Make finding most recent merge request diffs more efficient.
+- Fetch blobs in bulk when generating diffs.
+- Cache commits for MergeRequest diffs.
+- Use fuzzy search with minimum length of 3 characters where appropriate.
+- Add axios to common file.
+- Remove template selector from global namespace.
+- check the import_status field before doing SQL operations to check the import url.
+- Stop sending milestone and labels data over the wire for MR widget requests.
+
+### Added (22 changes, 15 of them are from the community)
+
+- Limit autocomplete menu to applied labels. !11110 (Vitaliy @blackst0ne Klachkov)
+- Make diff notes created on a commit in a merge request to persist a rebase. !12148
+- Allow creation of merge request from email. !13817 (janp)
+- Add an ability to use a custom branch name on creation from issues. !13884 (Vitaliy @blackst0ne Klachkov)
+- Add anonymous rate limit per IP, and authenticated (web or API) rate limits per user. !14708
+- Create a new form to add Existing Kubernetes Cluster. !14805
+- Add support of Mermaid (generation of diagrams and flowcharts from text). !15107 (Vitaliy @blackst0ne Klachkov)
+- Add total time spent to milestones. !15116 (George Andrinopoulos)
+- Add /groups/:id/subgroups endpoint to API. !15142 (marbemac)
+- Add administrative endpoint to list all pages domains. !15160 (Travis Miller)
+- Adds Rubocop rule for line break after guard clause. !15188 (Jacopo Beschi @jacopo-beschi)
+- Add edit button to mobile file view. !15199 (Travis Miller)
+- Add dropdown sort to group milestones. !15230 (George Andrinopoulos)
+- added support for ordering and sorting in notes api. !15342 (haseebeqx)
+- Hashed Storage migration script now supports migrating project attachments. !15352
+- New API endpoint - list jobs for a specified runner. !15432
+- Add new API endpoint - get a namespace by ID. !15442
+- Disables autocomplete in filtered searc. !15477 (Jacopo Beschi @jacopo-beschi)
+- Update empty state page of merge request 'changes' tab. !15611 (Vitaliy @blackst0ne Klachkov)
+- Allow git pull/push on group/user/project redirects. !15670
+- show status of gitlab reference links in wiki. !15694 (haseebeqx)
+- Add email confirmation parameters for user creation and update via API. (Daniel Juarez)
+
+### Other (17 changes, 7 of them are from the community)
+
+- Enable UnnecessaryMantissa in scss-lint. !15255 (Takuya Noguchi)
+- Add untracked files to uploads table. !15270
+- Move update_project_counter_caches? out of issue and merge request. !15300 (George Andrinopoulos)
+- Removed tooltip from clone dropdown. !15334
+- Clean up empty fork networks. !15373
+- Create issuable destroy service. !15604 (George Andrinopoulos)
+- Upgrade seed-fu to 2.3.7. !15607 (Takuya Noguchi)
+- Rename GKE as Kubernetes Engine. !15608 (Takuya Noguchi)
+- Prefer ci_config_path validation for leading slashes instead of sanitizing the input. !15672 (Christiaan Van den Poel)
+- Fix typo in docs about Elasticsearch. !15699 (Takuya Noguchi)
+- Add internationalization support for the prometheus integration. !33338
+- Export text utils functions as es6 module and add tests.
+- Stop reloading the page when using pagination and tabs - use API calls - in Pipelines table.
+- Clean up schema of the "issues" table.
+- Clarify wording of protected branch settings for the default branch.
+- Update svg external depencency.
+- Clean up schema of the "merge_requests" table.
+
+
+## 10.2.5 (2017-12-15)
+
+### Fixed (8 changes)
+
+- Create a fork network for forks with a deleted source. !15595
+- Correctly link to a forked project from the new fork page. !15653
+- Fix the fork project functionality for projects with hashed storage. !15671
+- Fix updateEndpoint undefined error for issue_show app root. !15698
+- Fix broken illustration images for monitoring page empty states. !15889
+- Fix related branches/Merge requests failing to load when the hostname setting is changed.
+- Fix gitlab:import:repos Rake task moving repositories into the wrong location.
+- Gracefully handle case when repository's root ref does not exist.
+
+### Performance (3 changes)
+
+- Keep track of all circuitbreaker keys in a set. !15613
+- Only load branch names for protected branch checks.
+- Optimize API /groups/:id/projects by preloading associations.
+
+
+## 10.2.4 (2017-12-07)
+
+### Security (5 changes)
+
+- Fix e-mail address disclosure through member search fields
+- Prevent creating issues through API when user does not have permissions
+- Prevent an information disclosure in the Groups API
+- Fix user without access to private Wiki being able to see it on the project page
+- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
+
+
## 10.2.3 (2017-11-30)
### Fixed (7 changes)
@@ -237,6 +446,17 @@ entry.
- Add Gitaly metrics to the performance bar.
+## 10.1.5 (2017-12-07)
+
+### Security (5 changes)
+
+- Fix e-mail address disclosure through member search fields
+- Prevent creating issues through API when user does not have permissions
+- Prevent an information disclosure in the Groups API
+- Fix user without access to private Wiki being able to see it on the project page
+- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
+
+
## 10.1.4 (2017-11-14)
### Fixed (4 changes)
@@ -485,6 +705,17 @@ entry.
- creation of keys moved to services. !13331 (haseebeqx)
- Add username as GL_USERNAME in hooks.
+## 10.0.7 (2017-12-07)
+
+### Security (5 changes)
+
+- Fix e-mail address disclosure through member search fields
+- Prevent creating issues through API when user does not have permissions
+- Prevent an information disclosure in the Groups API
+- Fix user without access to private Wiki being able to see it on the project page
+- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
+
+
## 10.0.5 (2017-11-03)
- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 01d4a546b97..2b79f0825e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -553,7 +553,7 @@ the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed
-1. [Unit and system tests][testing] that pass on the CI server
+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. [Changelog entry added][changelog], if necessary
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index cb6b534abe1..e40e4fc339c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.59.0
+0.66.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 4e32c7b1caf..e030a0157c9 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.10.1
+5.10.3
diff --git a/Gemfile b/Gemfile
index 3187b0e5ae9..4c4a61bb4e4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -12,7 +12,7 @@ gem 'sprockets', '~> 3.7.0'
gem 'default_value_for', '~> 3.0.0'
# Supported DBs
-gem 'mysql2', '~> 0.4.5', group: :mysql
+gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.26.0'
@@ -263,7 +263,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
-gem 'batch-loader'
+gem 'batch-loader', '~> 1.2.1'
# Perf bar
gem 'peek', '~> 1.0.1'
@@ -283,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~> 0.7.0.beta39'
+ gem 'prometheus-client-mmap', '~> 0.7.0.beta44'
gem 'raindrops', '~> 0.18'
end
@@ -311,7 +311,7 @@ group :development, :test do
gem 'fuubar', '~> 2.2.0'
gem 'database_cleaner', '~> 1.5.0'
- gem 'factory_girl_rails', '~> 4.7.0'
+ gem 'factory_bot_rails', '~> 4.8.2'
gem 'rspec-rails', '~> 3.6.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
@@ -334,10 +334,12 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
- gem 'rubocop', '~> 0.49.1', require: false
- gem 'rubocop-rspec', '~> 1.15.1', require: false
- gem 'rubocop-gitlab-security', '~> 0.1.0', require: false
- gem 'scss_lint', '~> 0.54.0', require: false
+ gem 'gitlab-styles', '~> 2.2.0', require: false
+ # Pin these dependencies, otherwise a new rule could break the CI pipelines
+ gem 'rubocop', '~> 0.52.0'
+ gem 'rubocop-rspec', '~> 1.20.1'
+
+ gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false
@@ -400,14 +402,18 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.59.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.64.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
# Feature toggles
-gem 'flipper', '~> 0.10.2'
-gem 'flipper-active_record', '~> 0.10.2'
+gem 'flipper', '~> 0.11.0'
+gem 'flipper-active_record', '~> 0.11.0'
+gem 'flipper-active_support_cache_store', '~> 0.11.0'
# Structured logging
gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
+
+# Asset synchronization
+gem 'asset_sync', '~> 2.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 6213167ae0b..c854cda48ea 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -58,6 +58,11 @@ GEM
asciidoctor (1.5.3)
asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5)
+ asset_sync (2.2.0)
+ activemodel (>= 4.1.0)
+ fog-core
+ mime-types (>= 2.99)
+ unf
ast (2.3.0)
atomic (1.1.99)
attr_encrypted (3.0.3)
@@ -73,7 +78,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
- batch-loader (1.1.1)
+ batch-loader (1.2.1)
bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -190,10 +195,10 @@ GEM
excon (0.57.1)
execjs (2.6.0)
expression_parser (0.9.0)
- factory_girl (4.7.0)
+ factory_bot (4.8.2)
activesupport (>= 3.0.0)
- factory_girl_rails (4.7.0)
- factory_girl (~> 4.7.0)
+ factory_bot_rails (4.8.2)
+ factory_bot (~> 4.8.2)
railties (>= 3.0.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
@@ -210,10 +215,13 @@ GEM
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
- flipper (0.10.2)
- flipper-active_record (0.10.2)
+ flipper (0.11.0)
+ flipper-active_record (0.11.0)
activerecord (>= 3.2, < 6)
- flipper (~> 0.10.2)
+ flipper (~> 0.11.0)
+ flipper-active_support_cache_store (0.11.0)
+ activesupport (>= 3.2, < 6)
+ flipper (~> 0.11.0)
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
@@ -276,7 +284,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.59.0)
+ gitaly-proto (0.64.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -295,6 +303,10 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.3)
+ gitlab-styles (2.2.0)
+ rubocop (~> 0.51)
+ rubocop-gitlab-security (~> 0.1.0)
+ rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
@@ -493,7 +505,7 @@ GEM
mustermann (1.0.0)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
- mysql2 (0.4.5)
+ mysql2 (0.4.10)
net-ldap (0.16.0)
net-ssh (4.1.0)
netrc (0.11.0)
@@ -622,7 +634,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.7.0.beta43)
+ prometheus-client-mmap (0.7.0.beta44)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -683,6 +695,9 @@ GEM
rake
raindrops (0.18.0)
rake (12.3.0)
+ rb-fsevent (0.10.2)
+ rb-inotify (0.9.10)
+ ffi (>= 0.5.0, < 2)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2)
@@ -696,7 +711,7 @@ GEM
json
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
- redis (3.3.3)
+ redis (3.3.5)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
@@ -706,7 +721,7 @@ GEM
redis-store (>= 1.3, < 2)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
- redis-rack (2.0.3)
+ redis-rack (2.0.4)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
@@ -769,21 +784,21 @@ GEM
pg
rails
sqlite3
- rubocop (0.49.1)
+ rubocop (0.52.0)
parallel (~> 1.10)
- parser (>= 2.3.3.1, < 3.0)
+ parser (>= 2.4.0.2, < 3.0)
powerpack (~> 0.1)
- rainbow (>= 1.99.1, < 3.0)
+ rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
- rubocop-gitlab-security (0.1.0)
- rubocop (>= 0.47.1)
- rubocop-rspec (1.15.1)
- rubocop (>= 0.42.0)
+ rubocop-gitlab-security (0.1.1)
+ rubocop (>= 0.51)
+ rubocop-rspec (1.20.1)
+ rubocop (>= 0.51.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
- ruby-progressbar (1.8.1)
+ ruby-progressbar (1.9.0)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.9.0)
@@ -797,7 +812,11 @@ GEM
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
- sass (3.4.22)
+ 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.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
@@ -807,9 +826,9 @@ GEM
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
- scss_lint (0.54.0)
+ scss_lint (0.56.0)
rake (>= 0.9, < 13)
- sass (~> 3.4.20)
+ sass (~> 3.5.3)
securecompare (1.0.0)
seed-fu (2.3.6)
activerecord (>= 3.1)
@@ -827,11 +846,11 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.0.4)
+ sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
- redis (~> 3.3, >= 3.3.3)
+ redis (>= 3.3.4, < 5)
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
@@ -975,11 +994,12 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7)
+ asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
- batch-loader
+ batch-loader (~> 1.2.1)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
@@ -1011,12 +1031,13 @@ DEPENDENCIES
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
- factory_girl_rails (~> 4.7.0)
+ factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
ffaker (~> 2.4)
flay (~> 2.8.0)
- flipper (~> 0.10.2)
- flipper-active_record (~> 0.10.2)
+ flipper (~> 0.11.0)
+ flipper-active_record (~> 0.11.0)
+ flipper-active_support_cache_store (~> 0.11.0)
fog-aliyun (~> 0.2.0)
fog-aws (~> 1.4)
fog-core (~> 1.44)
@@ -1032,10 +1053,11 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.59.0)
+ gitaly-proto (~> 0.64.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
+ gitlab-styles (~> 2.2.0)
gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
@@ -1072,7 +1094,7 @@ DEPENDENCIES
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
- mysql2 (~> 0.4.5)
+ mysql2 (~> 0.4.10)
net-ldap
net-ssh (~> 4.1.0)
nokogiri (~> 1.8.1)
@@ -1107,7 +1129,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.7.0.beta39)
+ prometheus-client-mmap (~> 0.7.0.beta44)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1138,9 +1160,8 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.49.1)
- rubocop-gitlab-security (~> 0.1.0)
- rubocop-rspec (~> 1.15.1)
+ rubocop (~> 0.52.0)
+ rubocop-rspec (~> 1.20.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8)
@@ -1148,7 +1169,7 @@ DEPENDENCIES
rugged (~> 0.26.0)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
- scss_lint (~> 0.54.0)
+ scss_lint (~> 0.56.0)
seed-fu (= 2.3.6)
select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5)
diff --git a/PROCESS.md b/PROCESS.md
index 7c8db689256..3fcf676b302 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see
-[limit conflicts with EE when developing on CE][limit_ee_conflicts].
+[Automatic CE->EE merge][automatic_ce_ee_merge] and
+[Guidelines for implementing Enterprise Edition features][ee_features].
### After the 7th
@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
-[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html
+[automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
+[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html
diff --git a/VERSION b/VERSION
index 73cdb768a24..80959b81ba4 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-10.3.0-pre
+10.4.0-pre
diff --git a/app/assets/images/file_icons.svg b/app/assets/images/file_icons.svg
new file mode 100644
index 00000000000..26ec1a6b388
--- /dev/null
+++ b/app/assets/images/file_icons.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 24 24" id="actionscript" xmlns="http://www.w3.org/2000/svg"><text style="line-height:113.99999857%" x="5.605" y="15.892" transform="scale(.91325 1.095)" font-weight="400" font-size="42.822" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/><path style="line-height:125%" d="M4.744 2.031c-1.157 0-1.994.31-2.51.93-.515.612-.771 1.678-.771 3.197v2.467c0 1.408-.402 2.111-1.201 2.111v2.035c.8 0 1.2.679 1.2 2.036v2.654c0 1.512.26 2.562.78 3.152.52.59 1.355.885 2.502.885V19.43c-.447 0-.77-.151-.97-.453-.195-.303-.292-.815-.292-1.538v-2.267c0-1.807-.404-2.937-1.214-3.395v-.045c.81-.464 1.214-1.581 1.214-3.351V6.025c0-1.283.42-1.925 1.262-1.925V2.03zm14.66 0V4.1c.842 0 1.262.642 1.262 1.925v2.268c0 1.843.402 2.996 1.207 3.46v.046c-.805.442-1.207 1.544-1.207 3.306v2.356c0 .715-.099 1.22-.299 1.516-.2.302-.52.453-.963.453v2.068c1.152 0 1.984-.295 2.494-.885.516-.59.772-1.663.772-3.218V14.84c0-1.379.404-2.069 1.209-2.069v-2.035c-.805 0-1.21-.696-1.21-2.09V6.113c0-1.49-.255-2.54-.77-3.152-.516-.62-1.348-.93-2.495-.93zm-3.054 4.46c-.455 0-.886.057-1.293.173a3.056 3.056 0 0 0-1.078.527c-.308.241-.551.549-.731.924-.18.37-.27.817-.27 1.336 0 .663.165 1.227.493 1.695.33.468.831.864 1.502 1.188.263.125.509.249.736.37.227.12.422.244.586.374.168.13.299.271.394.424a.963.963 0 0 1 .145.521c0 .144-.03.28-.09.405a.9.9 0 0 1-.275.318c-.12.088-.272.158-.455.21a2.34 2.34 0 0 1-.635.075c-.415 0-.825-.083-1.233-.25a3.644 3.644 0 0 1-1.13-.763v2.222a3.68 3.68 0 0 0 1.101.418c.427.093.875.139 1.346.139.459 0 .894-.05 1.305-.152a3.002 3.002 0 0 0 1.09-.5c.31-.237.556-.543.736-.918.183-.38.275-.849.275-1.405 0-.403-.052-.755-.156-1.056a2.542 2.542 0 0 0-.45-.813 3.295 3.295 0 0 0-.704-.633 6.754 6.754 0 0 0-.922-.535 12.4 12.4 0 0 1-.676-.348c-.2-.115-.37-.231-.51-.347a1.502 1.502 0 0 1-.322-.375.91.91 0 0 1-.115-.453c0-.153.033-.288.101-.408a.948.948 0 0 1 .29-.32c.123-.089.275-.156.454-.202a2.18 2.18 0 0 1 .598-.078c.16 0 .326.015.502.043.18.028.36.07.539.13.18.056.354.13.522.218.171.088.329.188.472.304V6.871a4.039 4.039 0 0 0-.957-.285 6.448 6.448 0 0 0-1.185-.096zm-8.774.165l-3.123 9.967h2.094l.605-2.217h3.053l.61 2.217h2.107L9.869 6.656H7.576zm1.072 1.78h.047c.028.347.077.646.145.896l.922 3.35H7.564l.934-3.377c.08-.288.13-.578.15-.87z" font-weight="400" font-size="51.019" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="android" xmlns="http://www.w3.org/2000/svg"><path d="M15 5h-1V4h1m-5 1H9V4h1m5.53-1.84L16.84.85c.19-.19.19-.51 0-.71a.513.513 0 0 0-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.14a.501.501 0 0 0-.7 0c-.2.2-.2.52 0 .71l1.31 1.31C6.97 3.26 6 5 6 7h12c0-2-1-3.75-2.47-4.84M20.5 8A1.5 1.5 0 0 0 19 9.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 20.5 8m-17 0A1.5 1.5 0 0 0 2 9.5v7A1.5 1.5 0 0 0 3.5 18 1.5 1.5 0 0 0 5 16.5v-7A1.5 1.5 0 0 0 3.5 8M6 18a1 1 0 0 0 1 1h1v3.5A1.5 1.5 0 0 0 9.5 24a1.5 1.5 0 0 0 1.5-1.5V19h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5V19h1a1 1 0 0 0 1-1V8H6v10z" fill="#c0ca33"/></symbol><symbol viewBox="0 0 24 24" id="angular" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="angular-component" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#0288d1"/></symbol><symbol viewBox="0 0 24 24" id="angular-directive" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ab47bc"/></symbol><symbol viewBox="0 0 24 24" id="angular-guard" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-pipe" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#00897b"/></symbol><symbol viewBox="0 0 24 24" id="angular-resolver" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-routing" xmlns="http://www.w3.org/2000/svg"><path d="M11 10H5L3 8l2-2h6V3l1-1 1 1v1h6l2 2-2 2h-6v2h6l2 2-2 2h-6v6a2 2 0 0 1 2 2H9a2 2 0 0 1 2-2V10z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-service" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ffca28"/></symbol><symbol viewBox="0 0 100 100" id="apiblueprint" xmlns="http://www.w3.org/2000/svg"><title>api-blueprint</title><path d="M50.133 7.521A16.998 16.998 0 0 0 33.135 24.52a16.998 16.998 0 0 0 4.945 11.974L24.861 57.398a16.998 16.998 0 0 0-3.175-.308A16.998 16.998 0 0 0 4.688 74.088a16.998 16.998 0 0 0 16.998 16.998 16.998 16.998 0 0 0 16.998-16.998 16.998 16.998 0 0 0-7.063-13.773l12.576-19.89a16.998 16.998 0 0 0 5.936 1.093 16.998 16.998 0 0 0 6.154-1.155l12.537 19.83a16.998 16.998 0 0 0-7.244 13.895 16.998 16.998 0 0 0 16.998 17 16.998 16.998 0 0 0 16.998-17A16.998 16.998 0 0 0 78.578 57.09a16.998 16.998 0 0 0-2.95.262L62.337 36.327A16.998 16.998 0 0 0 67.13 24.52 16.998 16.998 0 0 0 50.132 7.522z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="applescript" xmlns="http://www.w3.org/2000/svg"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" fill="#78909c"/></symbol><symbol viewBox="0 0 24 24" id="appveyor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-.084 0-.165.008-.248.01a10 10 0 0 0-.266.01 9.952 9.952 0 0 0-.754.066 10 10 0 0 0-.148.018 9.855 9.855 0 0 0-.93.177 10 10 0 0 0-.07.02c-.196.049-.392.1-.584.16v.012a10 10 0 0 0-2 .875V3.34c-.02.012-.038.027-.059.039a10 10 0 0 0-.953.635c-.09.067-.172.142-.26.213a10 10 0 0 0-.628.546c-.109.104-.211.211-.315.319a10 10 0 0 0-.476.539c-.1.12-.201.237-.295.361a10 10 0 0 0-.52.766c-.088.143-.17.288-.252.435a10 10 0 0 0-.363.723c-.072.161-.136.327-.2.492a10 10 0 0 0-.269.778c-.02.067-.044.131-.062.199a10 10 0 0 0-.008.027c-.098.364-.166.728-.22 1.09-.012.077-.024.153-.034.23a9.85 9.85 0 0 0-.08 1.182c0 .03-.006.057-.006.086a10 10 0 0 0 .008.148c.001.094-.002.188.002.282l.011.004a10 10 0 0 0 .333 2.158l-.012-.004c.012.047.033.091.047.139a10 10 0 0 0 .322.955c.02.052.037.106.059.158a10 10 0 0 0 .503 1.035c.065.116.14.226.21.34a10 10 0 0 0 .423.64c.092.128.187.252.285.375a10 10 0 0 0 .448.52c.112.123.222.248.341.365a10 10 0 0 0 .803.719 10 10 0 0 0 .01.006c.099.078.207.146.309.22a10 10 0 0 0 .648.442c.138.085.28.163.424.242a10 10 0 0 0 .715.358c.114.051.226.106.343.154a10 10 0 0 0 1.133.389c.016.004.031.01.047.015a10 10 0 0 0 .461.098 10 10 0 0 0 .482.103 10 10 0 0 0 .418.051 10 10 0 0 0 .575.065 10 10 0 0 0 .144.005A10 10 0 0 0 12 22a10 10 0 0 0 .197-.01 10 10 0 0 0 .496-.025 10 10 0 0 0 .49-.043 10 10 0 0 0 .489-.074 10 10 0 0 0 .51-.098 10 10 0 0 0 .47-.12 10 10 0 0 0 .477-.14 10 10 0 0 0 .47-.172 10 10 0 0 0 .481-.197 10 10 0 0 0 .414-.201 10 10 0 0 0 .475-.252 10 10 0 0 0 .39-.238 10 10 0 0 0 .452-.301 10 10 0 0 0 .38-.291 10 10 0 0 0 .385-.315 10 10 0 0 0 .375-.347 10 10 0 0 0 .36-.363 10 10 0 0 0 .293-.334 10 10 0 0 0 .353-.434 10 10 0 0 0 .28-.393 10 10 0 0 0 .263-.4 10 10 0 0 0 .264-.461 10 10 0 0 0 .228-.436 10 10 0 0 0 .195-.437 10 10 0 0 0 .196-.48 10 10 0 0 0 .228-.69 10 10 0 0 0 .028-.094 10 10 0 0 0 .021-.066 10 10 0 0 0 .098-.461 10 10 0 0 0 .103-.482 10 10 0 0 0 .051-.418 10 10 0 0 0 .065-.575 10 10 0 0 0 .005-.144A10 10 0 0 0 22 12a10 10 0 0 0-.01-.197 10 10 0 0 0-.025-.496 10 10 0 0 0-.043-.49 10 10 0 0 0-.074-.489 10 10 0 0 0-.098-.51 10 10 0 0 0-.12-.47 10 10 0 0 0-.14-.477 10 10 0 0 0-.172-.47 10 10 0 0 0-.197-.481 10 10 0 0 0-.201-.414 10 10 0 0 0-.252-.475 10 10 0 0 0-.238-.39 10 10 0 0 0-.301-.452 10 10 0 0 0-.291-.38 10 10 0 0 0-.315-.385 10 10 0 0 0-.347-.375 10 10 0 0 0-.363-.36 10 10 0 0 0-.334-.293 10 10 0 0 0-.434-.353 10 10 0 0 0-.393-.28 10 10 0 0 0-.4-.263 10 10 0 0 0-.461-.264 10 10 0 0 0-.436-.228 10 10 0 0 0-.437-.196 10 10 0 0 0-.48-.195 10 10 0 0 0-.69-.228 10 10 0 0 0-.094-.028 10 10 0 0 0-.066-.021 10 10 0 0 0-.461-.098 10 10 0 0 0-.482-.103 10 10 0 0 0-.418-.051 10 10 0 0 0-.575-.065 10 10 0 0 0-.144-.005A10 10 0 0 0 12 2zm-.016 5.002a5 5 0 0 1 .262.01 5 5 0 0 1 .227.011 5 5 0 0 1 .341.05 5 5 0 0 1 .135.019 5 5 0 0 1 .014.004 5 5 0 0 1 .115.025 5 5 0 0 1 .303.076 5 5 0 0 1 .265.086 5 5 0 0 1 .2.074 5 5 0 0 1 .242.106 5 5 0 0 1 .228.11 5 5 0 0 1 .196.109 5 5 0 0 1 .244.15 5 5 0 0 1 .17.12 5 5 0 0 1 .224.171 5 5 0 0 1 .186.16 5 5 0 0 1 .176.164 5 5 0 0 1 .172.18 5 5 0 0 1 .177.203 5 5 0 0 1 .133.172 5 5 0 0 1 .16.223 5 5 0 0 1 .133.214 5 5 0 0 1 .12.21 5 5 0 0 1 .107.216 5 5 0 0 1 .109.24 5 5 0 0 1 .084.223 5 5 0 0 1 .08.242 5 5 0 0 1 .07.264 5 5 0 0 1 .047.207 5 5 0 0 1 .045.277 5 5 0 0 1 .028.227 5 5 0 0 1 .02.351 5 5 0 0 1 .003.079 5 5 0 0 1-.012.271 5 5 0 0 1-.011.227 5 5 0 0 1-.05.341 5 5 0 0 1-.019.135 5 5 0 0 1-.004.014 5 5 0 0 1-.025.115 5 5 0 0 1-.076.303 5 5 0 0 1-.086.265 5 5 0 0 1-.074.2 5 5 0 0 1-.106.242 5 5 0 0 1-.11.228 5 5 0 0 1-.109.196 5 5 0 0 1-.15.244 5 5 0 0 1-.12.17 5 5 0 0 1-.171.224 5 5 0 0 1-.16.186 5 5 0 0 1-.164.176 5 5 0 0 1-.18.172 5 5 0 0 1-.203.177l-.002.002c-.018.019-.028.035-.047.053l-3.959 5.09-3.05-.979a141.684 141.684 0 0 0 3.177-3.084 5 5 0 0 1-.103-.015 5 5 0 0 1-.149-.024 5 5 0 0 1-.115-.025 5 5 0 0 1-3.57-3.04 5.072 5.072 0 0 1-.206-.661 5 5 0 0 1-.033-.147c-.025-.118-.036-.24-.054-.36-.987.993-1.964 1.993-2.954 3.05l-.98-3.053 5.092-3.957c.043-.044.082-.07.125-.11a5 5 0 0 1 .71-.634c.18-.13.367-.25.561-.356a5 5 0 0 1 .16-.08 4.94 4.94 0 0 1 .516-.222 5 5 0 0 1 .147-.057c.211-.07.43-.123.654-.164a5 5 0 0 1 .172-.027c.236-.035.476-.058.722-.059zM12 9a3 3 0 0 0-.053.002 3 3 0 0 0-.166.01 3 3 0 0 0-.133.011 3 3 0 0 0-.17.026 3 3 0 0 0-.113.021 3 3 0 0 0-.19.05 3 3 0 0 0-.103.03 3 3 0 0 0-.16.057 3 3 0 0 0-.129.053 3 3 0 0 0-.146.072 3 3 0 0 0-.12.063 3 3 0 0 0-.132.082 3 3 0 0 0-.123.08 3 3 0 0 0-.116.088 3 3 0 0 0-.126.105 3 3 0 0 0-.1.094 3 3 0 0 0-.111.111 3 3 0 0 0-.096.107 3 3 0 0 0-.094.116 3 3 0 0 0-.098.136 3 3 0 0 0-.072.11 3 3 0 0 0-.076.133 3 3 0 0 0-.07.132 3 3 0 0 0-.063.14 3 3 0 0 0-.054.14 3 3 0 0 0-.077.228 3 3 0 0 0-.007.026 3 3 0 0 0-.03.138 3 3 0 0 0-.031.149 3 3 0 0 0-.014.11 3 3 0 0 0-.02.183 3 3 0 0 0-.001.052A3 3 0 0 0 9 12a3 3 0 0 0 .002.053 3 3 0 0 0 .01.166 3 3 0 0 0 .011.133 3 3 0 0 0 .026.17 3 3 0 0 0 .021.113 3 3 0 0 0 .05.19 3 3 0 0 0 .03.103 3 3 0 0 0 .057.16 3 3 0 0 0 .053.129 3 3 0 0 0 .072.146 3 3 0 0 0 .063.12 3 3 0 0 0 .082.132 3 3 0 0 0 .08.123 3 3 0 0 0 .088.116 3 3 0 0 0 .105.126 3 3 0 0 0 .094.1 3 3 0 0 0 .111.111 3 3 0 0 0 .107.096 3 3 0 0 0 .116.094 3 3 0 0 0 .136.098 3 3 0 0 0 .11.072 3 3 0 0 0 .133.076 3 3 0 0 0 .132.07 3 3 0 0 0 .135.06 3 3 0 0 0 .153.061 3 3 0 0 0 .216.07 3 3 0 0 0 .004.003 3 3 0 0 0 .026.007 3 3 0 0 0 .138.03 3 3 0 0 0 .149.031 3 3 0 0 0 .11.014 3 3 0 0 0 .183.02 3 3 0 0 0 .011.001 3 3 0 0 0 .041 0A3 3 0 0 0 12 15a3 3 0 0 0 .053-.002 3 3 0 0 0 .166-.01 3 3 0 0 0 .133-.011 3 3 0 0 0 .17-.026 3 3 0 0 0 .113-.021 3 3 0 0 0 .19-.05 3 3 0 0 0 .103-.03 3 3 0 0 0 .16-.057 3 3 0 0 0 .129-.053 3 3 0 0 0 .146-.072 3 3 0 0 0 .12-.063 3 3 0 0 0 .132-.082 3 3 0 0 0 .123-.08 3 3 0 0 0 .116-.088 3 3 0 0 0 .126-.105 3 3 0 0 0 .1-.094 3 3 0 0 0 .111-.111 3 3 0 0 0 .096-.107 3 3 0 0 0 .094-.116 3 3 0 0 0 .098-.136 3 3 0 0 0 .072-.11 3 3 0 0 0 .076-.133 3 3 0 0 0 .07-.132 3 3 0 0 0 .06-.135 3 3 0 0 0 .061-.153 3 3 0 0 0 .07-.216 3 3 0 0 0 .003-.004 3 3 0 0 0 .007-.026 3 3 0 0 0 .03-.138 3 3 0 0 0 .031-.149 3 3 0 0 0 .002-.008 3 3 0 0 0 .012-.101 3 3 0 0 0 .02-.184 3 3 0 0 0 .001-.011 3 3 0 0 0 0-.041A3 3 0 0 0 15 12a3 3 0 0 0-.002-.053 3 3 0 0 0-.01-.166 3 3 0 0 0-.011-.133 3 3 0 0 0-.026-.17 3 3 0 0 0-.021-.113 3 3 0 0 0-.05-.19 3 3 0 0 0-.03-.103 3 3 0 0 0-.057-.16 3 3 0 0 0-.053-.129 3 3 0 0 0-.072-.146 3 3 0 0 0-.063-.12 3 3 0 0 0-.082-.132 3 3 0 0 0-.08-.123 3 3 0 0 0-.088-.116 3 3 0 0 0-.105-.126 3 3 0 0 0-.094-.1 3 3 0 0 0-.111-.111 3 3 0 0 0-.107-.096 3 3 0 0 0-.116-.094 3 3 0 0 0-.136-.098 3 3 0 0 0-.11-.072 3 3 0 0 0-.133-.076 3 3 0 0 0-.132-.07 3 3 0 0 0-.14-.063 3 3 0 0 0-.14-.054 3 3 0 0 0-.228-.077 3 3 0 0 0-.026-.007 3 3 0 0 0-.138-.03 3 3 0 0 0-.149-.031 3 3 0 0 0-.008-.002 3 3 0 0 0-.101-.012 3 3 0 0 0-.184-.02 3 3 0 0 0-.011-.001 3 3 0 0 0-.041 0A3 3 0 0 0 12 9z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 720 720" id="arduino" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="ana" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-opacity="100%" stroke-width="60" stroke="#00979c" d="M174 30a10.5 10.1 0 0 0 0 280C364 320 344 30 544 30a10.5 10.1 0 0 1 0 280C354 320 374 30 174 30"/><path d="M528 205v-32.8h-32.5v-13.7H528V126h13.9v32.5h32.5v13.7h-32.5V205H528z" text-anchor="middle" fill="#00979c" stroke-width="20" stroke="#00979c" font-family="sans-serif" font-size="167"/><path fill="#00979c" stroke="#00979c" stroke-width="23.6" transform="matrix(1.56 0 0 .64 -366 .528)" d="M321 266v-17.4h53.3V266H321z"/></symbol></defs><title>Layer 1</title><use x="20.063" y="360.85" transform="matrix(.997 0 0 .997 -18.596 -159.19)" xlink:href="#ana"/></symbol><symbol viewBox="0 0 24 24" id="assembly" xmlns="http://www.w3.org/2000/svg"><path d="M1.746 1.566v20.905H5.13v-2.088H3.438V3.656h1.69v-2.09H1.747zm17.219 0v2.09h1.693v16.727h-1.693v2.09h3.383V1.566h-3.383zM15.196 3.988c-.5 0-.93.076-1.29.225-.359.15-.652.372-.877.671-.226.302-.39.673-.494 1.108a6.715 6.715 0 0 0-.155 1.54c0 .573.049 1.083.15 1.528.1.442.264.811.49 1.11.222.298.512.524.872.676.36.153.795.23 1.304.23.518 0 .954-.075 1.308-.224.353-.153.643-.376.869-.671.219-.29.38-.661.484-1.112.104-.454.156-.967.156-1.54 0-.573-.052-1.079-.152-1.515a2.92 2.92 0 0 0-.485-1.106 2.09 2.09 0 0 0-.868-.686c-.354-.155-.79-.234-1.312-.234zm-6.814.12a.941.941 0 0 1-.138.458.849.849 0 0 1-.356.296A1.71 1.71 0 0 1 7.385 5a5.244 5.244 0 0 1-.631.037v1.11H8.19v3.6H6.754v1.188h4.545V9.745H9.894V4.11H8.382zm6.814 1.138c.375 0 .643.176.805.527.161.348.241.933.241 1.756 0 .814-.082 1.399-.247 1.756-.164.356-.43.534-.799.534-.369 0-.636-.178-.8-.534-.165-.357-.248-.941-.248-1.749 0-.829.082-1.415.243-1.763.162-.35.43-.527.805-.527zm-6.33 7.64c-.5 0-.93.073-1.29.223-.359.15-.651.374-.877.673-.225.302-.39.67-.494 1.106a6.715 6.715 0 0 0-.155 1.54c0 .573.05 1.082.15 1.527.1.442.264.814.49 1.112.222.3.514.525.874.677.36.152.793.229 1.302.229.519 0 .954-.076 1.308-.225.354-.153.643-.376.869-.672.22-.29.38-.66.484-1.111.104-.455.156-.967.156-1.54 0-.573-.05-1.079-.15-1.515a2.923 2.923 0 0 0-.487-1.106 2.084 2.084 0 0 0-.867-.686c-.353-.156-.791-.232-1.313-.232zm5.846.119a.941.941 0 0 1-.138.457.85.85 0 0 1-.356.296 1.71 1.71 0 0 1-.503.137 5.245 5.245 0 0 1-.631.037v1.112h1.435v3.597h-1.435v1.189h4.545v-1.189h-1.405v-5.636h-1.512zm-5.846 1.137c.375 0 .643.176.805.527.162.347.241.933.241 1.756 0 .813-.08 1.399-.245 1.755-.164.357-.432.534-.8.534-.37 0-.637-.177-.802-.534-.164-.356-.245-.939-.245-1.746 0-.83.08-1.418.242-1.765.161-.35.43-.527.804-.527z" fill="#ff6e40"/></symbol><symbol viewBox="0 0 24 24" id="aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="api" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apa"/><linearGradient id="apa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#apb"/><linearGradient id="apb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#apc"/><linearGradient id="apc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#apd"/><linearGradient id="apd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#ape"/><linearGradient id="ape" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#apf"/><linearGradient id="apf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apg"/><linearGradient id="apg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="app" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#aph"/><linearGradient id="aph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><g transform="rotate(11.282 -1.694 21.569) scale(.47102)" clip-rule="evenodd" fill="none" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#api)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#apj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#apk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#apl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#apm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#apn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#apo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#app)"/></g></symbol><symbol viewBox="0 0 24 24" id="autohotkey" xmlns="http://www.w3.org/2000/svg"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm3.668 3.447a.9.9 0 0 1 .652.256.84.84 0 0 1 .262.625c0 .34-.014.852-.041 1.537-.022.68-.033 1.19-.033 1.53 0 .111-.016.326-.047.644a6.149 6.149 0 0 0-.033.68l2.578-.485c1.007-.179 1.874-.281 2.603-.308.018-.3.048-1.105.088-2.416.01-.345.115-.742.317-1.19.25-.55.533-.826.851-.826.237 0 .448.08.631.236.197.17.295.382.295.637a.775.775 0 0 1-.025.201c-.09.327-.135.612-.135.854 0 .125-.014.32-.041.584-.023.26-.033.453-.033.578 0 .425-.022 1.056-.067 1.893a38.963 38.963 0 0 0-.068 1.892c0 .327.025.816.074 1.465.05.649.074 1.136.074 1.463a.84.84 0 0 1-.261.625.893.893 0 0 1-.65.254 1 1 0 0 1-.686-.254.777.777 0 0 1-.29-.611c0-.327-.015-.818-.046-1.471a39.552 39.552 0 0 1-.041-1.47c0-.256.004-.482.013-.679-.702.032-1.57.142-2.603.33-.86.157-1.719.316-2.578.477-.01.304-.042.812-.096 1.523a22.354 22.354 0 0 0-.066 1.538.84.84 0 0 1-.262.625.893.893 0 0 1-.65.253.898.898 0 0 1-.653-.253.84.84 0 0 1-.262-.625c0-.452.038-1.128.114-2.028.08-.9.12-1.575.12-2.027 0-.573.015-1.436.042-2.586.027-1.155.04-2.017.04-2.59a.84.84 0 0 1 .263-.625.895.895 0 0 1 .65-.256z" fill="#4caf50"/></symbol><symbol viewBox="0 0 24 24" id="autoit" xmlns="http://www.w3.org/2000/svg"><defs id="ardefs8"><style id="arstyle4482">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style><style id="arstyle4510">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style></defs><g id="arg4522" transform="translate(-59.538 -26.404) scale(.0555)"><path d="M12.8 2.133A10.666 10.666 0 0 0 2.136 12.799 10.666 10.666 0 0 0 12.8 23.465a10.666 10.666 0 0 0 10.668-10.666A10.666 10.666 0 0 0 12.8 2.133zm.15 4.713c.456 0 .836.105 1.142.314.306.21.565.469.78.78l6.089 8.812H9.627l1.82-2.506h3.36c.315 0 .589.01.822.03a11.93 11.93 0 0 1-.473-.663 39.13 39.13 0 0 0-.517-.75l-1.748-2.578-4.577 6.467H4.746l6.25-8.813c.204-.281.46-.534.772-.757.31-.224.705-.336 1.181-.336z" transform="matrix(16.89188 0 0 16.89188 1072.761 475.745)" id="arcircle4514" fill="#1976d2" stroke-width=".026"/></g></symbol><symbol viewBox="0 0 213.33333 213.33333" id="babel" xmlns="http://www.w3.org/2000/svg"><path d="M50.22 199.659c-.875-.406-1.261-1.6-.857-2.652.404-1.053.12-1.914-.63-1.914s-1.615.748-1.92 1.663c-.328.983-1.27.302-2.304-1.667-.962-1.831-3.718-5.533-6.126-8.226-9.418-10.535-7.71-27.444 5.432-53.77 12.459-24.96 23.117-39.033 45.966-60.696 30.229-28.66 52.679-46.223 70.587-55.22 10.98-5.518 13.025-5.059 2.778.624-11.004 6.102-11.378 6.359-10.512 7.226.33.33 7.306-2.67 15.504-6.667 15.87-7.737 16.34-7.912 16.34-6.082 0 .652-4.95 3.738-11 6.858-13.062 6.736-12.722 6.48-10.472 7.872 1.117.69 5.428-.582 11.54-3.406 5.367-2.48 10.397-4.508 11.179-4.508 2.755 0-3.928 5.302-11.541 9.157-20.437 10.35-68.937 46.043-68.07 50.097.166.777-5.792 7.639-13.241 15.248-15.257 15.587-26.14 30.002-33.748 44.706-6.379 12.326-7.457 17.734-5.385 26.996 3.482 15.56 11.592 18.366 31.482 10.895 28.228-10.603 45.758-28.704 47.022-48.556.602-9.442-1.317-13.479-8.52-17.93-4.01-2.48-5.268-2.621-12.065-1.365-4.173.771-10.153 2.906-13.289 4.744s-6.455 3.34-7.377 3.34c-.922 0-3.216 1.336-5.096 2.968-1.88 1.633.48-1.13 5.247-6.14 6.82-7.167 7.956-8.9 5.333-8.132-5.208 1.525-10.194 4.33-15.649 8.803-2.76 2.264-.923.175 4.08-4.641 11.565-11.131 21.183-15.97 33.088-16.641 17.097-.966 27.254 5.805 31.964 21.31 2.435 8.017 2.609 10.24 1.353 17.37-1.65 9.361-7.034 21.553-15.593 35.307-4.398 7.067-8.434 11.427-15.588 16.844-9.166 6.94-15.654 11.02-15.654 9.845 0-.295 2.455-2.161 5.455-4.147 8.818-5.835 5.075-5.377-8.326 1.02-6.854 3.27-15.199 6.593-18.542 7.38-7.106 1.675-30.527 3.164-32.846 2.089zm-8.408-19.899c0-1.1-.6-2-1.333-2-.734 0-1.334.9-1.334 2s.6 2 1.333 2c.734 0 1.334-.9 1.334-2zm89.255-8.204c1.53-1.945 2.473-3.845 2.097-4.222-.377-.377-.836-.435-1.02-.13-.182.306-1.787 2.206-3.565 4.223-1.778 2.016-2.571 3.666-1.763 3.666s2.72-1.591 4.25-3.536zm-77.644-1.745c-.82-2.172-1.74-3.7-2.045-3.396-.951.952 1.088 7.345 2.343 7.345.656 0 .522-1.777-.298-3.95zm82.303-27.915c-.837-.837-3.217 2.55-3.184 4.53.012.734.896.178 1.965-1.235 1.07-1.413 1.618-2.896 1.219-3.295zm-66.238-36.904c-1.312-1.312-3.676.702-3.676 3.133 0 2.035.175 2.031 2.254-.047 1.24-1.24 1.88-2.628 1.422-3.086zm39.657.768c4.403-2.196 6.8-3.986 5.333-3.982-2.838.01-16.667 6.028-16.667 7.254 0 1.6 3.717.527 11.333-3.272zm16.667-5.333c0-.733-.9-1.333-2-1.333s-2 .6-2 1.333.9 1.333 2 1.333 2-.6 2-1.333zm-3.334-3.923l5.334-1.104-7.334-.133c-4.033-.073-8.233.45-9.333 1.16-2.539 1.64 3.572 1.682 11.333.077zm35.738-63.976c2.788-1.69 4.765-3.376 4.393-3.748-.947-.947-11.942 5.654-14.237 8.548-1.792 2.258-1.714 2.276 1.44.329a1452.76 1452.76 0 0 1 8.403-5.13z" fill="#ffca28" stroke-width="1.333"/></symbol><symbol viewBox="0 0 400 400" fill-opacity=".05" id="bithound" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.88 0 0 .88 24.121 2.895)" fill="#e53935" fill-opacity="1"><path d="M370.5 207c-1.5-14.8-4.8-29.9-9.5-44-13.5-40.3-38.6-81.6-70.3-110.1-1.4-1.2-6.7-4.4-8.7-3.3-5.2 2.9 4.6 22.8 5.8 26.4 7.4 22 12.1 45.3 6.8 68.3-7.1 30.4-30.4 51.7-61.5 54.3-17.1 1.4-34.3-.5-51.4 1.5-25.6 3-51.7 11.8-68 32.8-1.9 2.4-3.6 5.1-5.2 7.9h-.4c-6.3.7-12.6-2-15.7-3.7-.8-.5-1.6-.9-2.2-1.2-19-10.5-33-34-41.6-53.4-3.9-9-7.2-18.4-9.3-27.9-1-4.3-1.1-8.8-1.3-13.2-.1-2.7.3-6.5-1.2-8.9-3.3-5.2-7.5-.2-8.2 4-1.1 6.9-2.1 13.7-1.8 20.7.5 11.8 3.8 23.5 8 34.5 6.2 16.2 14.9 31.1 26.2 44.4 4.7 5.5 9.7 10.6 15.1 15.3 4.8 4.3 10.9 7.7 14.5 13.2 4.2 6.3 4.9 14.1 4.5 21.4-1 19.3-1.6 37.4 3.9 56.2 4.8 16.7 10.8 33.8 20.8 48.1 5 7.1 11.2 14.6 18 19.9 4.6 3.6 13.3 4 8.3-9.2-11.1-29.3-12.1-59.7 5.2-87.1 14.5-22.8 40.1-43.1 69-39.5 42.5 5.3 72.1 44.3 70 86-.6 11.7-1 21.7-4.7 32.7-1.5 4.4-2.6 10-1.5 14.6 1.8 7.8 10.5 4.9 14.3-.2 10.3-14 21.1-27.6 30.8-42 31.6-47.2 47-101.8 41.3-158.5z"/><path d="M132.4 92.1c.7 2.3 1.4 4.8 1.9 7.5.1 1.1.4 2.3 1 3.4 2.6 6.8 8.9 10.5 14.8 14 3.6 2.2 10.1 4.3 14.1 5.9 5.2 2.1 16.4-.6 21.7-1 12.2-1 23.5-5.3 34.7 1.2-57.4 67.3-3.2 82.3 38.8 49.9 48-37 2.8-124.3 2.8-124.3s-1-6.8-19.2-10.8c-1.7-.9-3.4-1.7-5.1-2.4-18-8.3-34.2 5.3-47.2 16.4-3.8 3.2-7.5 6.4-11.5 9.4-5.4 4-11.2 7.3-17.3 10.2-6.4 3-14 6.4-21.1 6.7-1 0-2.9.2-4.9.6-3.1.3-4.7 1.1-5.4 2.5-1.2 1-2 2.4-1.8 4.2.2 2.5 1.4 4.6 2.7 6.2.4.1.7.3 1 .4z"/></g></symbol><symbol viewBox="0 0 400.00001 399.99999" id="bower" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12.061 33.203) scale(.81733)"><path d="M447.61 200.08c-23.139-22.234-138.85-36.114-175.36-40.154a107.137 107.137 0 0 0 4.517-12.944 146.107 146.107 0 0 1 15.905-5.901c.677 1.997 3.865 9.648 5.682 13.279 73.415 2.025 77.184-54.557 80.17-70.058 2.92-15.157 2.771-29.802 27.953-56.575-37.516-10.933-91.467 16.945-109.54 58.437-6.79-2.545-13.597-4.424-20.328-5.586-4.824-19.46-29.944-73.672-95.863-73.672-83.46 0-174.43 68.853-174.43 185.41 0 97.976 66.891 183.84 104.68 183.84 16.505 0 30.703-12.36 34.036-23.44 2.795 7.597 11.368 31.213 14.184 37.225 4.162 8.89 23.41 16.583 31.833 7.357 10.83 6.017 30.703 9.641 41.534-6.405 20.86 4.412 39.3-8.026 39.702-22.868 10.235-.546 15.256-14.918 13.021-26.363-1.647-8.426-19.248-38.66-26.113-49.098 13.59 11.054 48.013 14.183 52.194.007 21.911 17.198 56.057 8.171 58.765-5.815 26.624 6.917 57.16-8.276 52.146-26.676 42.771-2.958 37.296-48.464 25.296-59.996z" fill="#543729" stroke-width=".973"/><path d="M328.514 103.025c9.212-18.277 20.788-38.234 35.409-50.58-16.093 6.485-31.981 25.873-41.375 46.595a144.914 144.914 0 0 0-14.552-8.132c13.105-27.972 43.555-51.332 77.112-53.157-22.477 20.385-14.498 62.754-32.979 85.183-5.288-5.311-17.43-15.562-23.615-19.909zm-14.53 29.762c.01-.7.272-6.094.763-8.557-1.288-.304-9.3-1.87-13.476-1.772-.304 5.245 2.204 14.17 4.684 19.541 17.075-.358 29.408-5.471 36.667-10.172-6.18-2.88-16.726-5.442-24.745-6.974-.894 1.851-3.097 6.568-3.892 7.934z" fill="#00acee"/><g stroke-width=".973"><path d="M250.54 277.39c.004.024.015.057.018.082-2.165-4.657-4.463-10.314-7.208-17.708 10.688 15.557 44.184 7.533 42.427-6.407 16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455 28 5.4 54.832 10.783 63.256 12.938-5.595 9.123-18.339 15.566-37.549 11.089 10.38 14.14-9.773 31.105-37.844 21.76 6.18 13.883-18.814 26.38-47.22 11.91.361 13.889-35.24 15.488-49.315.143zm55.543-70.194c32.497 2.495 86.238 7.34 119.51 11.997-2.102-10.828-7.844-13.921-25.905-18.772-19.425 2.072-68.706 6.913-93.604 6.776z" fill="#2baf2b"/><path d="M285.78 253.36c16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455-33.103-6.383-67.84-12.788-75.719-13.908 4.78.254 12.702.797 22.59 1.556 24.899.137 74.18-4.704 93.604-6.775-31.452-7.975-95.666-19.613-140.01-22.48-2.055 3.003-5.833 8.097-12.413 13.51-19.403 41.053-54.557 68.34-93.454 68.34-11.335 0-24.018-1.912-38.233-6.456-8.865 9.497-46.661 16.694-77.329 1.641 24.326 56.961 80.74 94.984 143.19 94.984 52.591 0 75.912-53.704 70.808-67.914-1.238-3.45-6.145-14.889-8.891-22.283 10.689 15.556 44.185 7.532 42.429-6.408z" fill="#ffcc2f"/><path d="M253.91 145.27c4.644-2.526 20.69-12.253 35.981-15.908a67.843 67.843 0 0 1-.536-5.12c-10.032 2.403-28.945 10.51-39.784-.661 22.866 6.9 34.283-6.149 51.09-6.149 10.014 0 24.305 2.798 35.57 7.22-9.061-8.37-38.772-33.63-75.558-33.717-8.213 9.957-17.09 31.526-6.764 54.334z" fill="#cecece"/><path d="M115.58 253.33c14.215 4.544 26.898 6.457 38.233 6.457 38.896 0 74.05-27.29 93.454-68.341-14.351 11.978-39.291 22.228-78.241 22.228 34.694-7.866 64.56-25.156 79.753-50.427-10.68-16.998-22.263-54.603 7.07-84.33-4.512-14.497-26.475-52.766-75.095-52.766-84.85 0-155.17 71.001-155.17 166.15 0 22.525 4.547 43.65 12.67 62.664 30.666 15.054 68.462 7.858 77.327-1.64z" fill="#ef5734"/><path d="M141.03 108.45c0 21.644 17.546 39.191 39.19 39.191s39.192-17.548 39.192-39.191c0-21.644-17.548-39.191-39.192-39.191-21.644 0-39.19 17.547-39.19 39.191z" fill="#ffcc2f"/><path d="M156.76 108.45c0 12.958 10.507 23.463 23.463 23.463 12.96 0 23.464-10.506 23.464-23.463 0-12.959-10.504-23.464-23.464-23.464-12.957 0-23.463 10.506-23.463 23.464z" fill="#543729"/><ellipse cx="180.22" cy="98.044" rx="13.673" ry="8.501" fill="#fff"/></g></g></symbol><symbol viewBox="0 0 140 140" id="browserlist" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><path d="M70.314 10.066a59.828 59.828 0 0 0-59.828 59.828 59.828 59.828 0 0 0 59.828 59.828 59.828 59.828 0 0 0 59.828-59.828 59.828 59.828 0 0 0-59.828-59.828zm-4.836 8.785c.496 4.043 1.352 7.322 2.572 10.223 4.779-4.287 10.265-7.546 16.041-9.02-.981 3.938-1.357 7.295-1.261 10.43 6.026-2.314 12.349-3.404 18.3-2.706-3.182 2.413-5.482 4.717-7.128 7.015-2.201 12.074 6.858 20.43 14.779 24.551a5.128 5.128 0 0 1 5.183-3.888 5.128 5.128 0 0 1 3.7 8.435v.002c-.487 1.055-2.002 2.343-3.497 3.219-4.075 2.39-11.172 5.736-20.914 7.39.045 1.214.077 2.453.077 3.747 0 4.817-.485 8.291-1.385 10.699-3.3 13.313-12.648 26.76-24.695 31.95.357-4.083.197-7.485-.402-10.591-5.582 3.218-11.646 5.278-17.623 5.52h-.002c1.785-3.662 2.855-6.878 3.412-9.976-6.347.996-12.727.742-18.377-1.17 2.93-2.732 5.054-5.314 6.673-7.96-6.292-1.344-12.169-3.87-16.766-7.686 3.822-1.544 6.795-3.239 9.3-5.197-5.426-3.517-10.034-7.998-12.972-13.23 4.012-.07 7.321-.568 10.3-1.453-3.786-5.215-6.468-11.032-7.333-16.951 3.861 1.405 7.196 2.133 10.36 2.355-1.662-6.22-2.081-12.605-.768-18.436 3.03 2.634 5.824 4.48 8.63 5.815.678-6.406 2.576-12.52 5.893-17.496 1.926 3.622 3.914 6.391 6.111 8.672 2.93-5.754 6.9-10.798 11.791-14.262zm26.465 19.557c-2.395 5.514-1.665 11.297-.555 18.732a2.138 2.138 0 0 0 .28-4.178 3.419 3.419 0 1 1 .092 6.704c.574 3.882 1.157 8.18 1.421 13.125a67.143 67.143 0 0 0 3.25-.649c6.616-1.487 12.258-3.801 16.871-6.506.45-.264.884-.563 1.276-.867.366-.557.333-.957.035-1.285-4.831-1.245-10.891-4.53-15.258-8.795-4.764-4.653-7.428-10.164-7.412-16.281z" fill="#ffca28" stroke-width=".855"/></symbol><symbol viewBox="0 0 140 140" id="browserlist_light" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><g transform="translate(10.823 10.1)" stroke-width=".855"><circle cx="59.492" cy="59.795" r="59.828" fill="#ffca28"/><path d="M54.656 8.752c-4.89 3.464-8.862 8.508-11.791 14.262-2.198-2.28-4.185-5.05-6.111-8.672-3.318 4.976-5.216 11.09-5.893 17.496-2.807-1.335-5.6-3.18-8.63-5.814-1.314 5.83-.895 12.216.767 18.436-3.164-.223-6.498-.95-10.36-2.356.865 5.92 3.548 11.737 7.333 16.951-2.978.885-6.287 1.383-10.3 1.453 2.939 5.233 7.547 9.714 12.972 13.23-2.505 1.959-5.478 3.654-9.299 5.198 4.596 3.815 10.474 6.341 16.766 7.685-1.62 2.647-3.743 5.228-6.674 7.96 5.65 1.912 12.03 2.166 18.377 1.17-.556 3.098-1.626 6.314-3.412 9.975h.002c5.977-.24 12.042-2.3 17.623-5.52.6 3.108.76 6.51.402 10.593 12.047-5.19 21.395-18.638 24.695-31.951.9-2.408 1.385-5.881 1.385-10.7 0-1.293-.031-2.531-.076-3.745 9.742-1.655 16.839-5.001 20.914-7.39 1.494-.877 3.01-2.165 3.496-3.22v-.002a5.128 5.128 0 0 0-3.7-8.435 5.128 5.128 0 0 0-5.183 3.889c-7.92-4.122-16.98-12.477-14.779-24.551 1.646-2.299 3.947-4.603 7.13-7.016-5.952-.698-12.276.392-18.302 2.707-.095-3.135.28-6.492 1.262-10.43-5.776 1.473-11.262 4.733-16.041 9.02-1.22-2.902-2.076-6.18-2.572-10.223zm26.465 19.557c-.015 6.117 2.648 11.628 7.412 16.281 4.366 4.265 10.426 7.55 15.258 8.795.298.328.331.728-.035 1.285-.392.304-.825.603-1.275.867-4.613 2.704-10.256 5.019-16.871 6.506-1.071.24-2.154.458-3.25.649-.265-4.945-.848-9.243-1.422-13.125a3.419 3.419 0 1 0-.092-6.703 2.138 2.138 0 0 1-.28 4.177c-1.11-7.435-1.84-13.218.555-18.732z" fill="#37474f"/></g></symbol><symbol viewBox="0 0 24 24" id="bucklescript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm14.1 8.858a5.5 5.5 0 0 1 1.26.145c.417.093.778.213 1.082.357v1.723h-.18a3.281 3.281 0 0 0-.959-.603 2.867 2.867 0 0 0-1.155-.247c-.14 0-.277.011-.416.035a1.4 1.4 0 0 0-.395.12.756.756 0 0 0-.291.231.54.54 0 0 0-.123.348c0 .198.065.35.196.456.13.104.376.2.738.288.237.057.466.11.683.164.22.054.455.128.706.222.496.188.86.444 1.095.77.238.32.357.738.357 1.253 0 .737-.271 1.336-.813 1.798-.538.46-1.27.689-2.197.689a5.447 5.447 0 0 1-1.402-.161 6.725 6.725 0 0 1-1.117-.416v-1.794h.183c.344.318.73.563 1.155.734.429.17.839.256 1.233.256.1 0 .235-.01.4-.03.166-.02.3-.055.403-.102a.97.97 0 0 0 .313-.225c.084-.09.127-.223.127-.4a.568.568 0 0 0-.183-.424c-.119-.12-.294-.213-.526-.276-.243-.067-.5-.128-.773-.185a5.523 5.523 0 0 1-.76-.227c-.544-.204-.936-.48-1.177-.828-.237-.351-.357-.786-.357-1.305 0-.697.27-1.265.81-1.703.54-.442 1.235-.663 2.083-.663zm-8.981.135h2.51c.521 0 .903.02 1.143.06.243.041.484.13.721.266.246.144.43.338.548.583.121.24.181.518.181.83 0 .36-.082.68-.247.959a1.697 1.697 0 0 1-.7.642v.04c.423.098.758.298 1.004.603.249.305.373.706.373 1.205 0 .361-.063.686-.19.97-.125.285-.296.52-.516.707a2.31 2.31 0 0 1-.845.472c-.304.094-.69.141-1.159.141H8.12v-7.478zm1.659 1.372v1.582h.262c.263 0 .486-.007.672-.017.185-.01.332-.043.44-.1.15-.077.248-.175.294-.295.046-.124.07-.266.07-.427a.91.91 0 0 0-.083-.371.518.518 0 0 0-.282-.277 1.187 1.187 0 0 0-.456-.086c-.18-.007-.433-.01-.76-.01h-.157zm0 2.873V18.1H9.9c.469 0 .804-.002 1.007-.006.202-.003.39-.046.56-.13a.712.712 0 0 0 .357-.33c.067-.142.099-.302.099-.483 0-.237-.04-.42-.121-.547-.078-.13-.214-.228-.405-.291a1.842 1.842 0 0 0-.538-.072 49.47 49.47 0 0 0-.716-.003h-.366z" fill="#26a69a" stroke-width="1.067"/></symbol><symbol viewBox="0 0 24 24" id="c" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 15.97l.42 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96-1.14-1.27-1.68-2.88-1.68-4.83C6 9.9 6.68 8.13 8 6.89 9.28 5.64 10.92 5 12.9 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.6 2.49-1.04-.34c-.4-.1-.87-.15-1.4-.15-1.15-.01-2.11.36-2.86 1.1-.76.73-1.14 1.85-1.18 3.34.01 1.36.37 2.42 1.08 3.2.71.77 1.7 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.09-.32z" fill="#0277bd"/></symbol><symbol viewBox="0 0 300 300" id="cabal" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -822.52)" fill-rule="evenodd" color="#000"><rect transform="matrix(-.98339 .18149 .60192 .79856 0 0)" x="405.55" y="967.22" width="107.25" height="156.59" rx="12.306" ry="12.31" fill="#2d9bbd"/><rect transform="matrix(-.98528 .17093 -.59175 .80612 0 0)" x="-1156.5" y="1461.9" width="108.34" height="123.15" rx="10.69" ry="12.31" fill="#4a4bcd"/><path d="M52.112 965.158c-1.343 3.515-26.292 23.248-25.744 27.277.548 4.03 29.812 16.023 32.04 19.027s10.545 41.668 13.603 42.5 18.828-31.274 21.548-32.932c2.72-1.658 32.808 2.503 34.15-1.01 1.343-3.515-18.174-35.352-18.721-39.381-.548-4.03 9.732-40.12 7.502-43.125-2.229-3.005-30.06 9.427-33.118 8.594-3.059-.833-26.793-27.3-29.514-25.643-2.72 1.657-.405 41.177-1.747 44.693z" fill="#2e5bc1"/></g></symbol><symbol viewBox="0 0 24 24" id="cake" xmlns="http://www.w3.org/2000/svg"><path d="M12.254 6.621a1.807 1.807 0 0 0 1.808-1.807c0-.344-.09-.66-.262-.932l-1.546-2.684-1.546 2.684a1.72 1.72 0 0 0-.262.932 1.808 1.808 0 0 0 1.808 1.807m4.158 9.04l-.967-.976-.976.976c-1.175 1.166-3.236 1.175-4.42 0l-.959-.976-.994.976a3.134 3.134 0 0 1-3.977.353v4.167a.904.904 0 0 0 .904.904h14.463a.904.904 0 0 0 .904-.904v-4.167a3.134 3.134 0 0 1-3.977-.353m1.265-6.328h-4.52V7.525H11.35v1.808H6.83a2.712 2.712 0 0 0-2.711 2.712v1.392c0 .977.795 1.772 1.771 1.772.489 0 .94-.18 1.248-.515l1.952-1.926 1.908 1.926c.669.669 1.835.669 2.504 0l1.916-1.926 1.944 1.926c.316.334.768.515 1.247.515.976 0 1.78-.795 1.78-1.772v-1.392a2.712 2.712 0 0 0-2.711-2.712z" fill="#ff7043" stroke-width=".904"/></symbol><symbol viewBox="0 0 24 24" id="certificate" xmlns="http://www.w3.org/2000/svg"><path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3 3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2 3-2v3.5l3 1.5-3 1.5V15l-3-2-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2z" fill="#ff5722"/></symbol><symbol viewBox="0 0 24 24" id="changelog" xmlns="http://www.w3.org/2000/svg"><path d="M11 7v5.11l4.71 2.79.79-1.28-4-2.37V7m0-5C8.97 2 5.91 3.92 4.27 6.77L2 4.5V11h6.5L5.75 8.25C6.96 5.73 9.5 4 12.5 4a7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-7.5 7.5c-3.27 0-6.03-2.09-7.06-5h-2.1c1.1 4.03 4.77 7 9.16 7 5.24 0 9.5-4.25 9.5-9.5A9.5 9.5 0 0 0 12.5 2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="clojure" xmlns="http://www.w3.org/2000/svg"><path d="M3.355 1.78c-.845 0-1.525.68-1.525 1.525v17.441c0 .845.68 1.525 1.525 1.525h17.442c.845 0 1.525-.68 1.525-1.525V3.305c0-.845-.68-1.526-1.525-1.526H3.355zm6.168 2.572h1.963l6.368 14.931H15.93l-3.38-8.086-3.349 8.086H7.21l4.346-10.38-2.032-4.551z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="cmake" xmlns="http://www.w3.org/2000/svg"><path d="M11.99 2.965L2.977 20.999l9.874-8.47-.863-9.564z" fill="#1e88e5"/><path d="M12.007 2.963l.002.29 1.312 14.498-.001.006.023.26 7.362 2.979h.416l-.158-.311-.114-.228h-.002l-8.84-17.494z" fill="#e53935"/><path d="M8.607 16.11L2.98 20.995h17.743v-.016L8.607 16.11z" fill="#7cb342"/></symbol><symbol class="bfmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate" xmlns="http://www.w3.org/2000/svg"><path class="bfsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#eee" stroke-width="2.849"/></symbol><symbol class="bgmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate_light" xmlns="http://www.w3.org/2000/svg"><path class="bgsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#455a64" stroke-width="2.849"/></symbol><symbol viewBox="0 0 24 24" id="coffee" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="coldfusion" xmlns="http://www.w3.org/2000/svg"><rect transform="rotate(90)" x="2.283" y="-21.86" width="19.487" height="19.487" ry="0" fill="#0d3858" stroke="#4dd0e1" stroke-width=".7"/><text x="6.653" y="16.426" fill="#4dd0e1" font-family="Calibri" font-size="29.001" font-weight="bold" letter-spacing="0" stroke-width=".725" word-spacing="0" style="line-height:1.25"><tspan x="6.653" y="16.426" font-family="'Segoe UI'" font-size="10.634" font-weight="normal">C<tspan font-size="11.844">f</tspan></tspan></text></symbol><symbol viewBox="0 0 24 24" id="conduct" xmlns="http://www.w3.org/2000/svg"><path d="M10 17l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#cddc39"/></symbol><symbol viewBox="0 0 24 24" id="console" xmlns="http://www.w3.org/2000/svg"><path d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z" fill="#ff7043"/></symbol><symbol viewBox="0 0 24 24" id="contributing" xmlns="http://www.w3.org/2000/svg"><path d="M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="cpp" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C1.56 15.77 1 14.16 1 12.21c.05-2.31.72-4.08 2-5.32C4.32 5.64 5.96 5 7.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M11 11h2V9h2v2h2v2h-2v2h-2v-2h-2v-2m7 0h2V9h2v2h2v2h-2v2h-2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="credits" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="#9ccc65"/></symbol><symbol viewBox="0 0 200 200" id="crystal" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" stroke-width="1.153" fill="#cfd8dc"/></symbol><symbol viewBox="0 0 200 200" id="crystal_light" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" fill="#37474f" stroke-width="1.153"/></symbol><symbol viewBox="0 0 24 24" id="csharp" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C2.56 15.77 2 14.16 2 12.21c.05-2.31.72-4.08 2-5.32C5.32 5.64 6.96 5 8.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M13.89 19l.61-4H13l.34-2h1.5l.32-2h-1.5L14 9h1.5l.61-4h2l-.61 4h1l.61-4h2l-.61 4H22l-.34 2h-1.5l-.32 2h1.5L21 15h-1.5l-.61 4h-2l.61-4h-1l-.61 4h-2m2.95-6h1l.32-2h-1l-.32 2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="css" xmlns="http://www.w3.org/2000/svg"><path d="M5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81-5.48 1.81-4.75-1.81.33-1.64H2.85l-.79 4 7.85 3 9.05-3 1.2-6.03.24-1.21L21.94 3H5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="css-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#42a5f5"/><path d="M4.676 3l-.488 2.51h10.211l-.33 1.623H3.864l-.496 2.502H13.58l-.57 2.863-4.119 1.36-3.569-1.36.248-1.232H3.06l-.593 3.005 5.898 2.254 6.8-2.254.902-4.53.18-.91L17.406 3H4.675z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 33 33" id="cucumber" xmlns="http://www.w3.org/2000/svg"><title>cucumber-mark-transparent-pips</title><g transform="translate(0 -5)" fill="none" fill-rule="evenodd"><path d="M-4-1h40v40H-4z"/><path d="M16.641 7.092c-7.028 0-12.714 5.686-12.714 12.714 0 6.187 4.435 11.327 10.288 12.471v3.64C21.824 34.77 28.561 28.73 29.063 20.8c.303-4.772-2.076-9.644-6.09-12.01a10.575 10.575 0 0 0-1.455-.728l-.243-.097c-.223-.082-.448-.175-.68-.242a12.614 12.614 0 0 0-3.954-.632zm2.62 4.707a1.387 1.387 0 0 0-1.213.485c-.233.31-.379.611-.534.923-.466 1.087-.31 2.251.388 3.105 1.087-.233 2.01-.927 2.475-2.014a2.45 2.45 0 0 0 .243-1.02c.048-.824-.634-1.404-1.359-1.479zm-5.654.073c-.708.068-1.382.63-1.382 1.407 0 .31.087.709.243 1.02.466 1.086 1.46 1.78 2.546 2.013.621-.854.782-2.018.316-3.105-.155-.311-.3-.617-.534-.85a1.364 1.364 0 0 0-1.188-.485zm-3.809 3.735c-1.224.063-1.77 1.602-.752 2.402.31.233.612.403.922.559 1.087.466 2.344.306 3.275-.316-.233-1.009-1.023-1.936-2.11-2.402-.388-.155-.703-.243-1.092-.243-.087-.009-.161-.004-.243 0zm11.961 4.708a3.551 3.551 0 0 0-2.013.582c.233 1.01 1.023 1.936 2.11 2.401.389.156.705.244 1.093.244 1.397.077 2.08-1.65.994-2.427-.31-.233-.611-.379-.922-.534a3.354 3.354 0 0 0-1.262-.266zm-10.603.072a3.376 3.376 0 0 0-1.261.267c-.389.155-.69.325-.923.558-1.009.854-.33 2.48 1.068 2.402.388 0 .782-.087 1.092-.243 1.087-.465 1.859-1.392 2.014-2.401a3.474 3.474 0 0 0-1.99-.582zm3.931 2.378c-1.087.233-2.009.927-2.475 2.014-.155.31-.243.684-.243.995-.077 1.32 1.724 2.028 2.5 1.02.233-.312.378-.613.534-.923.466-1.01.306-2.174-.316-3.106zm2.887.073c-.621.854-.781 2.019-.315 3.106.155.31.3.615.534.848.854.932 2.65.243 2.572-.921 0-.31-.088-.71-.243-1.02-.466-1.087-1.46-1.78-2.547-2.013z" fill="#4caf50" stroke-width=".776"/></g></symbol><symbol id="cuda" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style>.bust0{fill:#76b900}</style><title>NVIDIA-Logo</title><path id="buEye_Mark" class="bust0" d="M76.362 75.199V64.116c1.095-.068 2.19-.137 3.284-.137 30.377-.958 50.286 26.135 50.286 26.135s-21.483 29.83-44.539 29.83c-3.079 0-6.089-.48-8.962-1.438v-33.66c11.836 1.436 14.23 6.636 21.277 18.471l15.804-13.273s-11.562-15.12-30.992-15.12c-2.053-.068-4.105.069-6.158.274m0-36.67v16.556l3.284-.205c42.213-1.437 69.784 34.618 69.784 34.618s-31.608 38.45-64.516 38.45c-2.873 0-5.678-.274-8.483-.753v10.262c2.326.274 4.72.48 7.046.48 30.65 0 52.817-15.668 74.3-34.14 3.558 2.874 18.13 9.784 21.14 12.794-20.388 17.104-67.937 30.856-94.893 30.856-2.6 0-5.062-.137-7.525-.41v14.436h116.44V38.532zm0 79.977v8.757C48.038 122.2 40.17 92.712 40.17 92.712s13.615-15.05 36.192-17.514v9.579h-.068c-11.836-1.437-21.14 9.646-21.14 9.646s5.268 18.678 21.209 24.082M26.077 91.481S42.839 66.714 76.43 64.115v-9.03C39.213 58.094 7.057 89.565 7.057 89.565s18.199 52.68 69.305 57.47v-9.579c-37.492-4.652-50.286-45.975-50.286-45.975z" fill="#8bc34a" stroke-width=".684"/></symbol><symbol viewBox="0 0 24 24" id="dart" xmlns="http://www.w3.org/2000/svg"><title>Dart</title><path d="M12.486 1.385a.978.978 0 0 0-.682.281l-.01.007-6.387 3.692 6.371 6.372v.004l7.659 7.659 1.46-2.63-5.265-12.64-2.456-2.457a.972.972 0 0 0-.69-.288z" fill="#00ca94"/><path d="M5.422 5.35L1.73 11.733l-.007.01a.967.967 0 0 0 .006 1.371l3.059 3.061 11.963 4.706 2.704-1.502-.073-.073-.018.002-7.5-7.512h-.01L5.423 5.35z" fill="#1565c0"/><path d="M5.405 5.353l6.518 6.525h.01l7.502 7.51 2.855-.544.005-8.449-3.016-2.955c-.66-.647-1.675-1.064-2.695-1.202l.002-.032-11.181-.853z" fill="#1565c0"/><path d="M5.414 5.361l6.521 6.522v.009l7.506 7.506-.546 2.855h-8.448l-2.954-3.017c-.647-.66-1.064-1.676-1.2-2.696l-.033.003L5.414 5.36z" fill="#00ee94"/></symbol><symbol viewBox="0 0 24 24" id="database" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4M4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4m0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="diff" xmlns="http://www.w3.org/2000/svg"><path d="M3 1c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h2v-2H3V3h11v2h2V3c0-1.11-.89-2-2-2H3m6 6c-1.11 0-2 .89-2 2v2h2V9h2V7H9m4 0v2h1v1h2V7h-3m5 0v2h2v11H9v-2H7v2c0 1.11.89 2 2 2h11c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-2m-4 5v2h-2v2h2c1.11 0 2-.89 2-2v-2h-2m-7 1v3h3v-2H9v-1H7z" fill="#42a5f5"/></symbol><symbol id="docker" viewBox="0 0 41 34.5" xmlns="http://www.w3.org/2000/svg"><style id="bystyle2">.byst0{fill:#fff}.byst1{clip-path:url(#bySVGID_4_)}</style><g id="byg34" transform="translate(.292 1.9)" fill="#0087c9"><g id="byg32"><g id="byg30"><title id="bytitle4">Group 3</title><g id="byg28"><g id="byg26"><g id="byg9"><path id="bySVGID_1_" class="byst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="byg24"><defs id="bydefs12"><path id="bySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><clipPath id="bySVGID_4_"><use xlink:href="#bySVGID_2_" id="byuse14" width="100%" height="100%" overflow="visible"/></clipPath><g class="byst1" clip-path="url(#bySVGID_4_)" id="byg22"><g id="byg20"><g id="byg18"><path id="bySVGID_3_" class="byst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol viewBox="0 0 24 24" id="document" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m9 16v-2H6v2h9m3-4v-2H6v2h12z" fill="#42a5f5"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone_light" xmlns="http://www.w3.org/2000/svg"><g fill="#424242" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol viewBox="0 0 3473 3473" id="editorconfig" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg"><defs id="ccdefs4"><style id="ccstyle2">.ccfil2{fill:#020202}.ccfil0{fill:#e3e3f8}.ccfil5{fill:#efefef}.ccfil6{fill:#faf1f1}.ccfil3{fill:#fdf2f2}.ccfil1{fill:#fdfdfd}.ccfil4{fill:#fef3f3}</style></defs><g id="ccLayer_x0020_1" transform="matrix(.8945 0 0 .8945 138.649 275.985)"><g id="cc_631799120"><g id="ccg11"><path class="ccfil0" d="M967 1895c46-30 84-105 61-158-63 27-60 89-61 158z" id="ccpath7" fill="#e3e3f8"/><path class="ccfil0" d="M1679 2067c50-16 98-72 71-130-39 27-64 64-71 130z" id="ccpath9" fill="#e3e3f8"/></g><g id="ccg21"><path class="ccfil1" d="M280 2895c0 63 16 131 60 155 162 91 730 20 923-23 101-23 183-98 278-139 214-93 369-168 540-293 124-91 321-347 342-500l-169-38c-4 172-43 211-196 251-103 28-304 34-409 16-139-23-202-96-265-179-122-162 27-275-166-286-203 249-561 70-718 45-67 97-224 727-222 871 97-33 158 3 245 37 308 119 39 224-84 193-84-20-110-75-159-110z" id="ccpath13" fill="#fdfdfd"/><path class="ccfil1" d="M683 1458c125 24 236 76 342 129 173 86 204 74 220 194 2 22-2 34 61 54 106 33-61-26 223-25 169 1 556 69 681 148 52 33 42 75 218 70-2-207-57-516-138-706-99-230-230-265-497-351-156-50-614-105-756-17-133 83-158 182-282 356-36 51-49 90-72 148z" id="ccpath15" fill="#fdfdfd"/><path class="ccfil1" d="M1784 1883c100 41-5 306-144 242-45-127 62-199 91-256-60-9-231-36-282-17-66 25-81 166-47 232 160 314 867 247 792 3-30-99-58-115-159-149-81-27-162-55-251-55z" id="ccpath17" fill="#fdfdfd"/><path class="ccfil1" d="M527 1848c80 77 261 89 378 95 15-155 28-271 152-262 61 83 29 181-35 244 109-1 172-83 156-202-92-66-371-198-511-217-39 42-135 272-140 342z" id="ccpath19" fill="#fdfdfd"/></g><path class="ccfil2" d="M339 2838c66-6 238 44 252 100-107 13-243 3-252-100zm-59 57c49 35 75 90 159 110 123 31 392-74 84-193-87-34-148-70-245-37-2-144 155-774 222-871 157 25 515 204 718-45 193 11 44 124 166 286 63 83 126 156 265 179 105 18 306 12 409-16 153-40 192-79 196-251l169 38c-21 153-218 409-342 500-171 125-326 200-540 293-95 41-177 116-278 139-193 43-761 114-923 23-44-24-60-92-60-155zm1399-828c7-66 32-103 71-130 27 58-21 114-71 130zm105-184c89 0 170 28 251 55 101 34 129 50 159 149 75 244-632 311-792-3-34-66-19-207 47-232 51-19 222 8 282 17-29 57-136 129-91 256 139 64 244-201 144-242zm-817 12c1-69-2-131 61-158 23 53-15 128-61 158zm-440-47c5-70 101-300 140-342 140 19 419 151 511 217 16 119-47 201-156 202 64-63 96-161 35-244-124-9-137 107-152 262-117-6-298-18-378-95zm-100-80c-37-102-37-261 120-274l-80 223c-21 48-21 37-40 51zm256-310c23-58 36-97 72-148 124-174 149-273 282-356 142-88 600-33 756 17 267 86 398 121 497 351 81 190 136 499 138 706-176 5-166-37-218-70-125-79-512-147-681-148-284-1-117 58-223 25-63-20-59-32-61-54-16-120-47-108-220-194-106-53-217-105-342-129zm1770-49c-19-63 16-59 77-102 35-25 63-51 106-75 161-90 461-105 589 2 52 43 137 127 124 237-27 219-177 339-300 439-125 102-333 207-548 137-18-44-4-323-25-426-19-92-9-102 44-157 156-162 494-280 686-141 81 60 58 83 100 129 52-56-45-244-403-232-243 8-348 198-450 189zM997 840c5-139 133-427 261-527 155-120 317-233 555-98 59 33 56 50 62 132 5 79-2 108-22 172-158 510-290 217-796 338 19-166 163-314 243-391 137-133 236-219 442-191 57 95 63 155-6 266-92 148-115 139-101 240 72-18 94-88 127-158 201-420-91-471-270-394-120 51-334 287-404 429-14 28-29 64-42 95zm792 21c21-125 145-156 145-541 0-166-204-315-471-204-229 94-264 166-386 350-115 174-111 365-210 526-29 46-55 62-87 108-23 34-40 77-63 117-47 77-95 133-133 225-120 3-221 5-233 129-16 170 64 212 64 276-1 69-281 765-203 1180 22 114 97 115 217 129 289 35 664 23 923-81l470-225c119-67 319-194 408-287 63-65 96-120 150-197 74-108 76-106 92-253 98 18 281 61 342 114-7 69-41 36-41 98 39 1 104-48 120-102-41-60-84-50-143-98 47-37 132-54 197-81 140-58 379-234 438-394 47-129 12-344-64-428-80-88-266-133-418-133-181 0-368 130-514 186-56-49-60-105-101-159-47-64-353-224-499-255z" id="ccpath23" fill="#020202"/><path class="ccfil3" d="M2453 1409c102 9 207-181 450-189 358-12 455 176 403 232-42-46-19-69-100-129-192-139-530-21-686 141-53 55-63 65-44 157 21 103 7 382 25 426 215 70 423-35 548-137 123-100 273-220 300-439 13-110-72-194-124-237-128-107-428-92-589-2-43 24-71 50-106 75-61 43-96 39-77 102z" id="ccpath25" fill="#fdf2f2"/><path class="ccfil4" d="M997 840l49-87c13-31 28-67 42-95 70-142 284-378 404-429 179-77 471-26 270 394-33 70-55 140-127 158-14-101 9-92 101-240 69-111 63-171 6-266-206-28-305 58-442 191-80 77-224 225-243 391 506-121 638 172 796-338 20-64 27-93 22-172-6-82-3-99-62-132-238-135-400-22-555 98-128 100-256 388-261 527z" id="ccpath27" fill="#fef3f3"/><path class="ccfil5" d="M427 1768c19-14 19-3 40-51l80-223c-157 13-157 172-120 274z" id="ccpath29" fill="#efefef"/><path class="ccfil6" d="M591 2938c-14-56-186-106-252-100 9 103 145 113 252 100z" id="ccpath31" fill="#faf1f1"/></g></g></symbol><symbol viewBox="0 0 24 24" id="elixir" xmlns="http://www.w3.org/2000/svg"><path d="M12.431 22.383c-3.86 0-6.99-3.64-6.99-8.13 0-3.678 2.774-8.172 4.916-10.91 1.014-1.295 2.931-2.321 2.931-2.321s-.982 5.238 1.683 7.318c2.365 1.847 4.105 4.25 4.105 6.363 0 4.232-2.784 7.68-6.645 7.68z" fill="#9575cd" stroke-width="1.256"/></symbol><symbol viewBox="0 0 323.00001 322.99999" id="elm" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.8053 0 0 .8053 30.106 31.524)"><path fill="#f0ad00" d="M160.8 153.865l68.028-68.03H92.77z"/><path fill="#7fd13b" d="M160.983 5.098H12.033l68.524 68.525H229.51z"/><path fill="#7fd13b" stroke-width=".974" d="M243.906 88.021l74.136 74.137-74.474 74.475-74.137-74.137z"/><path fill="#60b5cc" d="M318.2 145.045V5.098H178.252z"/><path fill="#5a6378" d="M152.164 162.499L3.4 13.733v297.533z"/><path fill="#f0ad00" d="M252.205 245.27l65.995 65.996v-131.99z"/><path fill="#60b5cc" d="M160.8 171.134L12.034 319.899h297.53z"/></g></symbol><symbol viewBox="0 0 24 24" id="email" xmlns="http://www.w3.org/2000/svg"><path d="M20 8l-8 5-8-5V6l8 5 8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 30 30" id="erlang" xmlns="http://www.w3.org/2000/svg"><path style="line-height:1.25;-inkscape-font-specification:'Wide Latin'" d="M5.217 4.367c-.048.052-.097.1-.144.153C2.697 7.182 1.51 10.798 1.51 15.366c0 4.418 1.156 7.862 3.46 10.34h19.414c2.553-1.152 4.127-3.43 4.127-3.43l-3.147-2.52-1.454 1.381c-.866.773-.845.931-2.314 1.78-1.496.674-3.04.966-4.634.966-2.516 0-4.423-.909-5.723-2.059-1.286-1.15-1.985-4.511-2.097-6.68l17.458.067-.182-1.472s-.847-7.129-2.542-9.372zm8.76.846c1.565 0 3.22.535 3.96 1.471.742.937.932 1.667.974 3.524H9.12c.111-1.955.436-2.81 1.372-3.697.937-.888 2.03-1.298 3.484-1.298z" font-weight="400" font-size="48" font-family="Wide Latin" letter-spacing="0" word-spacing="0" fill="#f44336" stroke-width=".97"/></symbol><symbol viewBox="0 0 299.99999 300.00001" id="eslint" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2.88 18.438) scale(1.0344)"><path d="M97.021 99.016l48.432-27.962c1.212-.7 2.706-.7 3.918 0l48.433 27.962a3.92 3.92 0 0 1 1.959 3.393v55.924a3.924 3.924 0 0 1-1.959 3.394l-48.433 27.962c-1.212.7-2.706.7-3.918 0l-48.432-27.962a3.92 3.92 0 0 1-1.959-3.394v-55.924a3.922 3.922 0 0 1 1.959-3.393" fill="#7986cb"/><path d="M273.34 124.49L215.473 23.82c-2.102-3.64-5.985-6.325-10.188-6.325H89.545c-4.204 0-8.088 2.685-10.19 6.325L21.488 124.27c-2.102 3.641-2.102 8.236 0 11.877l57.867 99.847c2.102 3.64 5.986 5.501 10.19 5.501h115.74c4.203 0 8.087-1.805 10.188-5.446l57.867-100.01c2.104-3.639 2.104-7.907.001-11.547m-47.917 48.41c0 1.48-.891 2.849-2.174 3.59l-73.71 42.527a4.194 4.194 0 0 1-4.17 0l-73.767-42.527c-1.282-.741-2.179-2.109-2.179-3.59V87.847c0-1.481.884-2.849 2.167-3.59l73.707-42.527a4.185 4.185 0 0 1 4.168 0l73.772 42.527c1.283.741 2.186 2.109 2.186 3.59z" fill="#3f51b5"/></g></symbol><symbol viewBox="0 0 24 24" id="exe" xmlns="http://www.w3.org/2000/svg"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z" fill="#e64a19"/></symbol><symbol viewBox="0 0 24 24" id="favicon" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.45 4.73L5.82 21 12 17.27z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="file" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 400 400" id="firebase" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 103)"><path d="M72.55 208.77l44.456-292.29 56.209 90.445L195.49-37.57 330.6 209.28z" fill="#ffa712"/><path d="M195.7 276.73l134.9-67.45-46.5-224.83L72.55 208.77z" fill="#fcca3f"/><path d="M173.22 6.932L72.56 208.772l136.35-144.58z" fill="#f6820c"/></g></symbol><symbol viewBox="0 0 24 24" id="flash" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cma"><stop offset="0" stop-color="#d92f3c"/><stop offset="1" stop-color="#791223"/></linearGradient><linearGradient xlink:href="#cma" id="cmb" x1="2.373" y1="12.027" x2="21.86" y2="12.027" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.09 -24.144)"/></defs><rect width="19.487" height="19.487" x="2.283" y="-21.86" transform="rotate(90)" ry="0" fill="url(#cmb)"/><path style="line-height:125%" d="M16.802 5.768l-.013.002a6.43 6.43 0 0 0-1.182.192 5.062 5.062 0 0 0-1.494.718c-.428.323-.817.72-1.17 1.191-.34.48-.682 1.032-1.022 1.66-.12.228-.233.424-.35.636v.002h-.004l-1.34 2.394-.005-.002c-.238.443-.461.847-.665 1.198a4.358 4.358 0 0 1-.716.94 2.79 2.79 0 0 1-.907.594c-.072.027-.161.042-.242.063h-.989v2.414h.989v-.002a6.427 6.427 0 0 0 1.185-.192 5.062 5.062 0 0 0 1.494-.718 5.94 5.94 0 0 0 1.171-1.191c.34-.48.681-1.033 1.021-1.66.12-.228.235-.425.353-.637l.006.002.003-.005.037-.066h2.53v.002h1.124v-2.408h-.33v-.001h-1.98c.22-.407.432-.789.621-1.115.214-.37.452-.682.717-.94a2.79 2.79 0 0 1 .906-.594c.07-.027.16-.041.239-.061h.992V8.18h-.002V5.77h-.977v-.002z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol class="cnflow-logo" viewBox="0 0 299.99999 300" id="flow" xmlns="http://www.w3.org/2000/svg"><title>Flow logo</title><path d="M38.75 33.427l77.461 77.47H54.436l61.145 61.16H38.437l93.462 93.478v-77.158l.01-.01v-77.47h-.01V66.982h46.691l20.394 20.393H153.57v76.531h22.05l24.474 24.473h-15.806l-.01-.01v.01h-31.665l-.01-.01v.01h-.313l.313.313v77.148h109.149l-39.2-39.2v-15.806l8.465 8.466v-77.37h-15.682l.017-38.191 30.09 30.086V56.362h-64.874l-22.94-22.934H113.71z" fill="#fbc02d" fill-opacity=".976" stroke-width=".955" class="cnflow-logo-mark"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="coa" x1="-388.15%" x2="237.68%" y1="-144.18%" y2="430.41%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cob" x1="72.945%" x2="-97.052%" y1="84.424%" y2="-147.7%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="coc" x1="-283.88%" x2="287.54%" y1="-693.6%" y2="101.71%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cod" x1="-821.19%" x2="101.99%" y1="-469.05%" y2="288.24%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coe" x1="-140.36%" x2="419.01%" y1="-230.93%" y2="261.98%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cof" x1="191.08%" x2="20.358%" y1="253.95%" y2="20.403%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cog" x1="-388.09%" x2="237.67%" y1="-173.85%" y2="518.99%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#coa"/><linearGradient id="coj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cob"/><linearGradient id="cok" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#coc"/><linearGradient id="col" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cod"/><linearGradient id="com" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#coe"/><linearGradient id="con" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cof"/><linearGradient id="coo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cog"/><linearGradient id="cop" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#coh"/><linearGradient id="coh" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#coi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#coj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cok)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#col)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#com)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#con)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#coo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cop)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia-open" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cpi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpa"/><linearGradient id="cpa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cpb"/><linearGradient id="cpb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#cpc"/><linearGradient id="cpc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cpd"/><linearGradient id="cpd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#cpe"/><linearGradient id="cpe" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cpf"/><linearGradient id="cpf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpg"/><linearGradient id="cpg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpp" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#cph"/><linearGradient id="cph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#cpi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#cpj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cpk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#cpl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#cpm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#cpn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#cpo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cpp)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#cddc39" fill-rule="nonzero"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#cddc39"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00acc1" fill-rule="nonzero"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00acc1"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#e57373" fill-rule="nonzero"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" stroke-width=".644"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#e57373"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" fill-rule="evenodd" stroke-width=".644"/></symbol><symbol id="folder-docker" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs id="cydefs10"><path id="cySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><path id="cypath2" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><style id="cystyle2">.cyst0{fill:#fff}.cyst1{clip-path:url(#cySVGID_4_)}</style><g id="cyg34" transform="translate(8.319 9.626) scale(.39491)" fill="#b3e5fc"><g id="cyg32"><g id="cyg30"><title id="cytitle4">Group 3</title><g id="cyg28"><g id="cyg26"><g id="cyg9"><path id="cySVGID_1_" class="cyst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="cyg24"><clipPath id="cySVGID_4_"><use id="cyuse14" width="100%" height="100%" xlink:href="#cySVGID_2_"/></clipPath><g id="cyg22" class="cyst1" clip-path="url(#cySVGID_4_)"><g id="cyg20"><g id="cyg18"><path id="cySVGID_3_" class="cyst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docker-open" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="cza"><use width="100%" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#SVGID_2_"/></clipPath></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><g transform="matrix(.3949 0 0 .39489 8.319 9.626)" fill="#b3e5fc"><title>Group 3</title><path class="czst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/><g class="czst1" clip-path="url(#cza)"><path class="czst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#01579b" fill-rule="nonzero"/><style>.dcst0{fill:#1173b6}.st1{fill:#585d67}</style><path class="dcst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#01579b"/><path class="ddst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M21.132 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.217 1.217m.608-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M21.133 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.216 1.217m.609-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ffca28" fill-rule="nonzero"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.411 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill-rule="nonzero" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ffca28"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.412 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#c0ca33" fill-rule="nonzero"/><path d="M17.39 12.544a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#c0ca33"/><path d="M17.391 12.543a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.036 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.968 4.968 0 0 1-2.679 2.203m-.155-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.1-1.238h2.894c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399h-1.806a4.902 4.902 0 0 1 2.672-2.202c-.37.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.049.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.94 4.94 0 0 1 2.679 2.202m-4.281-3.712a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.037 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.967 4.967 0 0 1-2.68 2.203m-.154-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.099-1.238h2.895c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399H13.06a4.902 4.902 0 0 1 2.672-2.202c-.371.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.05.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.941 4.941 0 0 1 2.679 2.202M17.34 9.322a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M16.473 13.927c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a17.015 17.015 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359a5.558 5.558 0 0 0-.203.604c.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.569-1.053c-.21-.372-.435-.702-.639-1.032-.38-.022-.78-.022-1.2-.022-.422 0-.823 0-1.202.022-.204.33-.428.66-.639 1.032l-.569 1.053.57 1.054c.21.372.434.702.638 1.032.38.021.78.021 1.201.021.421 0 .822 0 1.201-.02.204-.331.428-.661.639-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.245-9.284c-.436-.267-1.405.14-2.522 1.194.366.414.724.864 1.06 1.334.577.057 1.146.14 1.686.253.359-1.503.225-2.535-.224-2.78m-.492 4.03l.204.358c.077-.203.154-.407.203-.604-.19-.042-.4-.077-.618-.112l.211.358m1.018-4.95c1.033.589 1.145 2.141.71 3.953 1.784.527 3.069 1.398 3.069 2.584 0 1.187-1.285 2.058-3.07 2.585.436 1.812.324 3.364-.709 3.954-1.025.59-2.423-.085-3.77-1.37-1.35 1.285-2.747 1.96-3.78 1.37-1.025-.59-1.137-2.142-.702-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.203 6.538c.239.527.45 1.054.625 1.588 1.475-.443 2.303-1.075 2.303-1.588 0-.512-.828-1.144-2.303-1.587a15.81 15.81 0 0 1-.625 1.587m-7.136 0a15.806 15.806 0 0 1-.625-1.587c-1.474.443-2.303 1.075-2.303 1.587 0 .513.829 1.145 2.303 1.588.176-.534.387-1.06.625-1.588m6.321 1.588l-.21.358c.217-.035.428-.07.617-.113-.049-.196-.126-.4-.203-.604l-.204.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.522 1.194.45-.246.583-1.278.224-2.781-.54.112-1.11.196-1.685.253-.337.47-.695.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.113.049.196.126.4.203.604l.204-.359m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M16.473 13.928c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a16.997 16.997 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359c-.077.204-.154.408-.203.604.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.57-1.053c-.21-.372-.434-.702-.638-1.032-.38-.022-.78-.022-1.201-.022-.421 0-.822 0-1.2.022-.205.33-.43.66-.64 1.032l-.569 1.053.569 1.054c.21.372.435.702.64 1.032.378.021.779.021 1.2.021.421 0 .822 0 1.2-.02.205-.33.43-.661.64-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.244-9.284c-.435-.267-1.404.14-2.52 1.194.364.414.723.864 1.06 1.334.575.057 1.144.14 1.685.253.358-1.503.225-2.535-.225-2.78m-.491 4.03l.203.358c.078-.203.155-.407.204-.604-.19-.042-.4-.077-.618-.112l.21.358m1.02-4.95c1.032.589 1.144 2.141.708 3.953 1.784.527 3.07 1.398 3.07 2.584 0 1.187-1.286 2.058-3.07 2.585.436 1.812.323 3.364-.709 3.954-1.025.59-2.423-.085-3.771-1.37-1.348 1.285-2.746 1.96-3.778 1.37-1.026-.59-1.138-2.142-.703-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.204 6.538c.24.527.45 1.054.625 1.588 1.475-.443 2.304-1.075 2.304-1.588 0-.512-.829-1.144-2.304-1.587a15.81 15.81 0 0 1-.625 1.587m-7.135 0a15.808 15.808 0 0 1-.625-1.587c-1.475.443-2.303 1.075-2.303 1.587 0 .513.828 1.145 2.303 1.588.176-.534.386-1.06.625-1.588m6.32 1.588l-.21.358c.218-.035.428-.07.618-.113a5.56 5.56 0 0 0-.204-.604l-.203.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.521 1.194.45-.246.583-1.278.225-2.781-.54.112-1.11.196-1.685.253-.338.47-.696.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.112.049.197.126.4.203.604l.204-.358m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#fbc02d" fill-rule="nonzero"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.434h-1.217v8.518a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#fbc02d"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.433h-1.217v8.519a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#4caf50" fill-rule="nonzero"/><g fill="#c8e6c9" transform="translate(2.065 -.225) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#4caf50"/><g fill="#c8e6c9" fill-rule="evenodd" transform="translate(2.064 -.224) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1e88e5" fill-rule="nonzero"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1e88e5"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.811 8.52l-5.988 5.506-3.346-2.522-1.383.805 3.298 3.03-3.298 3.032 1.383.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.622v6.396l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.81 8.52l-5.988 5.506-3.346-2.522-1.384.805 3.3 3.03-3.3 3.032 1.384.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.621v6.397l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><g transform="translate(8.459 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688"/><g transform="translate(8.458 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#03a9f4" fill-rule="nonzero"/><g transform="translate(9.192 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#03a9f4"/><g transform="translate(9.193 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol viewBox="0 0 24 24" id="font" xmlns="http://www.w3.org/2000/svg"><path d="M9.62 12L12 5.67 14.37 12M11 3L5.5 17h2.25l1.12-3h6.25l1.13 3h2.25L13 3h-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 500 500" id="fsharp" xmlns="http://www.w3.org/2000/svg"><path d="M235.906 36.66L21.963 250.601l213.943 213.943v-84.36L106.209 250.487l129.697-129.696z" fill="#378bba" stroke-width="14.706"/><path d="M235.906 156.614l-93.622 93.62 93.622 93.622z" fill="#378bba" stroke-width="15.006"/><path d="M263.417 36.64L477.36 250.583 263.417 464.526v-84.36l129.696-129.697-129.696-129.696z" fill="#30b9db" stroke-width="14.706"/></symbol><symbol viewBox="0 0 152.99 160.01" id="fusebox" xmlns="http://www.w3.org/2000/svg"><defs id="fkdefs4"><style id="fkstyle2">.fkcls-1{fill:#fff}.fkcls-2{fill:#515151}.fkcls-3{fill:#1d79bf}.fkcls-4{fill:#383838}</style></defs><title id="fktitle6">Asset 3</title><g id="fkLayer_2" data-name="Layer 2" transform="matrix(.87285 0 0 .87285 10.17 10.175)"><g id="fkFuse_Box" data-name="Fuse Box"><g id="fkLOGO"><path class="fkcls-1" id="fkpolygon8" fill="#fff" d="M76.56 2.19l74.22 24.93-7.7 87.77-65.41 42.66-69.79-43.93-5.7-86.13z"/><path class="fkcls-2" d="M77.69 160L5.87 114.81 0 26 76.55 0 153 25.67l-7.94 90.4zM9.88 112.43l67.77 42.66 63.45-41.39 7.47-85.13-72-24.18L4.36 28.95z" id="fkpath10" fill="#515151"/><path class="fkcls-3" id="fkpolygon12" fill="#1d79bf" d="M76.4 148.8V61.68l66.93-29.82-5.99 78.77z"/><path id="fkF" class="fkcls-4" fill="#383838" d="M76.4 148.8l-60.35-37.39L9.63 31.8 76.4 61.68z"/><path class="fkcls-1" d="M25.58 52.73l.54 15.93 37.35 18.18.12 14.69-37-18.21 1.64 37.1-14.56-9-5.05-80.55 67.79 30.82v15.46z" id="fkpath15" fill="#fff"/><path class="fkcls-1" d="M135.91 90.77c-.08 13.12-6.33 26.59-16.77 33.12l-42.8 27.93V61.71l42.27-18.84c5.16-2.41 9.51-1.43 12.4 3.11 1.9 3 2.89 7.23 2.86 12.21A35.69 35.69 0 0 1 129.34 76c4.29 2 6.66 6.55 6.57 14.77zM123 63.76c0-4.64-2-6.93-4.92-5.45l-29 14.48L89 90l29.44-15.59c2.5-1.32 4.56-5.91 4.56-10.65zM125.15 96c0-5.71-2.42-8.24-6.55-5.93L89 106.64v19.58l29.34-17.46c4.43-2.64 6.79-7.27 6.81-12.76z" id="fkpath17" fill="#fff"/><path id="fkTOP" class="fkcls-4" fill="#383838" d="M76.4 8.82L9.71 31.77l109.77 2.38-84.02 9.21L76.4 61.68l20.76-9.25-27.73-1.37 49.78-8.46 24.12-10.74z"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="git" xmlns="http://www.w3.org/2000/svg"><path d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82z" fill="#e64a19"/></symbol><symbol viewBox="0 0 494 455" id="gitlab" xmlns="http://www.w3.org/2000/svg"><title>logo</title><defs><path id="fma" d="M0 1173.3h2000V0H0v1173.3z"/></defs><g transform="matrix(.88256 0 0 -.88256 -286.767 742.766)" fill="none" fill-rule="evenodd"><mask id="fmb" fill="#fff"><use width="100%" height="100%" xlink:href="#fma"/></mask><g mask="url(#fmb)"><g transform="translate(358.67 358.67)"><path d="M492.532 195.445l-27.559 84.815-54.617 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.619-168.1h-181.37l-54.62 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.617-168.1-27.557-84.815a18.775 18.775 0 0 1 6.82-20.992l238.51-173.29 238.51 173.29a18.777 18.777 0 0 1 6.82 20.992" fill="#fc6d26"/><path d="M247.2 1.16l90.684 279.1h-181.37z" fill="#e24329"/><path d="M247.201 1.16l-90.684 279.09H29.427z" fill="#fc6d26"/><path d="M29.422 280.256L1.862 195.44a18.774 18.774 0 0 1 6.822-20.991L247.194 1.16z" fill="#fca326"/><path d="M29.422 280.26h127.09l-54.619 168.1c-2.81 8.65-15.047 8.65-17.856 0z" fill="#e24329"/><path d="M247.2 1.16l90.684 279.09h127.09z" fill="#fc6d26"/><path d="M464.98 280.256l27.559-84.815a18.774 18.774 0 0 0-6.821-20.991L247.208 1.16z" fill="#fca326"/><path d="M464.97 280.26H337.88l54.619 168.1c2.81 8.65 15.047 8.65 17.856 0z" fill="#e24329"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="go" xmlns="http://www.w3.org/2000/svg"><path d="M10.575 1.695c-2.634 0-4.756 2.453-4.756 5.502v4.6l-.027-.003v4.71c0 3.05 2.123 5.502 4.757 5.502h2.286c2.634 0 4.757-2.453 4.757-5.502v-4.6a5.1 5.1 0 0 0 .026.003v-4.71c0-3.049-2.122-5.502-4.756-5.502h-2.287z" fill="#73cddc"/><rect width="2.289" height="3.335" x="-1.178" y="6.092" ry="1.125" transform="matrix(.4849 -.87457 .85979 .51065 0 0)" fill="#73cddc"/><rect width="2.297" height="3.39" x="10.261" y="-15.076" ry="1.143" transform="matrix(.44646 .8948 -.89204 .45195 0 0)" fill="#73cddc"/><circle cx="9.267" cy="5.13" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><circle cx="14.214" cy="5.116" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><ellipse cx="8.039" cy="5.051" rx=".792" ry=".901" fill="#030d18"/><path d="M11.792 9.556l.763.138a.403.689 0 0 1 .008.138.403.689 0 0 1-.402.69.403.689 0 0 1-.404-.69.403.689 0 0 1 .035-.276z" fill="#fff" stroke="#fff" stroke-width=".155"/><ellipse cx="8.51" cy="5.365" rx=".138" ry=".166" fill="#fff"/><ellipse cx="12.945" cy="5.189" rx=".792" ry=".901" fill="#030d18"/><ellipse cx="13.414" cy="5.446" rx=".138" ry=".166" fill="#fff"/><ellipse cx="-12.982" cy="-3.409" rx=".708" ry="1.026" transform="rotate(-129.403)" fill="#f6d2a1" stroke-width=".4"/><path d="M11.772 9.553l-.757.135a.4.672 0 0 0-.008.135.4.672 0 0 0 .4.672.4.672 0 0 0 .4-.672.4.672 0 0 0-.035-.27z" fill="#fff" stroke="#fff" stroke-width=".153"/><ellipse cx="1.841" cy="-21.563" rx=".707" ry="1.026" transform="scale(1 -1) rotate(50.597)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="-17.281" cy="-21.784" rx=".864" ry="1.27" transform="matrix(.3054 -.95222 -.97065 -.2405 0 0)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="22.885" cy="2.587" rx=".864" ry="1.27" transform="matrix(.22652 .974 .95652 -.29167 0 0)" fill="#f6d2a1" stroke-width=".4"/><path d="M10.708 8.392a.594.594 0 0 0-.594.597v.115c0 .331.264.598.594.598h.386a.973.772 0 0 1 .697-.235.973.772 0 0 1 .698.235h.334c.33 0 .594-.267.594-.598V8.99a.595.595 0 0 0-.594-.597h-2.115z" fill="#f6d2a1" stroke="#657075" stroke-width=".1"/><ellipse cx="11.734" cy="8.203" rx="1.208" ry=".68" fill="#030d18" stroke="#fff" stroke-width=".162"/></symbol><symbol viewBox="0 0 24 24" id="gradle" xmlns="http://www.w3.org/2000/svg"><path d="M21.718 5.503c-.731-1.315-2.04-1.708-2.963-1.727-1.133-.023-2.065.605-1.888 1.017.037.088.25.55.38.741.19.275.527.064.646 0 .353-.187.73-.248 1.16-.198.409.048.954.3 1.319 1.001.859 1.652-1.794 5.05-5.114 2.697-3.32-2.353-6.548-1.574-8.01-1.1-1.462.475-2.135.952-1.556 2.055.785 1.498.524 1.038 1.285 2.28 1.21 1.97 3.856-.908 3.856-.908-1.972 2.906-3.662 2.204-4.31 1.188a15.864 15.864 0 0 1-1.038-1.97c-4.993 1.76-3.642 9.534-3.642 9.534h2.48c.632-2.862 2.892-2.757 3.28 0h1.892c1.673-5.59 5.914 0 5.914 0h2.466c-.69-3.812 1.388-5.01 2.697-7.246 1.31-2.235 2.551-4.969 1.146-7.364zm-6.362 7.362c-1.304-.426-.837-1.723-.837-1.723s1.139.368 2.68.87c-.09.403-.856 1.175-1.843.853z" fill="#0097a7" stroke-width=".47"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 300 300" id="graphcool" xmlns="http://www.w3.org/2000/svg"><path d="M246.886 107.727c-12.237-6.892-27.616 2.1-30.081 3.646l-52.834 29.965c-7.8-6.196-18.914-5.933-26.412.625-7.499 6.558-9.24 17.537-4.14 26.094 5.102 8.556 15.588 12.246 24.923 8.768 9.335-3.478 14.852-13.129 13.111-22.937l52.688-29.9.321-.196c3.464-2.188 11.5-5.462 15.256-3.34 2.706 1.524 4.252 6.629 4.376 14.148h-.066v66.092a17.313 17.313 0 0 1-8.635 14.95l-75.739 43.755a17.312 17.312 0 0 1-17.261 0l-75.74-43.756a17.312 17.312 0 0 1-8.634-14.95V113.22c.01-6.165 3.3-11.86 8.634-14.95l68.549-39.562c6.522 7.482 17.451 9.25 26 4.206s12.283-15.468 8.886-24.794c-3.397-9.327-12.962-14.904-22.751-13.27-9.79 1.636-17.022 10.02-17.204 19.944L59.397 85.632a31.932 31.932 0 0 0-15.978 27.588v87.454a31.933 31.933 0 0 0 15.927 27.602l75.74 43.755a31.934 31.934 0 0 0 31.846 0l75.74-43.755a31.933 31.933 0 0 0 15.927-27.58V137.12h.05c.373-14.913-3.616-24.794-11.762-29.389z" fill="#27ae60" stroke="#27ae60" stroke-width="7.883622079999999"/></symbol><symbol viewBox="0 0 400 400" id="graphql" xmlns="http://www.w3.org/2000/svg"><path d="M67.008 293.022l-13.143-7.588L200.282 31.839l13.143 7.588z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M50.855 265.174H343.69v15.177H50.855z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M203.122 358.269L56.649 273.7l7.589-13.143 146.472 84.568zm127.24-220.407L183.889 53.293l7.589-13.143 146.472 84.568z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M64.278 137.803l-7.588-13.142 146.472-84.568 7.588 13.143z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M327.661 293.025L181.244 39.43l13.143-7.589 146.417 253.596zM62.466 114.597h15.176v169.136H62.466zm254.528 0h15.176v169.136h-15.176z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M200.538 351.845l-6.628-11.481L321.3 266.812l6.629 11.48z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M352.284 288.67c-8.777 15.268-28.342 20.48-43.61 11.703-15.268-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.36 8.869 20.57 28.342 11.703 43.61M97.574 141.567c-8.778 15.268-28.343 20.48-43.61 11.703-15.269-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.268 8.869 20.479 28.342 11.702 43.61M42.353 288.67c-8.777-15.268-3.566-34.741 11.702-43.61 15.268-8.776 34.741-3.565 43.61 11.703 8.776 15.268 3.565 34.741-11.703 43.61-15.36 8.776-34.833 3.565-43.61-11.703m254.71-147.103c-8.776-15.268-3.565-34.741 11.703-43.61 15.268-8.776 34.742-3.565 43.61 11.703 8.777 15.268 3.566 34.741-11.702 43.61-15.268 8.776-34.833 3.565-43.61-11.703m-99.745 236.608c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907c0 17.554-14.262 31.907-31.907 31.907m0-294.206c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907-14.262 31.907-31.907 31.907" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/></symbol><symbol viewBox="0 0 24 24" id="groovy" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.982a10.119 10.119 0 0 0-10.12 10.12A10.119 10.119 0 0 0 12 22.22 10.119 10.119 0 0 0 22.12 12.1 10.119 10.119 0 0 0 12 1.983zm1.254 2.422c.91 0 1.647.261 2.213.78.571.518.857 1.188.857 2.013 0 .889-.319 1.673-.959 2.35-.64.677-1.376 1.015-2.207 1.015-.486 0-.89-.119-1.213-.357-.317-.238-.476-.532-.476-.88 0-.212.06-.4.181-.563.127-.164.274-.246.438-.246.159 0 .238.092.238.277 0 .164.06.29.182.38.121.09.261.136.42.136.423 0 .828-.29 1.215-.866.391-.582.587-1.202.587-1.863 0-.465-.151-.844-.453-1.135-.301-.296-.69-.445-1.166-.445-.714 0-1.406.318-2.078.953-.666.635-1.211 1.47-1.635 2.506-.417 1.031-.627 2.014-.627 2.945 0 .857.185 1.54.555 2.047.37.503.863.754 1.477.754 1.037 0 2.027-.734 2.974-2.2l1.493-.212c.185-.026.277.018.277.135 0 .053-.072.28-.215.681-.143.402-.337 1.074-.586 2.016.82-.476 1.455-1.003 1.904-1.58v.914c-.36.418-1.046.888-2.062 1.412-.212 1.407-.682 2.493-1.406 3.26-.725.772-1.54 1.16-2.444 1.16-.433 0-.775-.102-1.023-.303-.243-.2-.365-.477-.365-.832 0-.984.955-1.94 2.865-2.865.2-.714.395-1.356.586-1.928-.333.482-.817.907-1.451 1.278-.635.37-1.225.554-1.77.554-.889 0-1.628-.383-2.22-1.15-.588-.772-.881-1.748-.881-2.928 0-1.243.333-2.42 1-3.531a7.747 7.747 0 0 1 2.625-2.674c1.084-.672 2.134-1.008 3.15-1.008zM12.03 16.592c-1.375.687-2.062 1.365-2.062 2.031 0 .354.169.533.508.533.666 0 1.184-.856 1.554-2.564z" fill="#26c6da"/></symbol><symbol viewBox="0 0 24 24" id="gulp" xmlns="http://www.w3.org/2000/svg"><path d="M8.37 15.94a596.238 596.238 0 0 1-.482-4.982c.002-.042-.225-.077-.505-.077h-.508V8.95h3.966V5.198l1.871-1.124c1.14-.685 1.978-1.125 2.144-1.125.4 0 .866.506.866.939 0 .19-.057.422-.127.517-.07.095-.722.53-1.45.966l-1.321.792-.029 1.393-.028 1.393h3.972v1.932h-.98l-.495 4.983-.495 4.983H8.854l-.485-4.906z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="h" xmlns="http://www.w3.org/2000/svg"><path d="M16.745 19.818h-3.007v-5.882q0-2.381-1.736-2.381-.869 0-1.438.663-.56.662-.56 1.718v5.882H6.988V4.533h3.016v6.508h.037q1.186-1.802 3.193-1.802 3.511 0 3.511 4.239z" stroke-width=".478" fill="#0277bd"/></symbol><symbol viewBox="0 0 253.6 253.6" id="hack" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-29.243 -29.515) scale(1.2301)"><path fill="#607d8b" d="M69.496 159.551v52.576l51.77-52.576zM123.507 41.523l-54.01 52.755v55.084l54.01-54.009z"/><path fill="#eceff1" d="M130.023 95.663v51.501l52.128-51.5z"/><path fill="#607d8b" d="M185.465 101.867l-55.442 55.174v55.083l55.442-55.262z"/><path fill="#ffa000" d="M73.068 154.283l50.427.09v-50.248z"/></g></symbol><symbol viewBox="0 0 300 300.00001" id="haml" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 165.6)"><path d="M78.42-132.307c-12.047-.302-26.924 5.998-26.924 5.998l49.195 99.791L74.605 85.005c23.81 20.134 50.07 10.504 50.07 10.504L136.76 9.212c1.526 1.446 3.146 2.77 4.777 3.995 5.244 3.714 10.925 6.553 16.606 8.738 5.68 2.185 11.583 3.933 17.482 5.244 3.933.874 7.645 1.53 11.578 1.967-1.748 3.933-2.84 8.083-2.621 12.672 0 .437.22.873.656 1.092h.217c4.152 2.185 8.521 3.934 13.328 5.027 4.589.874 9.615 1.312 14.422.656 5.026-.655 10.051-2.623 13.984-5.9 3.933-3.278 6.774-7.648 8.522-12.237l.219-.218v-.217l.656-5.899v-.22c2.185-1.311 4.37-2.621 6.555-4.37 2.622-2.184 5.025-4.589 6.773-7.648 1.748-3.059 2.84-6.774 2.621-10.488-.218-3.496-1.53-6.99-3.06-10.049-1.53-3.059-3.495-5.901-5.68-8.523-4.37-5.026-9.614-9.176-15.295-12.454-5.462-3.496-11.581-6.338-17.7-8.304l-2.404-.656-1.962-.655c-1.311-.437-2.406-1.092-3.498-1.53-2.185-1.31-3.717-2.622-4.809-4.37-2.185-3.278-2.403-8.301-1.31-13.545.218-1.311.656-2.623 1.093-3.934a96.064 96.064 0 0 0 1.31-4.152c.314-1.412.51-2.829.598-4.402l29.203-25.553c-2.275-8.404-27.488-17.158-27.488-17.158l-74.931 63.726-43.243-81.584c-1.553-.35-3.218-.527-4.94-.57zm107.682 73.14c-.449 2.336-.647 4.795-.647 7.258.219 3.715 1.311 7.87 3.715 11.366 2.403 3.496 5.68 6.117 8.957 7.646a29.663 29.663 0 0 0 5.027 1.967l2.623.654 2.184.438c5.68 1.53 11.142 3.714 16.168 6.554 5.025 2.84 9.833 6.337 13.766 10.27s6.992 8.959 7.43 13.984c.218 3.496-.22 6.118-1.313 8.303-1.093 2.404-2.84 4.588-4.807 6.555-.874.874-1.966 1.747-2.84 2.402a27.11 27.11 0 0 0-.654-5.898c-.219-1.093-.438-1.966-.875-3.059-.437-.874-.872-1.966-1.965-2.621-.218 0-.44-.001-.44.217-1.31 3.277-3.494 6.12-5.898 8.086-2.403 1.966-5.462 2.84-8.521 3.058-3.06.219-6.338-.436-9.616-1.31-3.277-.874-6.552-1.968-9.83-3.06l-.439-.22c-.656-.218-1.526.002-1.963.44-1.748 2.185-3.06 4.149-4.59 6.334a58.435 58.435 0 0 0-2.84 5.027c-3.933-1.53-7.649-2.841-11.582-4.37-5.462-2.186-10.925-4.37-15.95-6.991-5.245-2.404-10.268-5.246-14.638-8.524-3.15-2.363-6.062-4.845-8.185-7.681l2.404-17.172z" fill="#f4511e" stroke-width="0" stroke-linejoin="round"/></g></symbol><symbol viewBox="0 0 24 24" id="handlebars" xmlns="http://www.w3.org/2000/svg"><path d="M8.55 10.32c-2.753 0-4.202 3.48-5.793 3.48-.98 0-1.126-.677-1.126-.915 0-.332.236-.706.564-.706.59 0 .414.77.414.77s.798-.555.272-1.298c-.42-.595-1.31-.623-1.92-.17-.617.458-1.057 1.146-.853 2.287.1.551.468 1.35 1.233 1.805.764.455 1.925.566 2.335.566 2.194 0 4.342-1.633 6.639-2.322a5.513 5.513 0 0 1 1.497-.222 6.19 6.19 0 0 1 1.92.226c2.296.689 4.444 2.323 6.638 2.323.41 0 1.57-.11 2.335-.566.765-.455 1.132-1.256 1.231-1.807.204-1.14-.235-1.829-.853-2.287-.61-.453-1.497-.423-1.918.172-.526.743.27 1.297.27 1.297s-.176-.77.414-.77c.329 0 .565.373.565.705 0 .238-.147.914-1.126.914-1.592 0-3.04-3.478-5.794-3.478-2.565 0-3.076 1.177-3.462 1.718-.004.005-.005.011-.008.016-.005-.006-.007-.013-.012-.02-.386-.54-.896-1.717-3.461-1.717z" fill="#ff7043" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 300.00001 300" id="haskell" xmlns="http://www.w3.org/2000/svg"><g stroke-width="2.422"><path d="M23.928 240.5l59.94-89.852-59.94-89.855h44.955l59.94 89.855-59.94 89.852z" fill="#ef5350"/><path d="M83.869 240.5l59.94-89.852-59.94-89.855h44.955l119.88 179.71h-44.95l-37.46-56.156-37.468 56.156z" fill="#ffa726"/><path d="M228.72 188.08l-19.98-29.953h69.93v29.956h-49.95zm-29.97-44.924l-19.98-29.953h99.901v29.953z" fill="#ffee58"/></g></symbol><symbol viewBox="0 0 210 210" id="haxe" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -87)"><path fill="#f68712" stroke-width=".221" d="M42.78 191.545l63.431-63.43 63.431 63.43-63.431 63.431z"/><path d="M42.8 191.592L31.193 148.28 19.59 104.97 62.9 116.575l43.311 11.605-31.706 31.706z" fill="#fab20b" stroke-width=".266"/><path d="M105.956 128.111l-43.19-11.544-43.177-11.597 22.927.185 23.228.294 20.264 11.36z" fill="#fbc707" stroke-width=".265"/><path d="M19.59 104.97l11.596 43.176 11.545 43.19-11.303-19.948-11.36-20.263-.294-23.228z" fill="#fff200" stroke-width=".265"/><path d="M106.23 128.133l43.312-11.605 43.311-11.605-11.605 43.31-11.605 43.312-31.706-31.706z" fill="#f47216" stroke-width=".266"/><path d="M169.711 191.289l11.545-43.19 11.597-43.176-.185 22.927-.294 23.228-11.36 20.263z" fill="#f1471d" stroke-width=".265"/><path d="M192.853 104.923l-43.176 11.597-43.19 11.544 19.947-11.303 20.264-11.36 23.228-.293z" fill="#fbc707" stroke-width=".265"/><path d="M169.643 191.545l11.605 43.31 11.605 43.312-43.311-11.605-43.311-11.606 31.706-31.705z" fill="#f25c19" stroke-width=".266"/><path d="M106.487 255.025l43.19 11.544 43.176 11.598-22.927-.185-23.228-.294-20.264-11.36z" fill="#f68712" stroke-width=".265"/><path d="M192.853 278.167l-11.597-43.176-11.545-43.19 11.303 19.947 11.36 20.264.294 23.228z" fill="#f1471d" stroke-width=".265"/><path d="M106.211 254.976l-43.31 11.605-43.312 11.605 11.605-43.31L42.8 191.563l31.706 31.706z" fill="#f89c0e" stroke-width=".266"/><path d="M42.731 191.82l-11.545 43.19-11.597 43.176.185-22.927.294-23.228 11.36-20.263z" fill="#fff200" stroke-width=".265"/><path d="M19.59 278.186l43.175-11.597 43.19-11.544-19.947 11.303-20.264 11.36-23.228.293z" fill="#f25c19" stroke-width=".265"/></g></symbol><symbol viewBox="0 0 144 152" id="heroku" xmlns="http://www.w3.org/2000/svg"><path d="M118.68 13.279H26.865c-6.337 0-11.476 5.139-11.476 11.476V129.32c0 6.338 5.139 11.477 11.476 11.477h91.813c6.338 0 11.477-5.14 11.477-11.477V24.755c0-6.337-5.139-11.476-11.477-11.476zM44.08 121.669V96.165l14.346 12.752zm44.632 0v-38.08c-.063-2.976-1.496-6.551-7.97-6.551-12.966 0-27.51 6.52-27.654 6.586l-9.008 4.08V32.407h12.752v36.201c6.366-2.072 15.266-4.321 23.91-4.321 7.882 0 12.6 3.099 15.17 5.698 5.484 5.547 5.56 12.613 5.551 13.43v38.255zm3.188-68.54H79.149c5.011-6.576 8.158-13.496 9.564-20.723h12.751c-.86 7.243-3.796 14.187-9.563 20.722z" fill="#6963b9"/></symbol><symbol viewBox="0 0 24 24" id="hpp" xmlns="http://www.w3.org/2000/svg"><path d="M9.757 19.818H6.751v-5.882q0-2.381-1.737-2.381-.868 0-1.438.663-.56.662-.56 1.718v5.882H0V4.533h3.016v6.508h.037Q4.24 9.239 6.247 9.239q3.51 0 3.51 4.239z" stroke-width=".478" fill="#0277bd"/><path d="M13.073 11.448v2h-2v2h2v2h2v-2h2v-2h-2v-2zm7 0v2h-2v2h2v2h2v-2h2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="html" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.56l4.07-1.13.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58-2.22.6-2.22-.6-.14-1.66h-2l.29 3.19L12 17.56M4.07 3h15.86L18.5 19.2 12 21l-6.5-1.8L4.07 3z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" id="http" xmlns="http://www.w3.org/2000/svg"><path d="M16.046 13.784c.074-.613.13-1.225.13-1.856s-.056-1.244-.13-1.856h3.137c.148.594.241 1.215.241 1.856a7.65 7.65 0 0 1-.241 1.856m-4.78 5.16c.557-1.03.984-2.144 1.281-3.304h2.738a7.452 7.452 0 0 1-4.019 3.304m-.232-5.16H9.828a12.314 12.314 0 0 1-.149-1.856c0-.631.056-1.253.149-1.856h4.343c.084.603.149 1.225.149 1.856 0 .63-.065 1.243-.149 1.856M12 19.315c-.77-1.113-1.393-2.348-1.773-3.675h3.545c-.38 1.327-1.002 2.562-1.773 3.675m-3.712-11.1h-2.71a7.353 7.353 0 0 1 4.01-3.304c-.557 1.03-.975 2.144-1.3 3.304m-2.71 7.425h2.71c.325 1.16.743 2.274 1.3 3.304a7.433 7.433 0 0 1-4.01-3.304m-.761-1.856a7.65 7.65 0 0 1-.241-1.856c0-.64.093-1.262.241-1.856h3.137c-.074.612-.13 1.225-.13 1.856 0 .63.056 1.243.13 1.856m4.046-9.253c.77 1.114 1.393 2.357 1.773 3.684h-3.545c.38-1.327 1.002-2.57 1.773-3.684m6.422 3.684h-2.738a14.523 14.523 0 0 0-1.28-3.304 7.412 7.412 0 0 1 4.018 3.304m-6.423-5.568c-5.132 0-9.28 4.176-9.28 9.28a9.28 9.28 0 0 0 9.28 9.282 9.28 9.28 0 0 0 9.281-9.281A9.28 9.28 0 0 0 12 2.647z" fill="#e53935" stroke-width=".928"/></symbol><symbol viewBox="0 0 24 24" id="image" xmlns="http://www.w3.org/2000/svg"><path d="M13.009 9.202h5.368l-5.368-5.368v5.368M6.177 2.37h7.808l5.856 5.856v11.711a1.952 1.952 0 0 1-1.952 1.952H6.178a1.951 1.951 0 0 1-1.952-1.952V4.322c0-1.083.868-1.952 1.952-1.952m0 17.567h11.71V12.13l-3.903 3.903-1.952-1.951-5.856 5.855M8.13 9.202a1.952 1.952 0 0 0-1.952 1.952 1.952 1.952 0 0 0 1.952 1.952 1.952 1.952 0 0 0 1.952-1.952A1.952 1.952 0 0 0 8.13 9.202z" fill="#26a69a" stroke-width=".976"/></symbol><symbol viewBox="0 0 512 512" id="ionic" xmlns="http://www.w3.org/2000/svg"><g fill="#4f8ff7"><path d="M423.592 132.804A31.855 31.855 0 0 0 429 115c0-17.675-14.33-32-32-32a31.853 31.853 0 0 0-17.805 5.409C344.709 63.015 302.11 48 256 48 141.125 48 48 141.125 48 256c0 114.877 93.125 208 208 208 114.873 0 208-93.123 208-208 0-46.111-15.016-88.71-40.408-123.196zM391.83 391.832c-17.646 17.646-38.191 31.499-61.064 41.174-23.672 10.012-48.826 15.089-74.766 15.089-25.94 0-51.095-5.077-74.767-15.089-22.873-9.675-43.417-23.527-61.064-41.174s-31.5-38.191-41.174-61.064C68.982 307.096 63.905 281.94 63.905 256c0-25.94 5.077-51.095 15.089-74.767 9.674-22.873 23.527-43.417 41.174-61.064s38.191-31.5 61.064-41.174c23.673-10.013 48.828-15.09 74.768-15.09 25.939 0 51.094 5.077 74.766 15.089a191.221 191.221 0 0 1 37.802 21.327A31.853 31.853 0 0 0 365 115c0 17.675 14.327 32 32 32 5.293 0 10.28-1.293 14.678-3.568a191.085 191.085 0 0 1 21.327 37.801c10.013 23.672 15.09 48.827 15.09 74.767 0 25.939-5.077 51.096-15.09 74.768-9.675 22.873-23.527 43.418-41.175 61.064z"/><circle cx="256.003" cy="256" r="96"/></g></symbol><symbol viewBox="0 0 24 24" id="java" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="javascript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3m4.73 15.04c.4.85 1.19 1.55 2.54 1.55 1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08-.58 0-.82-.4-1.09-.87l-1.38.83m5.98-.18c.5.98 1.51 1.73 3.09 1.73 1.6 0 2.8-.83 2.8-2.36 0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02 0-.41.31-.73.81-.73.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33-1.51 0-2.48.96-2.48 2.23 0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13 0 .48-.45.83-1.15.83-.83 0-1.31-.43-1.67-1.03l-1.38.8z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="javascript-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#ffca28"/><path d="M2.444 2.506h14.135v14.136H2.444V2.506m3.714 11.811c.315.668.935 1.218 1.995 1.218 1.178 0 1.987-.629 1.987-2.003V8.993H8.805v4.508c0 .675-.275.848-.707.848-.455 0-.644-.314-.856-.683l-1.084.651m4.697-.14c.392.769 1.185 1.358 2.426 1.358 1.257 0 2.199-.652 2.199-1.854 0-1.107-.636-1.602-1.767-2.089l-.33-.141c-.573-.243-.816-.408-.816-.801 0-.322.243-.573.636-.573.377 0 .628.165.856.573l1.028-.683c-.432-.754-1.044-1.045-1.884-1.045-1.186 0-1.948.754-1.948 1.752 0 1.083.636 1.594 1.594 2.002l.33.141c.613.267.974.432.974.888 0 .377-.354.652-.903.652-.652 0-1.029-.338-1.312-.81l-1.083.63z" fill="#ffca28"/></symbol><symbol viewBox="0 0 180 180" id="jenkins" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="gia"><path transform="scale(1 -1)" fill="#37474f" d="M.899-144.42h144.42V0H.899z"/></clipPath></defs><g transform="matrix(1.0691 0 0 -1.0691 9.4 166.143)" clip-path="url(#gia)"><g fill-rule="evenodd"><path d="M107.96 30.661l-12.506-1.876-16.883-1.876-10.943-.312-10.629.312-8.13 2.502-7.19 7.815-5.628 15.945-1.25 3.44-7.504 2.5-4.377 7.191-3.126 10.317 3.44 9.067 8.128 2.814 6.565-3.127 3.127-6.878 3.752.626 1.25 1.563-1.25 7.19-.313 9.068 1.876 12.505-.074 7.143 5.701 9.114 10.005 7.19 17.508 7.504 19.383-2.814 16.883-12.193 7.817-12.505 5.002-9.067 1.25-22.51-3.752-19.384-6.877-17.195-6.566-9.066" fill="#f0d6b7"/><path d="M97.334-23.425l-44.709-1.876v-7.503l3.752-26.262-1.876-2.19-31.264 10.63-2.19 3.752-3.126 35.328-7.19 21.26-1.563 5.002 25.01 17.195 7.818 3.127 6.877-8.441 5.94-5.315 6.88-2.188 3.125-.938L68.57 1.899l2.814-3.44 7.19 2.502-5.002-9.693 27.2-12.818-3.439-1.876" fill="#335061"/><path d="M23.238 85.687l8.128 2.814 6.566-3.127 3.127-6.878 3.751.626.938 3.751-1.876 7.19 1.876 17.197-1.563 9.379 5.627 6.565 12.193 9.692-3.44 4.69-17.194-8.442-7.191-5.627-4.064-8.754-6.253-8.442-1.876-10.005 1.251-10.63" fill="#6d6b6d"/><path d="M36.055 115.07s4.69 11.567 23.448 17.195c18.759 5.628.938 4.065.938 4.065l-20.321-7.817-7.817-7.816-3.438-6.253 7.19.626M26.676 87.875s-6.566 21.886 18.446 25.012l-.938 3.752-17.195-4.065-5.003-16.257 1.251-10.63 3.439 2.188" fill="#dcd9d8"/></g><g fill="#f7e4cd"><path d="M36.681 58.799l4.094 3.966s1.847-.214 2.16-2.402c.312-2.19 1.25-21.886 14.693-32.516 1.227-.97-10.004 1.564-10.004 1.564L37.62 45.042M94.209 64.739s.729 9.477 3.28 8.748c2.553-.729 2.553-3.28 2.553-3.28s-6.198-4.01-5.833-5.468" fill-rule="evenodd"/><path d="M120.16 99.442s-5.153-1.088-5.628-5.628c-.474-4.54 5.628-.938 6.566-.625M82.327 99.129s-6.879-.938-6.879-5.314c0-4.378 7.817-4.065 10.005-2.19"/><g fill-rule="evenodd"><path d="M39.807 78.808s-11.881 7.191-13.131.312c-1.25-6.877-4.065-11.88 1.876-19.07l-4.064 1.25-3.752 9.691-1.25 9.38 7.19 7.504 8.129-.626 4.69-3.751.312-4.69M45.435 98.504s5.315 27.512 32.203 32.827c22.136 4.375 33.765-.938 38.142-5.94 0 0-19.696 23.447-38.455 16.257-18.759-7.191-32.514-20.322-32.202-28.762.532-14.377.313-14.382.313-14.382M117.97 122.27s-9.066.312-9.38-7.817c0 0 0-1.25.625-2.5 0 0 7.192 8.129 11.568 3.751"/><path d="M78.268 111.1s-1.56 12.477-12.199 5.223c-6.878-4.69-6.252-11.255-5.002-12.505s.91-3.77 1.862-2.04c.952 1.728.638 7.356 4.078 8.918 3.439 1.564 9.077 3.31 11.26.404"/></g></g><g fill="#49728b" fill-rule="evenodd"><path d="M48.874 26.597L19.486 13.466s12.193-48.46 5.94-63.467l-4.377 1.563-.313 18.446-8.128 35.015-3.44 9.692 30.639 20.633 9.067-8.753M51.896-.206l4.17-5.087v-18.76h-5.003s-.625 13.132-.625 14.696c0 1.563.624 7.19.624 7.19M52-26.866l-14.069-.625 4.065-2.813L52-31.868"/></g><g fill-rule="evenodd"><path d="M100.15-23.739l11.567.313 2.814-28.764-11.881-1.563-2.5 30.014" fill="#335061"/><path d="M103.27-23.739l17.508.938s7.19 18.133 7.19 19.07c0 .939 6.253 26.263 6.253 26.263l-14.069 14.694-2.813 2.501-7.504-7.503V3.148l-6.565-26.887" fill="#335061"/><path d="M111.09-21.55l-10.942-2.188 1.563-8.755c4.064-1.876 10.943 3.127 10.943 3.127M111.4 33.162l21.885-16.257.626 7.503-16.57 15.32-5.94-6.566" fill="#49728b"/><path d="M62.85-85.332l-6.473 26.266-3.22 19.38-.531 14.385 29.296 1.56 18.226.003-1.658-32.83 2.814-25.324-.312-4.69-23.76-1.876-14.382 3.126" fill="#fff"/><path d="M96.083-23.426s-1.563-32.515 3.127-55.65c0 0-9.38-5.94-23.136-7.503l26.262.938 3.126 1.875-3.752 51.273-.938 10.944" fill="#dcd9d8"/><path d="M115.06-49.691l12.193 3.44 23.135 1.25 3.44 10.629-6.254 18.446-7.19.938-10.005-3.127-9.599-4.686-5.095.935-3.972-1.56" fill="#fff"/><path d="M114.84-43.435s8.128 3.751 9.38 3.438L120.78-22.8l4.065 1.563s2.814-16.257 2.814-18.133c0 0 17.507-.938 19.07-.938 0 0 3.752 7.191 2.814 14.694l3.44-10.005.312-5.628-5.002-7.503-5.627-1.25-9.38.312-3.126 4.064-10.943-1.563-3.44-1.25" fill="#dcd9d8"/></g><path d="M102.56-21.241L95.682-3.733l-7.19 10.317s1.562 4.377 3.75 4.377h7.192l6.878-2.501-.625-11.568-3.127-18.134" fill="#fff"/><path d="M103.9-15.297S95.145 1.585 95.145 4.086c0 0 1.563 3.752 3.752 2.814 2.19-.938 6.879-3.439 6.879-3.439v5.94l-10.63 2.19-7.19-.939 12.193-28.763 2.5-.313" fill="#dcd9d8" fill-rule="evenodd"/><path d="M65.664 25.968l-8.661.942-8.13 2.501v-2.814l3.972-4.38 12.506-5.627" fill="#fff"/><path d="M51.689 25.031s9.693-4.065 12.819-3.127l.311-3.748-8.752 1.872-5.316 3.752.938 1.251" fill="#dcd9d8" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43" fill="#d33833" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669" fill="#d33833" fill-rule="evenodd"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695" fill="#d33833" fill-rule="evenodd"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M102.87 10.649s-2.19 3.127-.626 4.065c1.564.938 3.127 0 4.065 1.563s0 2.501.313 4.377 1.877 2.189 3.44 2.501c1.562.313 5.94.938 6.565-.625l-1.876 5.627-3.752 1.25-11.88-6.877-.626-3.44v-6.877M70.041.331c-.376 4.88-.773 9.752-1.215 14.626-.662 7.279 1.748 6.009 8.057 6.009.964 0 5.933-1.15 6.289-1.876 1.705-3.483-2.851-2.709 1.964-5.335 4.065-2.216 11.246 1.346 9.603 6.273-.919 1.095-4.789.341-6.176 1.06l-7.327 3.8c-3.108 1.612-10.29 3.962-13.603 1.709-8.395-5.71.53-19.974 3.524-25.93" fill="#ef3d3a" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M78.268 111.1c-8.521 1.985-12.755-3.566-15.338-9.323-2.306.559-1.389 3.695-.806 5.294 1.525 4.194 7.672 9.778 12.694 9.02 2.161-.325 5.086-2.301 3.45-4.99M119.79 101.4l.404-.016c1.926-4 3.593-8.238 6.022-11.769-1.628-3.79-12.322-7.144-12.157-.338 2.313 1.01 6.305.206 8.356 1.497-1.186 3.254-2.897 6.024-2.625 10.626M82.63 101.29c1.827-3.35 2.422-6.868 5.019-9.4 1.17-1.14 3.444-2.529 2.316-5.698-.263-.747-2.189-2.414-3.3-2.741-4.06-1.2-13.521-.248-10.317 4.814 3.358-.157 7.871-2.18 10.38.257-1.927 3.081-5.363 9.177-4.098 12.768M118.26 67.253c-6.113-3.927-12.93-8.197-22.947-7.207-2.14 1.86-2.956 6.002-.877 8.737 1.082-1.861.402-5.284 3.419-5.799 5.684-.972 12.299 3.477 16.387 5.032 2.535 4.275-.219 5.847-2.503 8.597-4.675 5.636-10.947 12.622-10.72 21.06 1.89 1.37 2.053-2.092 2.325-2.722 2.44-5.714 8.585-13.021 13.07-17.912 1.1-1.205 2.914-2.36 3.115-3.157.582-2.315-1.513-5.09-1.27-6.63M37.668 71.387c-1.916 1.094-2.372 5.91-4.622 6.048-3.215.195-2.629-6.25-2.616-10.018-2.213 2.009-2.602 8.194-.976 11.37-1.853.91-2.68-1.003-3.708-1.677 1.32 9.595 14.036 4.45 11.922-5.723M122.15 63.257c-2.846-5.417-6.871-11.382-15.222-11.555-.17 1.75-.3 4.411.009 5.464 6.384.614 10.325 3.863 15.212 6.091M82.149 59.745c5.326-2.8 15.114-3.102 22.353-2.89.388-1.586.379-3.545.394-5.48-9.305-.463-20.307 1.84-22.747 8.37M81.136 54.523c3.683-9.247 16.341-8.182 27.016-7.927-.47-1.2-1.489-2.62-2.755-3.132-3.42-1.392-12.855-2.448-17.604.074-3.011 1.601-4.946 5.219-6.596 7.34-.797 1.024-4.765 3.64-.06 3.645"/></g><path d="M117.82 3.516c-4.322-7.402-8.457-15.005-13.585-21.534 2.15 6.32 3.07 16.9 3.394 24.965 4.498 2.105 8.349-.474 10.191-3.43" fill="#81b0c4" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M141.07-23.089c-4.839-.969-8.239-5.671-12.959-5.37 2.594 3.658 7.14 5.2 12.959 5.37M143.21-30.661c-3.944-.417-8.576-1.055-12.577-.726 1.894 2.892 9.19 1.894 12.577.726M144.58-37.19c-4.433-.096-9.942-.008-14.155.346 2.492 2.677 11.28.993 14.155-.346"/></g><g fill-rule="evenodd"><path d="M109.48-55.057c.636-5.567 2.843-11.207 2.566-17.304-2.45-.827-3.858-1.55-7.142-1.545-.232 5.181-.925 13.102-.718 18.041 1.615-.107 3.997 1.154 5.294.808" fill="#dcd9d8"/><path d="M102.33 26.985c-2.226-1.453-4.121-3.267-6.259-4.818-4.74-.235-7.327.328-10.81 3.05.057.219.407.121.42.39 5.075-2.262 11.524.92 16.648 1.378" fill="#f0d6b7"/><path d="M75.694-7.603c1.394 6.04 6.857 9.17 11.817 12.497 5.12-6.498 8.234-14.855 11.663-22.92-8.102 2.443-16.38 6.406-23.481 10.423" fill="#81b0c4"/><path d="M104.18-55.865c-.207-4.94.486-12.86.718-18.041 3.283-.004 4.691.718 7.142 1.545.276 6.096-1.93 11.737-2.566 17.304-1.298.346-3.679-.914-5.294-.808zm-51.13 28.09c2.165-19.906 5.301-36.639 11.054-54.266 12.766-3.876 28.157-4.214 39.441-.716-2.072 9.948-1.167 22.06-2.378 32.677-.912 7.98-.447 16.009-1.698 24.15-13.673 2.844-33 .665-46.418-1.845zm49.651 1.72c-.115-8.549.383-16.982 1.036-25.542 3.282.493 5.51.822 8.56 1.49-.99 8.241-.869 17.514-2.886 24.804-2.332-.023-4.385.027-6.71-.752zm16.653 1.378c-1.558.357-3.372.014-4.86-.015.7-6.969 2.397-14.659 2.995-21.974 2.342-.073 3.593 1.032 5.52 1.403.102 6.421-.562 15.268-3.655 20.586zm25.215-23.038c4.882 1.186 7.952 7.165 6.586 13.305-.916 4.127-2.548 11.898-4.295 14.538-1.29 1.953-4.79 4.51-7.584 2.72-4.545-2.91-12.552-3.755-15.867-7.278 1.662-5.534 2.178-13.135 2.864-20.146 5.678-.354 12.665 1.562 17.387-.471-3.297-1.068-7.575-1.077-10.423-2.633 2.328-1.125 7.778-.897 11.332-.035zM99.17-18.025c-3.43 8.063-6.543 16.42-11.663 22.918-4.96-3.327-10.423-6.456-11.817-12.497 7.1-4.017 15.379-7.98 23.481-10.422zm8.453 24.971c-.325-8.065-1.245-18.644-3.395-24.965 5.128 6.53 9.263 14.132 13.585 21.534-1.842 2.957-5.693 5.536-10.19 3.431zm-9.582 3.405c-1.943.21-3.592-2.233-6.117-1.177-.58-.64-1.105-1.333-1.695-1.958 5.579-6.723 8.114-16.262 12.423-24.163 2.312 7.59 2.045 15.904 2.555 24.188-3.177-.201-4.94 2.873-7.166 3.11zm-6.161 8.132c-.208-2.303.328-3.056.791-5.695 7.57-2.367 6.248 10.388-.791 5.695zm-8.394 2.755c-3.261 1.782-8.161 3.723-12.374 4.527-5.222.999-4.732-7.123-4.51-11.968.173-3.836 2.168-7.893 3.035-10.441.406-1.19.498-2.453 1.515-2.69 1.798-.418 7.73 1.954 9.42 2.875 3.575 1.95 6.348 5.045 9.384 7.123.04 1.011.078 2.021.119 3.032-1.826.91-3.935 1.555-6.615 1.673 1.818.914 4.492.901 6.148 1.989.016.405.033.81.047 1.21-3.024.234-4.176 1.58-6.17 2.67zm-31.152 5.659c-2.707-2.748 7.592-6.494 10.871-6.696-.018 1.739.991 3.378.788 4.626-3.895.684-9.013.232-11.66 2.07zm33.345-1.29c-.013-.27-.363-.172-.42-.39 3.482-2.722 6.07-3.285 10.81-3.05 2.137 1.551 4.033 3.365 6.259 4.818-5.124-.458-11.574-3.64-16.648-1.379zm30.606-9.282c-.146 3.053-.948 9.332-2.835 10.431-3.961 2.312-11.002-4.668-13.984-5.732.324-.934.86-1.674.901-2.868 1.764.434 3.912.137 5.44-.615-1.767-.198-3.727-.185-4.897-1.027-.429-1.239.105-2.927-.18-4.647 4.196-1.184 8.989-1.814 14.294-1.97 1.032 1.341 1.383 3.896 1.261 6.429zM47.777 24.24c-.85.606-6.6 8.087-7.388 7.777-10.405-4.103-20.134-11.199-28.828-17.91 8.29-17.787 11.635-39.579 12.227-60.582 9.496-4.441 17.836-10.844 30.722-11.512-1.491 10.55-2.852 19.962-3.699 29.895-3.237 1.365-7.882-.062-10.913.423-.025 3.651 4.628 1.6 5.015 4.054.292 1.858-2.56 1.998-1.631 4.923 2.368-.861 3.612-2.763 6.138-3.477 2.309 5.05-.032 13.985.3 18.205.064.792.397 4.39 2.172 3.759 1.57-.559-.09-9.569.082-13.563.157-3.68-.444-7.242 1.046-9.552a355.817 355.817 0 0 0 38.576 3.16c-2.964 1.272-6.485 2.475-10.345 4.651-2.093 1.18-8.69 3.635-9.293 5.622-.964 3.167 2.528 4.855 3.125 7.57-6.285-3.428-7.511 3.286-8.998 8.042-1.347 4.308-2.114 7.526-2.445 10.01-5.414 2.581-11.203 5.195-15.863 8.505zm63.009 6.872c8.67 4.204 10.232-15.711 6.834-22.127.525-1.914 2.331-2.646 3.069-4.366-4.838-8.667-10.211-16.756-15.148-25.32 3.672 2.286 8.917.409 13.238 2.12 1.58.624 2.722 4.24 3.918 7.133 3.29 7.958 6.743 17.99 8.28 25.586.346 1.73 1.292 5.5 1.08 7.04-.378 2.758-4.12 4.803-6.022 6.508-3.506 3.15-5.714 5.921-9.371 8.866-1.483-2.189-4.666-3.66-5.878-5.44zM27.95 107.99c-4.13-4.545-3.266-13.062-2.766-19.121 7.467 4.697 17.377-.372 17.284-8.36 3.565.094 1.332 4.452.687 7.259-2.107 9.169 3.55 19.13.256 27.516-6.395-.485-11.649-3.097-15.46-7.294zm29.558 26.38c-9.352-2.65-21.337-9.446-25.18-17.847 2.976.432 5.041 1.933 7.977 2.119 1.11.072 2.563-.466 3.838-.148 2.54.63 4.685 6.327 6.602 8.447 1.868 2.07 4.114 2.954 5.651 4.841.988.477 2.448.444 2.504 1.927-.428.457-.879.806-1.392.66zm48.681-2.493c-9.707 5.477-26.136 9.596-36.462 4.449-8.331-4.155-19.593-11.027-23.433-19.737 3.587-8.405-1.062-16.106-1.36-24.64-.157-4.54 2.139-8.504 2.315-13.446-1.228-2.025-4.978-2.275-7.574-2.136-.873 4.372-2.403 9.287-6.906 9.78-6.371.697-11.03-4.576-11.319-10.085-.342-6.48 4.978-17.22 12.517-16.475 2.913.287 3.629 3.207 6.802 3.177 1.72-3.432-2.653-4.51-3.103-6.964-.117-.634.363-3.112.642-4.274 1.37-5.658 4.422-12.982 7.427-17.29 3.814-5.464 11.307-6.288 19.37-6.823 1.44 3.101 6.743 2.846 10.2 2.035-4.143 1.64-7.993 5.617-11.185 9.137-3.665 4.039-7.378 8.371-7.566 13.65 6.927-9.61 12.65-18.003 25.246-22.23 9.53-3.196 20.662 1.465 27.986 6.608 3.039 2.137 4.853 5.529 7.013 8.634 8.082 11.626 11.854 28.219 11.024 44.303-.342 6.633-.327 13.244-2.552 17.706-2.326 4.666-10.193 8.84-14.8 4.62-.853 4.537 3.83 7.344 9.331 5.71-3.922 5.063-8.039 11.145-13.614 14.29zm18.084-149.66c7.585 3.77 21.757 10.149 26.512-.014 1.755-3.746 3.814-10.079 4.723-13.946 1.284-5.456-1.392-16.923-7-18.754-4.953-1.617-10.733-1.518-16.7-.32-.702.585-1.484 1.603-2.03 2.665-4.261.165-8.25-.229-11.615-1.98.319-3.15-1.812-3.656-3.81-4.305-1.48-5.872 2.963-13.541 1.9-18.896-.76-3.815-5.453-4.405-8.902-5.118-.113-2.12.15-3.89.386-5.683-.789-2.907-4.327-4.561-7.679-4.967-11.029-1.326-27.775-1.922-38.384 1.893-2.96 7.261-5.292 16.093-7.758 24.384-10.346-1.105-18.715 4.464-26.603 8.113-2.731 1.266-6.51 1.964-7.53 4.138-.99 2.105-.584 6.14-.83 9.95-.625 9.733-1.16 19.12-3.73 29.086-1.154 4.472-3.165 8.418-4.568 12.727C9.358 5.184 7.092 10.12 6.5 14.1c-.877 5.903 4.681 6.232 8.235 8.79 5.494 3.954 9.806 6.142 15.756 9.711 1.762 1.057 7.077 3.733 7.681 4.966 1.202 2.443-2.062 5.888-2.935 7.803-1.38 3.03-2.1 5.602-2.298 8.59-4.992.789-8.775 3.76-11.06 7.109-3.781 5.543-6.403 15.798-3.132 23.599.257.614 1.536 1.822 1.725 2.765.372 1.858-.7 4.329-.768 6.305-.343 10.14 1.716 18.875 8.541 21.932 2.771 11.038 12.688 14.71 22.032 20.195 3.493 2.05 7.343 3.36 11.32 4.824 14.263 5.25 36.15 4.261 47.987-4.692 5.02-3.797 13.044-11.813 15.914-17.617 7.58-15.323 7.042-40.931 1.74-59.571-.712-2.503-1.746-6.181-3.19-9.187-1.006-2.1-4.134-6.3-3.754-8.153.391-1.916 7.132-7.034 8.577-8.428 2.603-2.51 7.548-5.843 7.948-9.012.43-3.372-1.485-7.984-2.456-11.238-3.245-10.858-6.412-20.895-10.091-30.576" fill="#231f20"/><path d="M73.674 57.38c.411.548 2.674 1.38 5.84-.144 0 0-3.752-.626-3.44-6.881l-1.564.313s-1.615 5.672-.836 6.712" fill="#f7e4cd"/><path d="M101.09 3.617a1.72 1.72 0 1 0-3.44.001 1.72 1.72 0 0 0 3.44-.001M102.81-4.355a1.72 1.72 0 1 0-3.44 0 1.72 1.72 0 0 0 3.44 0" fill="#1d1919"/></g><g><rect transform="matrix(.8 0 0 -.8 0 144)" x="16.854" y="177.38" width="70.412" height="4.12" rx=".983" ry=".983"/><rect transform="scale(1 -1)" x="78.502" y="-2.097" width="50.037" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="13.483" y="-3.697" width="54.831" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="83.296" y="-3.697" width="45.243" height="3.296" rx=".786" ry=".786"/></g></g></symbol><symbol viewBox="0 0 24 24" id="json" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#fbc02d"/></symbol><symbol viewBox="0 0 50 50" id="julia" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" stroke-width="5.673"><circle cx="13.497" cy="281.632" r="9.555" fill="#bc342d"/><circle cx="36.081" cy="281.632" r="9.555" fill="#864e9f"/><circle cx="24.722" cy="262.389" r="9.555" fill="#328a22"/></g></symbol><symbol viewBox="0 0 64 64" id="karma" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -233)"><path d="M38.556 288.413l-20.29-26.687 9.532-7.246 20.29 26.686h-.001.002l5.527 7.247z" fill="#359b8b" stroke-width=".173"/><path d="M35.681 241.172L24.92 255.327v-14.13H12.947v13.817l7.84 33.235h4.132v-13.147l.003.003 20.29-26.686-.008-.006 5.504-7.24H35.84v.12z" fill="#3cbeae" stroke-width=".206"/></g></symbol><symbol viewBox="0 0 24 24" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7 14a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2m5.65-4A5.99 5.99 0 0 0 7 6a6 6 0 0 0-6 6 6 6 0 0 0 6 6 5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65z" fill="#26a69a"/></symbol><symbol viewBox="0 0 24 24" id="kivy" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.89 0 0 1.89 -12.157 -11.429)" fill="#90a4ae"><path d="M7.026 8.63v4.474l1.928-1.928a.437.437 0 0 0 0-.619zM9.38 16.072v-4.474l-1.927 1.927a.437.437 0 0 0 0 .62zM18.576 10.412l-5.346.564-.017.018 2.39 2.39zM9.922 8.502s.023 3.304-.003 4.452c-.02.856.371 1.114.746 1.507.538.564 1.599 1.57 1.599 1.57a.53.53 0 0 0 .75 0l1.843-1.844a.53.53 0 0 0 0-.75z"/></g></symbol><symbol viewBox="0 0 24 24" id="kl" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:#3aaae1}.b{fill:#fdfeff}</style></defs><title>kl</title><path d="M12.033 1.737c-.25-.003-.5.11-.729.337C8.225 5.15 5.15 8.227 2.078 11.31c-.144.144-.229.346-.341.521v.41c.16.223.294.474.485.666a3259.51 3259.51 0 0 0 8.936 8.937c.193.192.443.325.666.486h.41c.205-.142.436-.256.609-.428 3.046-3.041 6.09-6.083 9.133-9.127.47-.47.472-1.005.006-1.472l-9.218-9.217c-.23-.23-.48-.347-.731-.35zm-1.062 4.545l1.386.832c.702.422 1.403.846 2.109 1.262a.544.544 0 0 1 .04.026l.016.013.017.013c.061.056.089.123.088.224a510.281 510.281 0 0 0 0 3.794.463.463 0 0 1-.007.094c-.015.069-.054.103-.142.109a.464.464 0 0 1-.044.002c-.045-.002-.09-.002-.136-.003-.323-.006-.648-.001-.998-.001v-.527-1.34-.671-.003l.004-.668c0-.147-.039-.231-.17-.308-.893-.528-1.78-1.066-2.67-1.6-.051-.03-.101-.065-.173-.111l.001-.003h-.001zm.362 3.39c.068-.003.119.043.173.138.085.148.174.293.264.44l.015.025c.096.154.194.31.292.47l-1.915 1.176c-.337.207-.673.417-1.014.617-.113.067-.154.143-.154.277.01.977.01 1.955.014 2.932V16H7.7V16h-.002c-.004-.053-.014-.112-.014-.17-.005-1.25-.006-2.501-.015-3.751 0-.142.045-.222.164-.294a467.13 467.13 0 0 0 3.353-2.054l.016-.01a.606.606 0 0 1 .032-.017l.016-.008a.308.308 0 0 1 .033-.013l.012-.004a.157.157 0 0 1 .028-.005l.01-.001zm5.677 3.126l.314.54.346.594v.001c-.158.094-.298.178-.438.259l-3.097 1.798c-.106.062-.189.071-.3.01l-.893-.496-1.524-.843-.895-.493c-.035-.02-.068-.044-.129-.085h.001l.137-.25.495-.902 1.446.795c.442.243.886.483 1.323.734.121.07.212.072.334 0 .894-.525 1.792-1.043 2.689-1.563.057-.034.118-.061.191-.1z" fill="#29b6f6" stroke-width=".041"/></symbol><symbol viewBox="0 0 24 24" id="kotlin" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="gpb"><stop offset="0" stop-color="#cb55c0"/><stop offset="1" stop-color="#f28e0e"/></linearGradient><linearGradient id="gpa"><stop offset="0" stop-color="#0296d8"/><stop offset="1" stop-color="#8371d9"/></linearGradient><linearGradient xlink:href="#gpa" id="gpc" x1="1.725" y1="22.67" x2="22.185" y2="1.982" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/><linearGradient xlink:href="#gpb" id="gpd" x1="1.869" y1="22.382" x2="22.798" y2="3.377" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/></defs><path d="M3.307 3.003v18.048h18.05v-.03L16.88 16.51l-4.48-4.515 4.48-4.515 4.443-4.477H3.307z" fill="url(#gpc)"/><path d="M12.538 3.003l-9.23 9.23v8.818h.083l9.032-9.032-.025-.024 4.48-4.515 4.444-4.477h-8.784z" fill="url(#gpd)"/></symbol><symbol viewBox="0 0 240 240" id="laravel" xmlns="http://www.w3.org/2000/svg"><path d="M216.05 119.036c-1.433.343-24.945 6.673-24.945 6.673l-19.227-28.622c-.537-.828-.99-1.656.359-1.849 1.345-.196 23.195-4.477 24.182-4.723.99-.245 1.837-.536 3.053 1.267 1.21 1.8 17.836 24.626 18.464 25.506.627.877-.447 1.41-1.883 1.748m-4.101 49.326c.588 1.003 1.176 1.64-.67 2.367-1.843.73-62.243 22.847-63.418 23.39-1.173.546-2.092.73-3.607-1.637-1.51-2.362-21.16-39.264-21.16-39.264l64.03-18.075c1.876-.644 2.317-.405 3.103.822 1.074 1.68 21.143 31.403 21.726 32.4m-103.7-21.087c-.78.202-37.566 9.733-39.525 10.22-1.965.485-1.965.246-2.188-.49-.226-.727-43.728-98.053-44.333-99.271-.605-1.214-.574-2.177 0-2.177.571 0 34.734-3.313 35.944-3.383 1.207-.07 1.08.205 1.526 1.033l49.025 91.818c.84 1.58 1.239 1.81-.452 2.248m94.588-59.77c-3.5-4.58-5.2-3.751-7.357-3.41-2.154.336-27.277 4.915-30.194 5.449-2.918.536-4.758 1.803-2.963 4.53 1.597 2.422 18.113 27.824 21.751 33.42l-65.663 17.066L66.18 49.832c-2.075-3.342-2.507-4.514-7.236-4.28-4.735.23-40.969 3.495-43.55 3.731-2.58.233-5.416 1.479-2.835 8.09 2.583 6.612 43.734 102.82 44.88 105.62 1.149 2.803 4.128 7.345 11.11 5.527 7.157-1.871 31.969-8.894 45.52-12.742 7.163 14.07 21.77 42.619 24.473 46.707 3.607 5.459 6.089 4.56 11.626 2.738 4.325-1.42 67.65-26.129 70.502-27.4 2.855-1.273 4.613-2.184 2.685-5.275-1.419-2.28-18.124-26.558-26.876-39.26 5.993-1.733 27.305-7.888 29.575-8.557 2.646-.779 3.008-2.19 1.572-3.94-1.436-1.755-21.293-28.72-24.79-33.296z" fill="#ff5722" stroke="#ff5722" stroke-width="8.852" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="less" xmlns="http://www.w3.org/2000/svg"><path d="M13.696 2.999V5h2.002v5a2 2 0 0 0 1.999 2 2 2 0 0 0-2 2v5h-2v2h2a2 2 0 0 0 2-2v-4a2 2 0 0 1 2-2h1V11h-1a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2.001zm-.03 12.766v.47a1 1 0 0 0 .03-.236 1 1 0 0 0-.03-.234zM10.566 21v-2.001H8.565v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2.001v-2H8.565a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-.999V13h1a2 2 0 0 1 2 2v3.999A2 2 0 0 0 8.564 21zm.03-12.766v-.47a1 1 0 0 0-.03.236 1 1 0 0 0 .03.234z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="lib" xmlns="http://www.w3.org/2000/svg"><path d="M19 7H9V5h10m-4 10H9v-2h6m4-2H9V9h10m1-7H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M4 6H2v14a2 2 0 0 0 2 2h14v-2H4V6z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 40 40" id="livescript" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -257)" fill="#317eac"><path stroke-width="3.299" d="M5.419 260.18h3.685v34.207H5.419z"/><path stroke-width="3.299" d="M37.074 288.197v3.685H2.867v-3.685z"/><path stroke-width="2.894" d="M29.612 265.658l2.004 2.005L7.428 291.85l-2.004-2.005z"/><path stroke-width="2.325" d="M10.73 262.471h2.835v22.08H10.73z"/><path stroke-width="2.063" d="M15.36 262.519h2.835v17.382H15.36z"/><path stroke-width="1.77" d="M19.99 262.471h2.835v12.802H19.99z"/><path stroke-width="1.422" d="M24.526 262.491h2.835v8.254h-2.835z"/><path stroke-width="1.128" d="M28.783 262.463h2.835v5.197h-2.835z"/><path stroke-width="2.325" d="M34.801 286.545v-2.835h-22.08v2.835z"/><path stroke-width="2.063" d="M34.753 281.914v-2.835H17.371v2.835z"/><path stroke-width="1.77" d="M34.801 277.284v-2.835H21.999v2.835z"/><path stroke-width="1.422" d="M34.781 272.749v-2.835h-8.254v2.835z"/><path stroke-width="1.128" d="M34.809 268.492v-2.835h-5.197v2.835z"/></g></symbol><symbol viewBox="0 0 24 24" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="lua" xmlns="http://www.w3.org/2000/svg"><circle cx="12.203" cy="12.102" r="10.322" fill="none" stroke="#42a5f5"/><path d="M12.33 5.746a6.483 6.381 0 0 0-6.482 6.381 6.483 6.381 0 0 0 6.482 6.38 6.483 6.381 0 0 0 6.484-6.38 6.483 6.381 0 0 0-6.484-6.38zm1.86 1.916a2.329 2.292 0 0 1 2.33 2.293 2.329 2.292 0 0 1-2.33 2.291 2.329 2.292 0 0 1-2.329-2.29 2.329 2.292 0 0 1 2.328-2.294z" fill="#42a5f5" fill-rule="evenodd"/><ellipse cy="4.615" cx="19.631" rx="2.329" ry="2.292" fill="#42a5f5" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="markdown" xmlns="http://www.w3.org/2000/svg"><path d="M2 16V8h2l3 3 3-3h2v8h-2v-5.17l-3 3-3-3V16H2m14-8h3v4h2.5l-4 4.5-4-4.5H16V8z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" preserveAspectRatio="xMidYMid" id="markojs" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -120.96)" stroke-width=".984"><path d="M4.002 126.482c-.655 1.07-1.32 2.14-1.976 3.21-.655 1.06-1.308 2.142-1.963 3.212l.002.002-.002.002c.655 1.07 1.308 2.15 1.963 3.211.655 1.07 1.32 2.141 1.976 3.211h3.33c-.664-1.07-1.318-2.14-1.974-3.21-.653-1.069-1.307-2.145-1.961-3.214.654-1.068 1.308-2.146 1.961-3.215a601.93 601.93 0 0 1 1.974-3.209z" fill="#2196f3"/><path d="M3.999 126.482l-.002.002c.655 1.07 1.31 2.15 1.964 3.212.655 1.07 1.32 2.14 1.974 3.21h3.331c-.664-1.07-1.319-2.14-1.974-3.21-.653-1.068-1.306-2.146-1.96-3.214z" fill="#26a69a"/><path d="M15.203 126.482l.002.002c-.655 1.07-1.31 2.15-1.965 3.212-.655 1.07-1.319 2.14-1.974 3.21h-3.33c.664-1.07 1.318-2.14 1.973-3.21.654-1.069 1.307-2.146 1.961-3.214z" fill="#8bc34a"/><path d="M11.874 126.484c.664 1.07 1.318 2.14 1.974 3.21.653 1.068 1.307 2.146 1.961 3.214-.654 1.069-1.308 2.145-1.961 3.213-.656 1.07-1.31 2.14-1.974 3.21h3.33c.655-1.07 1.319-2.14 1.974-3.21.655-1.06 1.31-2.14 1.966-3.21l-.002-.003.002-.002c-.656-1.07-1.311-2.152-1.966-3.213-.655-1.07-1.319-2.138-1.974-3.209z" fill="#ffc107"/><path d="M16.74 126.482c.665 1.07 1.319 2.14 1.974 3.21.654 1.068 1.306 2.146 1.96 3.214-.654 1.069-1.306 2.145-1.96 3.213-.655 1.07-1.31 2.141-1.974 3.211h3.33c.656-1.07 1.32-2.14 1.974-3.21.655-1.062 1.31-2.141 1.966-3.212l-.002-.002.002-.002c-.655-1.07-1.31-2.152-1.966-3.213-.655-1.07-1.318-2.138-1.973-3.209z" fill="#f44336"/></g></symbol><symbol viewBox="0 0 23 24" id="mathematica" xmlns="http://www.w3.org/2000/svg"><path d="M11.512 1.523l-.073.025-.46.794-.454.763-1.217 2.09H9.29L5.435 3.5l-.1-.047h-.018v.092l.025.163v.086l.132 1.226v.082l.032.252v.082l.22 2.137v.075l.018.082v.06l-2.348.507-.04.015-.457.1-.025.01h-.042l-1.096.244-.04.007-.17.036v.082l.018.01 1.859 2.086.053.052.114.132.804.909v.005l-.053.05-.22.257-2.564 2.875-.01.007v.082l.071.006.295.075 1.697.366v.006l2.139.472h.015v.047l-.036.252v.08l-.046.412v.082l-.036.244v.082l-.045.412v.08l-.05.41v.08l-.036.244v.082l-.046.412v.082l-.05.407v.082l-.032.248V20l-.05.407v.104h.037l3.642-1.6.294-.134h.018l.177.312.539.911.015.032.854 1.465.16.262.404.695.007.022h.092l.005-.022.017-.025.56-.947.014-.042.6-1.033.316-.539.644-1.091.05.013 3.906 1.721h.035v-.085l-.138-1.32v-.082l-.032-.244v-.082l-.035-.245v-.085l-.033-.244v-.081l-.032-.245v-.082l-.032-.244v-.085l-.035-.245v-.082l-.032-.245v-.082l-.033-.244v-.085l-.025-.17v-.053l1.632-.354.043-.008.458-.107h.028v-.01l.23-.05.03-.01h.042l.382-.09.025-.01h.043l.194-.05h.033l1.015-.23.07-.007v-.064l-.015-.013-1.19-1.342-.028-.028-.197-.22-1.428-1.604v-.006l.295-.323.4-.457 2.148-2.408.015-.01v-.065l-.035-.008-1.288-.28-.372-.084-.047-.01-2.481-.544v-.045l.432-4.265v-.02h-.042l-.302.135-.01.014h-.025l-3.307 1.45-.297.135h-.015l-2.028-3.483-.099-.145-.014-.045zm-.001 1.114l1.365 2.323.34.592-.008.025-1.18 1.511-.517.66-.012-.01-.258-.335-.04-.05-1.397-1.787.03-.063 1.378-2.365.287-.491zm4.908 2.039l-.007.025-.168.225-.538.066zm-9.817.004l.053.02.677.3h-.499l-.224-.3zM16.947 5l-.123 1.248-.113-.928.226-.307zm-9.26.156l.053.024.705.309-.757-.175zm7.388.116l.02.168-1.318.403.003-.003.16-.071 1.015-.444zM9.669 6.388l.944 1.204v.01L9.483 7.2zm3.55.172l.21.682-.234.084-.089.022-.702.255.008-.022.776-.982zm-5 .836l.986.356.898.312.048.02 1.054.373.011 3.086-.362-.117-.67-.224-.081-.038-.735-.245-.77-.256-.29-.1-.011-.255-.032-1.195-.01-.287-.015-.894-.013-.297zm6.583 0l-.011.227-.028.9-.008.303-.032 1.475-.01.262-.337.117-.734.245-.77.256-.712.245-.355.117.01-3.086 1.632-.578zm.585.437l.09.735.79-.097-.915 1.302-.018.006.01-.183.018-.877zm-9.451.536l.152.22 1.447 2.049-2.607.968-.05.015-1.972-2.214-.28-.312.003-.01.115-.018.424-.1.14-.021.337-.078.042-.01zm11.146.003l3.284.713.029.01-.022.025-1.954 2.192-.277.312-.092-.036-2.564-.95.475-.681.152-.216zM6.787 8.52h.86l.036 1.258-.013-.006-.763-1.078zm1.358 2.625l.152.06.77.252.712.245.746.247.49.167-.065.092-1.723 2.334-1.015-.302-.082-.017-.035-.015-1.902-.56.938-1.22.981-1.277zm6.73 0l.033.006 1.787 2.327.132.17-.128.036-.032.014-2.196.642-.105.032-.564.17-.018-.003-1.053-1.44-.174-.239-.547-.726-.007-.018.469-.16.769-.254.713-.245.77-.252zm-7.766.305l-.007.02-.405.523-.291-.291.657-.245zm8.802 0l.043.007.578.212.714.27-.661.394-.375-.479-.03-.042-.262-.342zm-10.843.75l-.67.668.355-.397.207-.23zm12.911.016l.068.025.045.042.554.627.042.043.204.228-.255.135zm-6.473.265l.022.015 1.38 1.872.032.05.343.465.008.031-.088.117-.422.629-.047.074-.245.343-.97 1.43-.013.007-1.18-1.72-.096-.16-.493-.708-.008-.037 1.618-2.191.007-.01zm7.827 1.194l.565.633.063.082-.272-.093-.037-.013zm-15.785.148l.297.299-.637.218-.152.05.038-.058zm13.224.47l-.855.448.346.66-.185-.058-.27-.088-1.092-.348.012-.01zm-9.687.255l1.222.356-.006.007-.458.145-.443.135-.032.01-.49.157zm-2.765.048l.318.32 2.007.517-.567.18-.055.004-2.103-.469-.744-.156.007-.006zm14.966.205l.548.188v.003l-.457.1-.043.014-1.069.23zm-10.23.507l.007.227.01.347.025 1.363.025.691-.007.255-.24.107-2.863 1.255.032-.372.033-.255.017-.227.031-.256.037-.407.045-.42.018-.23.032-.251.032-.412.05-.414.013-.14 1.455-.457.003-.014.301-.098zm4.908 0l1.245.39v.014l.312.1 1.146.362.022.23.03.255.043.408.04.42.017.23.033.251.032.412.042.325.078.848-.078-.04-3.025-1.322-.004-.305.06-2.368zm-4.295.617l.015.007.067.107.6.875-.64.531-.034-1.438zm3.671 0h.008l-.005.06-.02.678-.005.214-.479-.223zm-2.888 3.605l.763.915.001.37-.017-.006-.025-.05-.464-.791-.012-.018zm1.53.61l.184.083-.343.586-.018.007.002-.532z" fill="#f44336" fill-rule="evenodd" stroke="#f44336" stroke-width=".7747499999999999" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 720 720" id="matlab" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><path d="M209.247 329.98L52.368 387.638l121.325 85.822 96.752-95.804-61.198-47.674z" fill="#4db6ac" fill-rule="evenodd" stroke-width=".3"/><path d="M480.193 71.446c-13.123 1.784-9.565 1.013-28.4 16.09-18.008 14.418-69.925 100.347-97.673 129.256-24.688 25.722-34.46 12.199-60.102 33.661-25.68 21.494-65.273 64.464-65.273 64.464l63.978 47.32L394.15 222.754c23.948-32.932 23.694-37.266 36.744-71.82 6.384-16.907 17.76-29.9 27.756-45.809 12.488-19.874 30.186-34.855 21.543-33.68z" fill="#00897b" fill-rule="evenodd" stroke-width=".3"/><path d="M478.206 69.796c-31.268-.189-62.068 137.245-115.56 242.691-54.543 107.519-162.235 176.82-162.235 176.82 18.156 8.243 34.681 4.91 54.236 23.394 13.375 16.164 52.09 95.976 75.174 146.117 0 0 18.964-10.297 42.994-27.695 24.03-17.397 53.124-41.896 73.384-70.3 26.883-37.692 47.897-61.043 65.703-75.271 17.806-14.23 32.404-19.336 46.458-20.54 50.238-4.305 124.582 85.792 124.582 85.792S527.267 70.09 478.206 69.796z" fill="#ffb74d" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 24 24" id="merlin" xmlns="http://www.w3.org/2000/svg"><text style="line-height:1.25;-inkscape-font-specification:'Century Gothic Bold'" x="1.953" y="21.178" transform="scale(.99582 1.0042)" font-weight="700" font-size="30.255" font-family="Century Gothic" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-width=".756"><tspan x="1.953" y="21.178" style="-inkscape-font-specification:'Century Gothic Bold'" font-size="22.745">M</tspan></text></symbol><symbol viewBox="0 0 192 191.99999" id="mocha" xmlns="http://www.w3.org/2000/svg"><title>Mocha Logo</title><g transform="translate(-354.75 -262.42) scale(4.835)" fill="#a1887f"><path d="M103.6 69.6c0-.5-.4-1-1-1H83.8c-.5 0-1 .4-1 1 0 3.4.5 15.1 5.5 20.8.2.2.4.3.7.3h8.4c.3 0 .5-.1.7-.3 5-5.6 5.5-17.3 5.5-20.8zm-7.4 18.2h-5.9c-.3 0-.5-.1-.7-.3-3.4-4-3.8-12-3.9-14.8 0-.5.4-1 1-1h13.2c.5 0 1 .4 1 1 0 2.8-.5 10.7-3.9 14.8-.3.2-.5.3-.8.3zM95.1 66.6s3.6-2.1 1.4-5.9c-1.3-2-1.9-3.7-1.4-4.4-1.3 1.6-3.5 3.3-1.1 6.9.8.9 1.2 2.8 1.1 3.4zM91.1 66.9s2.4-1.4.9-4c-.9-1.3-1.3-2.5-.9-2.9-.9 1.1-2.3 2.2-.7 4.7.5.5.7 1.8.7 2.2z"/><path d="M99.3 78.5c-.4 2.7-1.2 5.8-2.9 7.8-.2.2-.4.3-.6.3h-5c-.2 0-.5-.1-.6-.3-1.2-1.5-2-3.5-2.5-5.6 0 0 5.8.8 9.1-.4 2.4-.9 2.5-1.8 2.5-1.8z"/></g></symbol><symbol viewBox="0 0 24 24" id="movie" xmlns="http://www.w3.org/2000/svg"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4h-4z" fill="#ff9800"/></symbol><symbol viewBox="0 0 24 24" id="music" xmlns="http://www.w3.org/2000/svg"><path d="M16 9V7h-4v5.5c-.42-.31-.93-.5-1.5-.5A2.5 2.5 0 0 0 8 14.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V9h3m-4-7a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" fill="#ef5350"/></symbol><symbol viewBox="0 0 24 24" id="mxml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#ffa726"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#ab47bc" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#26c6da" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#e53935" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#9ccc65" stroke-width="12.914"/></symbol><symbol viewBox="0 0 24 24" id="nim" xmlns="http://www.w3.org/2000/svg"><path d="M4.464 15.75L2.288 3.78l5.985 7.617L12.08 3.78l3.809 7.617 5.985-7.617-2.177 11.97H4.464m15.234 3.264a1.088 1.088 0 0 1-1.088 1.088H5.553a1.088 1.088 0 0 1-1.089-1.088v-1.089h15.234z" stroke-width="1.088" fill="#ffca28"/></symbol><symbol viewBox="0 0 500 500" id="nix" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.965 36.302)" stroke-width=".395"><path d="M135.59 415.7c0-.295-2.752-5.283-6.116-11.084-3.364-5.801-6.116-10.776-6.116-11.055s9.514-16.889 21.143-36.912c11.629-20.022 21.323-36.798 21.542-37.279.346-.76-1.608-4.363-14.896-27.466-8.412-14.625-15.294-26.785-15.294-27.023 0-.5 24.46-43.501 25.206-44.31.414-.45.592-.384 1.078.395.32.513 16.876 29.256 36.791 63.87 62.62 108.85 74.852 130.01 75.41 130.46.3.242.544.554.544.694 0 .14-11.836.21-26.302.154-23.023-.09-26.313-.175-26.393-.694-.11-.714-27.662-48.825-28.86-50.392-.746-.978-.906-1.035-1.426-.51-.688.696-28.954 49.323-29.49 50.733l-.365.96h-13.229c-10.896 0-13.229-.095-13.229-.538zm167.58-125.61c-.134-.216 1.188-2.863 2.938-5.882 6.924-11.944 84.291-145.75 96.491-166.88 7.143-12.371 13.142-22.465 13.333-22.433.363.062 25.861 43.105 25.861 43.655 0 .174-6.761 11.952-15.026 26.173-8.46 14.557-14.932 26.104-14.81 26.421.185.483 4.564.564 30.213.564h29.996l.958 1.48c.526.814 3.296 5.547 6.155 10.518 2.859 4.971 5.45 9.29 5.756 9.597.706.705.704.724-.16 1.572-.395.388-3.36 5.323-6.587 10.965-3.228 5.643-6.056 10.387-6.285 10.543-.23.156-19.695.171-43.256.034l-42.84-.249-.804 1.15c-.441.632-7.504 12.736-15.696 26.897l-14.892 25.747H339.03c-8.517 0-20.015.116-25.55.259-6.55.168-10.15.121-10.309-.135zM169.42 132.23c-56.373-.055-102.5-.182-102.5-.282 0-.1 5.617-10.132 12.481-22.294l12.481-22.112h30.332c27.113 0 30.332-.065 30.332-.611 0-.336-6.659-12.228-14.797-26.427-8.139-14.199-14.797-25.917-14.797-26.04 0-.123 2.682-4.853 5.96-10.51s6.003-10.578 6.055-10.934c.086-.586 1.376-.648 13.572-.648 7.413 0 13.463.143 13.446.317-.017.174.222.707.531 1.184.31.476 9.763 16.937 21.007 36.578 11.244 19.64 20.71 36.022 21.036 36.4.554.647 2.549.691 31.428.691h30.837l12.896 22.145c7.093 12.18 12.8 22.301 12.682 22.492-.118.19-4.776.303-10.352.249-5.575-.054-56.26-.143-112.63-.198z" fill="#5075c1"/><path d="M25.289 203.14c-6.098 10.563-6.69 11.711-6.225 12.078.283.224 3.18 5.044 6.44 10.712 3.261 5.668 6.017 10.355 6.124 10.417.106.061 13.585.153 29.95.204 16.367.052 29.994.23 30.285.399.472.273-1.08 3.094-14.637 26.574L62.06 289.793l12.907 21.865c7.1 12.026 12.982 21.906 13.068 21.956.086.05 23.257-39.831 51.492-88.624 11.352-19.617 21.214-36.64 30.37-52.442 23.308-40.452 30.68-53.468 30.73-54.132-1.097-.11-6.141-.187-13.006-.216-3.945-.01-7.82-.02-12.75-.002l-25.341.092-15.42 26.706c-14.256 24.693-15.445 26.663-16.278 26.86l-.024.037c-.011.003-1.62-.001-1.825 0-4.29.062-20.453.063-40.226-.01-22.632-.082-41.615-.125-42.183-.096-.568.03-1.147-.03-1.29-.132-.142-.102-3.29 5.066-6.996 11.485zm205.16-190.3c-.123.149 5.62 10.392 12.761 22.763 12.199 21.131 89.393 155.03 96.276 167 1.502 2.613 2.92 4.803 3.443 5.348.9-1.249 3.531-5.63 7.954-13.219a1342.88 1342.88 0 0 1 10.049-17.76l6.606-11.443c.692-1.403.754-1.818.653-2.117-.162-.48-6.904-12.332-14.982-26.337-8.078-14.005-14.824-25.849-14.991-26.32a.73.73 0 0 1-.009-.366l-.426-.913L359.42 72.5c3.69-6.307 6.425-11.042 9.47-16.29 9.159-15.948 12.037-21.189 11.896-21.55-.126-.324-2.7-4.83-5.72-10.017-3.021-5.185-5.845-10.148-6.275-11.026-.483-.987-.734-1.364-1.1-1.456-.054.014-.083.018-.145.035-.42.112-5.454.195-11.189.185-5.734-.01-11.22.024-12.188.073l-1.76.089-14.997 25.978c-12.824 22.212-15.084 25.964-15.595 25.883-.024-.004-.15-.189-.235-.301-.109.066-.2.09-.272.05-.255-.148-7.143-11.902-15.306-26.119l-14.36-25.016c-.115-.186-.444-.744-.457-.752-.477-.275-50.502.287-50.737.57zm-18.646 283.09c-.047.109-.026.262.042.48.329 1.05 25.338 43.735 25.772 43.985.207.119 14.178.239 31.05.266 26.651.044 30.75.152 31.234.832.308.43 9.988 17.214 21.513 37.296s21.152 36.627 21.394 36.767c.242.14 5.927.243 12.633.23 6.706-.013 12.401.099 12.657.246.132.076.382-.141.852-.795l6.008-10.406c5.234-9.065 6.62-11.684 6.294-11.888-.575-.36-15.597-26.643-23.859-41.482-3.09-5.45-5.37-9.516-5.441-9.774-.195-.712-.065-.822 1.156-.98 1.956-.252 57.397-.057 58.07.205.238.092.79-.569 2.594-3.497 1.866-3.067 5.03-8.524 11-18.866 7.22-12.505 13.044-22.784 12.942-22.843-.102-.059-.771-.051-1.489.016l-.046.001c-4.452.204-33.918.203-149.74.025-38.96-.06-69.786-.09-71.912-.072-1.121.01-2.095.076-2.66.172a.25.25 0 0 0-.062.083z" fill="#7db7e1"/></g></symbol><symbol viewBox="0 0 24 24" id="nodejs" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.85c-.27 0-.55.07-.78.2l-7.44 4.3c-.48.28-.78.8-.78 1.36v8.58c0 .56.3 1.08.78 1.36l1.95 1.12c.95.46 1.27.47 1.71.47 1.4 0 2.21-.85 2.21-2.33V8.44c0-.12-.1-.22-.22-.22H8.5c-.13 0-.23.1-.23.22v8.47c0 .66-.68 1.31-1.77.76L4.45 16.5a.26.26 0 0 1-.11-.21V7.71c0-.09.04-.17.11-.21l7.44-4.29c.06-.04.16-.04.22 0l7.44 4.29c.07.04.11.12.11.21v8.58c0 .08-.04.16-.11.21l-7.44 4.29c-.06.04-.16.04-.23 0L10 19.65c-.08-.03-.16-.04-.21-.01-.53.3-.63.36-1.12.51-.12.04-.31.11.07.32l2.48 1.47c.24.14.5.21.78.21s.54-.07.78-.21l7.44-4.29c.48-.28.78-.8.78-1.36V7.71c0-.56-.3-1.08-.78-1.36l-7.44-4.3c-.23-.13-.5-.2-.78-.2M14 8c-2.12 0-3.39.89-3.39 2.39 0 1.61 1.26 2.08 3.3 2.28 2.43.24 2.62.6 2.62 1.08 0 .83-.67 1.18-2.23 1.18-1.98 0-2.4-.49-2.55-1.47a.226.226 0 0 0-.22-.18h-.96c-.12 0-.21.09-.21.22 0 1.24.68 2.74 3.94 2.74 2.35 0 3.7-.93 3.7-2.55 0-1.61-1.08-2.03-3.37-2.34-2.31-.3-2.54-.46-2.54-1 0-.45.2-1.05 1.91-1.05 1.5 0 2.09.33 2.32 1.36.02.1.11.17.21.17h.97c.05 0 .11-.02.15-.07.04-.04.07-.1.05-.16C17.56 8.82 16.38 8 14 8z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 300 300" id="nodemon" xmlns="http://www.w3.org/2000/svg"><title>nodemon</title><path d="M149.868 20.62c-2.124 0-4.25.55-6.154 1.648L41.899 81.083a12.306 12.306 0 0 0-6.15 10.652v117.633a12.29 12.29 0 0 0 6.152 10.646l101.815 58.766h.001a12.282 12.282 0 0 0 12.291 0l101.84-58.766a12.29 12.29 0 0 0 6.153-10.652V91.738a12.31 12.31 0 0 0-6.146-10.652L156.015 22.27a12.302 12.302 0 0 0-6.153-1.648zM83.303 70.93s11.789 33.031 35.477 31.934l27.74-15.961a7.348 7.348 0 0 1 3.414-.99h.641a7.233 7.233 0 0 1 3.404.99l27.738 15.961c23.69 1.094 35.475-31.934 35.475-31.934 5.233 23.154 1.06 38.641-5.924 48.942l4.541 2.614h.002c2.321 1.327 3.734 3.795 3.737 6.49l-.12 95.811a3.724 3.724 0 0 1-1.855 3.227 3.624 3.624 0 0 1-3.735 0L177.1 206.971c-2.311-1.363-3.742-3.818-3.742-6.48v-44.763a7.44 7.44 0 0 0-3.737-6.465l-15.642-9.01a7.28 7.28 0 0 0-3.715-1.01 7.378 7.378 0 0 0-3.742 1.01l-15.648 9.01c-2.316 1.323-3.729 3.798-3.729 6.467v44.762c0 2.663-1.413 5.1-3.738 6.48l-36.748 21.041a3.571 3.571 0 0 1-3.71 0c-1.173-.65-1.864-1.887-1.864-3.224l-.137-95.812a7.483 7.483 0 0 1 3.74-6.49l4.541-2.615c-6.982-10.302-11.16-25.79-5.925-48.942z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 990 990" id="npm" xmlns="http://www.w3.org/2000/svg"><defs><style>.hncls-1{fill:#cb3837}.cls-2{fill:#fff}</style></defs><title>n</title><path class="hncls-1" d="M113.26 876.74V113.27h763.47v763.47zm143.59-620.4v476.18h240.61V355.63h140.21v376.96h95.457V256.34z" fill="#e53935" stroke-width=".771"/></symbol><symbol id="nunjucks" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.host0{fill:#388e3c}</style><path class="host0" d="M11.2 21.1H8.1l-2.3-7.9v7.9H2.7V2.9h3.1l2.3 7.4V2.9h3.1zM21.3 19.2c0 1-.8 1.9-1.9 1.9h-4.8c-1 0-1.9-.8-1.9-1.9v-3.8l3.2-.7V18h2.3V7.2h3.1v12z"/></symbol><symbol viewBox="0 0 150 150.00001" id="ocaml" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.76136 0 0 .76136 11.616 19.98)"><path d="M83.02 101.645l.023-.062c-.035-.159-.047-.195-.024.062z" fill="none" stroke-width="1.028"/><linearGradient id="hpa" gradientUnits="userSpaceOnUse" x1="-696.735" y1="97.7" x2="-696.735" y2="142.997" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M82.313 138.79c-.471-1.004-1.904-3.621-2.624-4.46-1.562-1.828-1.927-1.966-2.386-4.275-.799-4.02-2.913-11.31-5.405-16.341-1.286-2.596-3.426-4.777-5.385-6.66-1.71-1.652-5.565-4.431-6.237-4.294-6.296 1.257-8.249 7.432-11.21 12.323-1.638 2.705-3.374 5.007-4.665 7.885-1.192 2.646-1.087 5.577-3.128 7.849-2.093 2.333-3.454 4.814-4.48 7.829-.194.574-.747 6.596-1.348 8.015l9.357-.659c8.719.594 6.2 3.936 19.81 3.208l21.487-.665c-.666-1.97-1.584-4.25-1.938-4.991-.599-1.248-1.352-3.69-1.848-4.763z" fill="url(#hpa)" stroke-width="1.028"/><linearGradient id="hpb" gradientUnits="userSpaceOnUse" x1="-666.972" y1="142.12" x2="-666.972" y2="142.12" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><linearGradient id="hpc" gradientUnits="userSpaceOnUse" x1="-675.228" y1="-1.28" x2="-675.228" y2="142.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M109.553 94.296c-1.652 1.193-4.88 4.06-11.902 5.145-3.152.487-6.1.527-9.335.365-1.584-.076-3.077-.157-4.665-.177-.936-.008-4.074-.107-3.919.193l-.349.871c.054.287.169 1.004.2 1.177.129.704.165 1.265.192 1.912.048 1.331-.11 2.719-.043 4.062.141 2.787 1.175 5.326 1.306 8.137.143 3.13 1.69 6.442 3.188 8.998.569.973 1.434 1.084 1.811 2.283.442 1.373.024 2.83.239 4.293.842 5.675 2.477 11.606 5.032 16.728.018.043.038.09.06.128 3.156-.53 6.318-1.665 10.418-2.271 7.517-1.115 17.972-.54 24.688-1.17 16.993-1.597 26.216 6.97 41.478 3.459V22.459c0-11.84-9.594-21.438-21.435-21.438H19.239C7.4 1.021-2.197 10.62-2.197 22.458v46.774c3.067-1.11 7.479-7.635 8.861-9.222 2.419-2.775 2.858-6.315 4.062-8.544 2.743-5.078 3.215-8.57 9.451-8.57 2.907 0 4.061.67 6.027 3.31 1.368 1.834 3.731 5.224 4.837 7.49 1.277 2.615 3.357 6.153 4.272 6.867.677.53 1.35.928 1.976 1.163 1.012.38 1.848-.316 2.525-.855.863-.687 1.235-2.088 2.035-3.957 1.152-2.696 2.408-5.926 3.122-7.054 1.237-1.949 1.658-4.261 2.993-5.381 1.97-1.652 4.54-1.768 5.246-1.908 3.957-.781 5.755 1.906 7.704 3.645 1.276 1.138 3.019 3.432 4.256 6.507.967 2.4 2.199 4.622 2.714 6.008.497 1.339 1.725 3.484 2.453 6.055.661 2.336 2.43 4.125 3.102 5.235 0 0 1.029 2.882 7.285 5.516 1.357.572 4.1 1.501 5.736 2.096 2.718.988 5.351.86 8.704.458 2.391 0 3.686-3.462 4.772-6.234.643-1.639 1.259-6.334 1.678-7.667.406-1.297-.544-2.3.265-3.437.946-1.327 1.508-1.399 2.054-3.129 1.172-3.704 7.95-3.89 11.761-3.89 3.176 0 2.772 3.083 8.16 2.028 3.086-.605 6.059.398 9.335 1.265 2.758.732 5.352 1.566 6.906 3.385 1.005 1.178 3.5 7.08.958 7.331.244.3.423.84.88 1.135-.566 2.226-3.03.64-4.4.355-1.845-.383-3.147.057-4.952.856-3.085 1.374-7.598 1.214-10.286 3.452-2.281 1.898-2.277 6.133-3.34 8.507-.002-.001-2.955 7.6-9.402 12.248z" fill="url(#hpc)" stroke-width="1.028"/><linearGradient id="hpd" gradientUnits="userSpaceOnUse" x1="-735.137" y1="90.833" x2="-735.137" y2="141.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M38.247 105.09c-1.467-.15-2.83-.317-4.256-.605-2.662-.536-5.57-1.06-8.193-1.688-1.592-.385-6.895-2.263-8.048-2.792-2.702-1.246-4.496-4.63-6.609-4.282-1.348.22-2.662.682-3.5 2.042-.685 1.11-.917 3.016-1.391 4.294-.55 1.485-1.5 2.87-2.331 4.284-1.53 2.595-4.282 4.941-5.468 7.469-.239.52-.45 1.101-.649 1.708V144.415a48.57 48.57 0 0 1 4.45.96c11.955 3.19 14.872 3.46 26.598 2.119l1.1-.146c.897-1.867 1.59-8.227 2.171-10.195.454-1.51 1.077-2.712 1.313-4.253.223-1.463-.02-2.858-.146-4.188-.329-3.332 2.427-4.522 3.742-7.384 1.186-2.589 1.871-5.535 2.853-8.181.941-2.54 2.41-6.13 4.918-7.408-.305-.355-5.237-.518-6.554-.65z" fill="url(#hpd)" stroke-width="1.028"/></g></symbol><symbol viewBox="0 0 24 24" id="pdf" xmlns="http://www.w3.org/2000/svg"><path d="M14 9h5.5L14 3.5V9M7 2h8l6 6v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m4.93 10.44c.41.9.93 1.64 1.53 2.15l.41.32c-.87.16-2.07.44-3.34.93l-.11.04.5-1.04c.45-.87.78-1.66 1.01-2.4m6.48 3.81c.18-.18.27-.41.28-.66.03-.2-.02-.39-.12-.55-.29-.47-1.04-.69-2.28-.69l-1.29.07-.87-.58c-.63-.52-1.2-1.43-1.6-2.56l.04-.14c.33-1.33.64-2.94-.02-3.6a.853.853 0 0 0-.61-.24h-.24c-.37 0-.7.39-.79.77-.37 1.33-.15 2.06.22 3.27v.01c-.25.88-.57 1.9-1.08 2.93l-.96 1.8-.89.49c-1.2.75-1.77 1.59-1.88 2.12-.04.19-.02.36.05.54l.03.05.48.31.44.11c.81 0 1.73-.95 2.97-3.07l.18-.07c1.03-.33 2.31-.56 4.03-.75 1.03.51 2.24.74 3 .74.44 0 .74-.11.91-.3m-.41-.71l.09.11c-.01.1-.04.11-.09.13h-.04l-.19.02c-.46 0-1.17-.19-1.9-.51.09-.1.13-.1.23-.1 1.4 0 1.8.25 1.9.35M8.83 17c-.65 1.19-1.24 1.85-1.69 2 .05-.38.5-1.04 1.21-1.69l.48-.31m3.02-6.91c-.23-.9-.24-1.63-.07-2.05l.07-.12.15.05c.17.24.19.56.09 1.1l-.03.16-.16.82-.05.04z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="perl" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c-1 0-3 1-3 2 0 2 3 2 3 2v-1a1 1 0 0 1-1-1 1 1 0 0 1 1-1v-1m0 5s-4-.5-4-2.5c0-3 3-3.75 4-3.75V11.5c-1 0-5 1.5-5 4.5 0 4 5 4 5 4v-1M10.07 7.03l1.19.53c.43-2.44 1.58-4.06 1.58-4.06-.43 1.03-.71 1.88-.89 2.55C13.16 3.55 15.61 2 15.61 2a15.916 15.916 0 0 0-2.64 3.53c1.58-1.68 3.77-2.78 3.77-2.78-2.69 1.72-3.9 4.45-4.2 5.21l.55.08c0 .52 0 1 .25 1.38C14.1 11.31 18 11.47 18 16s-4.03 6-6.17 6C9.69 22 5 21.03 5 16s4.95-5.07 5.83-7.08c.12-.38-.76-1.89-.76-1.89z" fill="#9575cd"/></symbol><symbol viewBox="0 0 24 24" id="php" xmlns="http://www.w3.org/2000/svg"><path d="M12 18.08c-6.63 0-12-2.72-12-6.08s5.37-6.08 12-6.08S24 8.64 24 12s-5.37 6.08-12 6.08m-5.19-7.95c.54 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.58 1.09-.28.22-.71.33-1.29.33h-.87l.53-2.76h.99m-3.5 5.55h1.44l.34-1.75h1.23c.54 0 .98-.06 1.33-.17.35-.12.67-.31.96-.58.24-.22.43-.46.58-.73.15-.26.26-.56.31-.88.16-.78.05-1.39-.33-1.82-.39-.44-.99-.65-1.82-.65H4.59l-1.28 6.58m7.25-8.33l-1.28 6.58h1.42l.74-3.77h1.14c.36 0 .6.06.71.18.11.12.13.34.07.66l-.57 2.93h1.45l.59-3.07c.13-.62.03-1.07-.27-1.36-.3-.27-.85-.4-1.65-.4h-1.27L12 7.35h-1.44M18 10.13c.55 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.57 1.09-.29.22-.72.33-1.3.33h-.85l.5-2.76h1m-3.5 5.55h1.44l.34-1.75h1.22c.55 0 1-.06 1.35-.17.35-.12.65-.31.95-.58.24-.22.44-.46.58-.73.15-.26.26-.56.32-.88.15-.78.04-1.39-.34-1.82-.36-.44-.99-.65-1.82-.65h-2.75l-1.29 6.58z" fill="#1E88E5"/></symbol><symbol viewBox="0 0 79 78" id="postcss" xmlns="http://www.w3.org/2000/svg"><title>postcss-logo-symbol</title><g transform="translate(5.48 5.52) scale(.85425)" fill="#e53935" fill-rule="evenodd" stroke="#e53935" stroke-width="1.519"><path d="M15.447 32.623c.106.08.29.132.106.29-.132.184-.29.342-.395.553-.105.185-.184.237-.342.106.21-.343.42-.66.63-.95zM68.342 60.24c0 .078.026.13.026.21.053-.105.053-.158.08-.21zm0 .236v-.026zm-5.368 10.277l-4.58-25.402c-.078-.025-.183-.077-.368-.13.053.105.08.184.106.263.13-.026.184-.026.236-.052 0-.026 0-.052.027-.08l4.58 25.404zm-4.737-31.12c-.026.078-.026.158-.026.237 0-.08 0-.16.028-.238zm.026.526c-.026 0-.026 0-.052-.028v.026c.028.026.028.026.054 0zm-.052.21v-.185c-.077.026-.156.026-.262.053.132.05.264.078.264.13z"/><path d="M78.71 33.967c-.052-1.028-.078-2.056-.184-3.083-.184-1.397-.368-2.82-.684-4.19-.237-1.133-.63-2.214-1.026-3.294-.5-1.265-1-2.556-1.632-3.768-1.026-1.95-2.368-3.69-3.605-5.508-.818-1.16-1.87-2.108-2.66-3.294-.447-.685-1.105-1.264-1.763-1.79-1.053-.845-2.158-1.61-3.263-2.347a32.525 32.525 0 0 0-2.58-1.634c-.71-.397-1.473-.713-2.21-1.056-.842-.395-1.658-.87-2.605-1.054-.238-.05-.448-.13-.685-.21-.605-.21-1.184-.447-1.79-.632-.92-.29-1.815-.632-2.763-.87C50.342 1 49.394.843 48.446.71 47.394.555 46.316.5 45.262.397a26.83 26.83 0 0 0-2.026-.184C42.236.16 41.21.16 40.21.134c-.5-.027-1.026-.08-1.526-.053-.763.026-1.526.105-2.29.21-.736.08-1.473.21-2.183.317-.867.105-1.735.158-2.604.264-.816.106-1.658.264-2.473.396-.29.053-.58.158-.87.21-.63.132-1.288.185-1.92.396-1.13.344-2.263.74-3.368 1.16-1.027.422-2.027.87-3 1.397-1 .552-1.948 1.21-2.895 1.844a45.325 45.325 0 0 0-2.66 1.923c-.84.66-1.63 1.397-2.394 2.135-.42.42-.763.922-1.158 1.396-.657.765-1.315 1.502-1.947 2.293-.524.66-1 1.344-1.5 2.03-.893 1.21-1.656 2.502-2.366 3.794-.29.527-.553 1.054-.816 1.58-.395.79-.816 1.555-1.184 2.372-.264.554-.474 1.16-.632 1.766-.367 1.292-.736 2.61-1.078 3.9-.316 1.16-.395 2.372-.42 3.558-.027 1.054.078 2.082.183 3.136.027.264-.13.58.184.79-.105.29-.026.45.13.5-.182.29.08.476-.024.74-.027.052.08.157.13.236 0 .08-.025.185 0 .264.028.237.133.474.133.738 0 .184.157.395.21.58.026.078 0 .21-.053.263-.158.184-.132.342.105.448.133.342.08.5.054.66.052.236-.027.315 0 .368.21.422.29.896.315 1.37 0 .106.053.212.106.343.026 0 0 .5 0 .5.13-.078.237-.104.368-.157.08.342.158.66.263.95.132.21.132.314.08.34.105.474.157.922.34 1.37 0-.5-.05-1-.13-1.475.368.132.684.263.895.263.027-.08.053-.184.08-.237-.158-.157-.29-.394-.448-.552.053.21 0 .29 0 .37-.105-.054-.237-.107-.368-.16.105-.13.21-.263.368-.42 0-.238-.13-.45-.5-.423.158-.052.316-.13.5-.184.29-.157-.026-.447-.026-.816.026-.447-.237-.895-.316-1.37-.132-.737-.105-1.844-.184-2.582-.158-.132-.29.21-.316.237.08.632.158 1.264.21 1.897-.157-.527-.263-1.107-.394-1.74-.027.185-.053.264-.053.37-.13.13-.026.29.053.474-.184-.08-.395-.052-.395-.052v.738c-.262-.264-.34-.474-.473-.66-.052-.21-.08-.42-.13-.63.05-.133 0-.212 0-.29a15.968 15.968 0 0 1-.08-.634c.026-.026-.026-.42-.026-.42.21.025.343.05.474.05-.263-.34-.08-.552.027-.763.053-.106.237-.13.29-.238.21-.395.553-.71.553-1.212 0-.237.08-.5.105-.738.053-.448.105-.896.13-1.344.054-.58 0-1.16.133-1.713.212-.92.475-1.843.764-2.766.21-.66.448-1.29.71-1.95.395-1.028.764-2.056 1.264-3.03.71-1.424 1.526-2.794 2.316-4.19.5-.87 1.026-1.687 1.58-2.53.525-.817 1.05-1.66 1.657-2.425a21.452 21.452 0 0 1 2.79-2.978c1.053-.948 2.053-1.923 3.184-2.793a32.218 32.218 0 0 1 4.685-3.005c1.343-.71 2.737-1.266 4.132-1.793.895-.342 1.868-.5 2.79-.79 1.052-.343 2.105-.5 3.21-.527.71-.027 1.395-.106 2.105-.185.632-.05 1.263-.104 1.948-.183-.08.105-.106.158-.132.21-.288.422-.604.844-.894 1.265-.237.343-.5.712-.737 1.054-.422.555-.87 1.108-1.264 1.688-.605.87-1.158 1.766-1.79 2.635-.63.843-1.315 1.634-1.973 2.45-.868 1.134-1.684 2.293-2.552 3.426-.79 1.08-1.63 2.11-2.394 3.19-.684.947-1.29 1.95-1.948 2.923-.973 1.45-1.947 2.872-2.92 4.322a271.93 271.93 0 0 1-2.316 3.294c-.053.08-.132.104-.21.157-.21.342-.21.527-.29.685-.21.395-.42.79-.658 1.16-.132.21-.316.394-.474.605-.026-.316.42-.474.21-.87-.13.212-.263.396-.394.607l-.316.63c.105.08.29.133.105.29-.08.133-.158.29-.237.423a.954.954 0 0 0 .29-.264c0 .29-.158.526-.29.763-.105.21-.368.37-.552.527.026.027.21.106.237.132.237-.08.316-.21.343-.132.08-.105.158-.184.184-.263.104-.264.262-.474.525-.58.106-.053.184-.132.263-.21.79-.818 1.606-1.608 2.316-2.478 1.106-1.345 2.106-2.74 3.16-4.11.446-.58.973-1.16 1.446-1.714.078.606.026 1.185 0 1.74-.08.974-.132 1.95-.21 2.95-.027.395 0 .79-.027 1.186 0 .105-.08.184-.08.29 0 .263.08.553.08.817-.08.975-.186 1.923-.265 2.898-.027.21.078.422.13.607-.13 1.422.16 2.925-.078 4.427.184-.29.237-.474.237-.658.025-.158 0-.316 0-.5v-.264c.025-.475.13-.975.078-1.45-.053-.527-.053-1.027.053-1.528.053-.21-.026-.474.106-.738v.395c-.026 1.5.027 3.003-.183 4.505-.027.132.08.37-.21.343-.238.474.052.817-.21 1.08-.054.053.05.29.077.448-.106.317-.106.317.052.343.026.58.08 1.106.105 1.66.42-1 .21-2.03.396-3.058.026.422.053.844.026 1.29 0 .687-.026 1.345-.052 2.03 0 .132-.027.264-.053.396-.08.37-.105.738-.237 1.08-.105.264-.052.66-.052.975v1.003c.105.448-.027.685.052.948-.08.265-.105.344-.08.423l.08.395c.527-.053.29.343.5.553-.158.212-.105.29-.105.397 0 .237-.025.448-.052.685 0 .606-.026 1.212-.026 1.792 0 .08.026.157.026.236 0 .054-.026.74-.026.74.053.078 0 .157-.08.236-.025 0-.104-3.347-.104-3.347h-.395c-.052 1.58.08 3.003-.21 4.48-.316.025-.42.078-.764.078-.816 0-1.632 0-2.448.026-.974 0-1.92.026-2.895.026-.472 0-.972.054-1.446.054-.632 0-1.29-.08-1.92-.08-.975 0-1.922.08-2.896.106-.71.026-1.42.026-2.13.053-.475.025-.95.05-1.422.104-.21.026-.395.105-.658.184-.08 0-.263-.026-.42 0-.265.053-.5.21-.765.264-.395.08-.5.184-.448.58v.263c-.026.052.58-.08.58-.08-.054 0-.08.158-.16.29.212-.08.343-.132.475-.184.395.185.737.08 1.052.16 1.026.262 2.078.37 3.13.473.685.053 1.343.08 2.027.105.973.053 1.947.106 2.92.106.816 0 1.606-.08 2.42-.08 1.13 0 2.264.052 3.395.08.237 0 .5-.028.763-.028h1.92c1.712-.052 3.422-.08 5.133-.13.975-.028 1.975-.08 2.948-.107l3-.08c1.158-.026 2.316-.026 3.448-.05.868 0 1.71-.03 2.58-.055.972-.026 1.972-.105 2.946-.157.527-.027 1.054-.08 1.58-.132.632-.052 1.29-.13 1.92-.157.948-.054 1.922-.08 2.87-.133 1.184-.078 2.368-.183 3.578-.21 1.106-.052 2.237-.026 3.343-.052.974-.027 1.948-.08 2.948-.106l1.66-.08s1.104-.026 1.657-.08c.947-.052 1.894-.157 2.842-.183.604-.027 1.21 0 1.815-.027.973-.026 1.973-.08 2.947-.08.367 0 .762.054 1.236.08-.21.185-.342.29-.5.422.105.026.21.08.316.132a.71.71 0 0 1-.42.13c-.054.133-.107.186-.16.45h.474c-.184 0-.342.237-.526.395-.21-.054-.395 0-.5.29.184.104.158.183.132.29-.316.104-.553.21-.42.552-.107.052-.238.105-.37.184-.13.21-.368.263-.316.553.106.025.21.08.29.104-.132.053-.263.132-.395.184-.473.29-.262.422-.157.554-.08.053-.158.105-.237.132.052.237.13.29.157.29a9.3 9.3 0 0 0-.395.316c-.08.237-.185.342-.29.5s-.158.37-.29.527c-.552.607-.947 1.32-1.657 1.793-.264.185-.5.422-.737.66-.474.447-.895.948-1.395 1.37a29.595 29.595 0 0 1-2.052 1.554 151.56 151.56 0 0 1-2.604 1.792c-.474.315-1 .552-1.5.842s-.974.554-1.474.843c-.316.21-.606.5-.948.66-.868.37-1.79.685-2.684 1.028-.87.37-1.5.685-2.158.922-.605.21-1.237.37-1.868.5-.21.054-.448 0-.685.027-.448.08-.895.186-1.343.238-1.158.158-2.316.264-3.473.422-.685.08-1.343.21-2.027.29-.473.026-.973-.026-1.447-.026-.342 0-.71.08-1.053.027-.552-.08-1.105-.21-1.658-.316-.13-.026-.316-.08-.42-.026-.21.106-.396-.052-.607 0-.13.027-.262-.08-.394-.08-.106-.025-.238.028-.37 0-.29-.078-.552-.183-.87-.157-.313.026-.63-.132-.97-.21-.475-.106-.92-.21-1.396-.317a2.38 2.38 0 0 1-.525-.237c-.685 0-1.133-.026-1.554-.185-.368-.13-.71-.315-1.105-.262-.104.026-.183-.026-.29-.026-.08-.106-.157-.317-.235-.317-.526.027-.842-.42-1.29-.553-.236-.08-.42-.343-.657-.422-.58-.237-1.052-.737-1.71-.816-.21-.027-.42-.132-.658-.21.08.104.13.183.21.262-.763-.37-1.473-.79-2.184-1.186-.104-.026-.183-.13-.262-.184l-.71-.474c-.395.08-.553-.08-.66-.132-.71-.5-1.525-.817-2.21-1.37-.29-.238-.63-.396-.84-.686-.37-.448-.817-.764-1.317-1.027-.394-.21-.762-.448-1.13-.685-.185-.132-.37-.29-.37-.58 0-.185-.078-.37-.315-.264-.105-.158-.21-.342-.342-.395-.316-.13-.526-.37-.763-.58s-.42-.5-.71-.605c-.527-.21-.843-.658-1.158-1.027-.738-.87-1.396-1.82-2.08-2.74-.053-.08-.158-.133-.237-.212.105.29.237.527.368.79-.262-.105-.446-.29-.604-.474-.027.027 1.815 3.057 1.815 3.057.16.237.29.475.448.712a.813.813 0 0 1-.79-.422c-.236-.42-.5-.684-1.026-.63a4.588 4.588 0 0 1-.13-.58c-.107 0-.185 0-.37-.027.37.58.685 1.08 1.027 1.66-.133-.08-.21-.132-.265-.158.473.5.815 1.133 1.42 1.45.132.605.816.895.974 1.475-.13-.027-.238-.053-.37-.08-.21-.263-.447-.526-.683-.816.052.184.13.342.236.474.316.395.606.79.974 1.133.132.134.316.187.316.424.21.105.29.13.368.13.054.16-.025.397.29.344.21.395.42.395.71.264.343.343.528.37.764.16 0 .13.026.262.026.368.105-.053.08-.132.08-.264.13.105.21.158.262.21.263.37.5.712.868 1.002.5.422.948.87 1.42 1.265.922.765 1.95 1.398 2.975 1.977 1.264.712 2.475 1.476 3.764 2.16 1.552.818 3.21 1.372 4.92 1.767.632.132 1.237.263 1.87.42.55.16 1.104.397 1.657.528.842.185 1.71.343 2.552.5.183.027.37.054.58.08.235.053.524-.053.577.027.132.21.237.104.395.078.184-.053.395-.053.605-.053.737.026 1.447.184 2.184.132.16 0 .396-.133.528.13.236-.105.368-.105.473-.13.028.236 0 .236-.05.262-.054.026-.133.053-.238.132.947.184 1.842.21 2.63 0 1.37.105 2.554-.053 3.686-.448.105.132.184.316.342.053.052-.08.184-.107.29-.133.236-.053.526-.158.736-.08.238.08.317-.13.5-.13.317 0 .606-.027.896-.08.158-.026.316-.105.5-.158a1.285 1.285 0 0 0-.58-.133c.317-.158.606-.29.896-.42-.053.078-.106.183-.21.183h.367c-.08 0-.185.237-.316.395.946-.237 1.814-.448 2.657-.66-.29-.552.315-.367.526-.684-.263.08-.526.158-.79.21.895-.447 1.816-.842 2.71-1.237-.13.158-.29.237-.525.37.158.025.263.025.342.05.42.133.316-.262.447-.5.5 0 .71-.078.947-.158.263-.08.526-.158.79-.263.42-.184.815-.42 1.236-.63.08-.028.21 0 .316 0 .29-.186.394-.344.473-.318.37.053.63-.08.736-.42.184-.133.316-.238.447-.318.578-.316 1.13-.632 1.71-.948.21 0 .316 0 .368-.027.344-.16.66-.342.975-.527a2.258 2.258 0 0 1-.263-.13c.262-.054.34-.08.5-.133.63-.74 1.5-1.24 2.157-1.82.29-.026.29-.105.29-.157.104-.132.21-.29.34-.396.58-.527 1.21-.975 1.737-1.528a37.16 37.16 0 0 0 2.184-2.374c.63-.738 1.264-1.475 1.79-2.292.737-1.133 1.368-2.293 2.026-3.48.474-.842.895-1.685 1.37-2.528.05-.08.157-.185.236-.185.71-.08 1.422-.13 2.106-.21.158-.026.342-.13.5-.21-.08-.132-.132-.29-.21-.422-.106-.16-.264-.29-.37-.45-.104-.13-.183-.29-.262-.447-.08-.13-.158-.236-.237-.37a9.7 9.7 0 0 1-.45-.894c-.026-.08-.08-.21-.052-.29.474-1.027.658-2.134 1.105-3.162.447-1.054.58-2.24.79-3.373.184-1.08.29-2.16.42-3.24.08-.764.185-1.502.21-2.266.16-1.212.106-2.346.08-3.48-.026-1-.08-2.028-.13-3.03zM12.685 66.405c-.184-.21-.342-.448-.526-.658l.08-.08c.287.317.577.633.866.976-.158-.08-.342-.132-.42-.238zm.42.238c.08-.027.16-.027.238-.053.08.132.132.29.21.448-.368-.027-.552-.185-.447-.395zm27.37 10.883v-.08c.5-.052.973-.105 1.473-.157v.077c-.5.08-.973.13-1.473.158zm6.63-.685c-.367.08-.762.133-1.13.186-.132.026-.29.158-.342-.08-.053.027-.106.027-.158.054.13.394.447.078.71.236-.58.08-1.13.132-1.684.21v-.052c.16-.026.343-.053.5-.08v-.078a7.743 7.743 0 0 0-.79-.053c-.077 0-.183.106-.262.132-.105.026-.21.053-.342.053-.447.026-.894.026-1.316.052-.027 0-.08-.026-.106-.026v-.08c1.763-.236 3.5-.473 5.263-.71.027.052.027.105.053.157-.158 0-.263.055-.395.08zm.396-.262c.606-.08 1.16-.132 1.738-.21-1.21.342-1.605.394-1.737.21zM24.58 23.374c.84-1.16 1.71-2.32 2.552-3.505.263-.345.473-.714.736-1.056.08-.106.185-.158.316-.264l-.026-.05c.105-.133.21-.24.263-.344.134-.21.213-.448.318-.685a.385.385 0 0 1 .105-.103c.37.184.37-.21.5-.343.237-.264.474-.553.684-.817.158-.21.316-.395.448-.632.026-.08-.053-.21-.08-.317h-.078c.08-.052.158-.13.237-.184.026 0 .026 0 .052-.026.158-.238.316-.475.474-.686.315-.42.657-.842 1.025-1.21-.052.13-.105.263-.158.368.027 0 .027.027.053.027.316-.422.658-.817.974-1.24-.027-.025-.053-.052-.08-.052-.13.132-.236.264-.368.396-.026-.027-.052-.053-.08-.053.265-.343.528-.685.79-1.08.053.08.106.184.21.395.107-.263.212-.447.29-.632-.078.08-.183.158-.262.238l-.08-.08.474-.71c.5-.712 1-1.45 1.5-2.162.185-.263.42-.474.58-.738.5-1 1.29-1.792 1.894-2.714.132-.184.316-.342.474-.5.13-.16.237-.106.342.026.71.896 1.42 1.818 2.13 2.714.528.66 1.054 1.29 1.554 1.976.605.844 1.184 1.687 1.79 2.53.684.975 1.368 1.95 2.026 2.95 1 1.477 1.947 2.953 2.947 4.428.737 1.08 1.474 2.135 2.184 3.215h-1.344c-1.236-.025-2.5-.13-3.736-.078-1.684.08-3.394.264-5.078.396-2.132.185-4.29.21-6.42.21-.765 0-1.528.107-2.29.16-.922.052-1.817.105-2.738.13-1.08.054-2.13.08-3.21.107-.606.026-1.237 0-1.895 0zm30.183 12.12v.238c-.026 0-.052.027-.105.027-.105-.37-.21-.766-.342-1.135-.263-.765-.553-1.53-1.027-2.214-.528-.737-1-1.5-1.528-2.265-.13-.185-.316-.343-.474-.5-.553-.607-1.106-1.24-1.816-1.687a21.485 21.485 0 0 0-3.29-1.688 7.374 7.374 0 0 1-.92-.474h.63l4.5-.08c.974-.025 1.922-.025 2.895-.078.236 0 .368.08.5.29.236.395.473.79.736 1.186.027.052.08.13.08.21 0 .58 0 1.186.026 1.766.025.606.08 1.186.104 1.792 0 .606-.053 1.238-.026 1.87.027.897.053 1.82.053 2.74zM26.447 26.67c1.237-.053 2.42-.132 3.632-.185.945-.053 1.92-.08 2.866-.132.395-.025.764-.05 1.158 0-.42.212-.842.423-1.21.686-.474.316-.92.737-1.395 1.08-.475.342-.896.764-1.29 1.212-.5.605-1.053 1.132-1.58 1.712-.37.422-.79.817-1.105 1.265-.447.58-.842 1.21-1.263 1.87.132-2.504.29-4.98.184-7.51zm17.185 25.35c-.843.21-1.71.448-2.58.553-.736.106-1.5.08-2.263.08a25.42 25.42 0 0 1-2.028-.08c-.763-.078-1.526-.157-2.263-.5-.633-.29-1.29-.553-1.92-.87-.634-.316-1.265-.684-1.74-1.264-.34-.423-.815-.765-1.236-1.134.08.316.263.58.553.764-.132.158-.316.08-.58-.343-.078.053-.157.08-.21.106.08-.185.158-.37.237-.527-.105-.21-.237-.448-.342-.66-.21-.342-.42-.71-.605-1.053-.053-.08-.053-.158-.105-.237a5.893 5.893 0 0 1-.37-.475c-.21-.315-.394-.657-.657-.974 0 .08.027.158.027.264-.027 0-.053.026-.053.026l-.554-1.344c-.026 0-.026 0-.052.026l.473 1.74c-.026 0-.052.025-.08.025-.077-.104-.156-.21-.21-.34-.052-.212-.21-.212-.34-.133-.08.053-.133.237-.106.316.185.448.395.896.606 1.344.052.158.105.29.184.448.027.053.106.105.106.184.106.21.185.42.316.606.237.316.5.632.737.948.235.316.445.66.656.975.026.053.105.053.13.08.133.395.58.684.896.526.08.606.737.817 1 1.397a11.957 11.957 0 0 1-.763-.343c-.027.026-.027.052-.054.105.316.158.632.316.92.5.265.16.528.317.765.5.316.29.685.45 1.13.554a.282.282 0 0 0-.05-.107c.736.343 1.5.712 2.078 1-2.737.054-5.658.107-8.685.16 0-.5-.026-.975-.026-1.476 0-.21.052-.395.025-.606-.08-1.21-.08-2.424-.237-3.61-.157-1.264-.157-2.503-.13-3.77.025-.683-.027-1.394-.054-2.08 0-.922 0-1.82.028-2.74 0-.132.053-.237.106-.37h.08c.025.054 0 .133.05.16.08.08.212.21.265.184.157-.106.394-.21.447-.37.13-.315.184-.658.184-.974 0-.236.106-.394.21-.553.054-.08.08-.158.133-.263-.105-.08-.21-.132-.342-.237.106-.29.08-.633.475-.79.052-.027.052-.16.08-.238.025-.213.05-.45.078-.66.052.08.08.105.13.157a.42.42 0 0 1 .054-.08c0-.104-.026-.315 0-.315.316-.053.184-.395.342-.553.025-.028-.027-.107-.027-.16 0-.052 0-.13.026-.13.367-.08.315-.475.552-.66.08-.053.105-.13.21-.263.21.368-.158.553-.184.816.446-.263.578-.895.315-1.08.105-.08.21-.184.29-.29.29-.316.604-.606.868-.922.185-.236.29-.526.474-.763.106-.132.316-.237.474-.317.474-.262.92-.552 1.21-1 .053-.053.132-.105.21-.158.08-.053.238-.053.264-.132.027-.052-.052-.184-.105-.263.104-.053.21-.158.42-.264-.08.158-.105.264-.158.37l.13.13c.238-.184.606-.394.843-.552 0-.025-.132-.13-.132-.13-.157.08-.394.21-.63.316.05-.08.05-.132.08-.158.367-.237.735-.474 1.13-.66.92-.42 1.842-.842 2.763-1.237.158-.08.37-.026.553-.026.078 0 .13 0 .21-.026.42-.132.842-.264 1.263-.37.183-.052.393-.078.58-.078.787.025 1.577.025 2.366.078.342.026.658.105.974.21a9.88 9.88 0 0 1 1.184.5c.447.24.868.502 1.29.792.763.5 1.473 1.054 2.236 1.502.737.448 1.316 1.054 1.79 1.74.58.816 1.237 1.554 1.5 2.555l.394 1.74c.08.316.264.632.185 1-.133.66-.238 1.345-.343 2.004-.052.265-.105.53-.078.79.05.82-.265 1.53-.58 2.268-.106.237-.264.475-.395.738a.798.798 0 0 0 .21.106l.237-.474c.027 0 .027 0 .053.027-.132.368-.237.764-.37 1.133-.314.817-.63 1.66-1.025 2.45-.21.448-.58.817-.842 1.24-.262.368-.473.763-.736 1.106-.237.29-.473.58-.79.79-.71.527-1.447 1.054-2.21 1.476-.473.29-1.026.448-1.552.58zm-14.027-1.4l-.026.027c-.055-.026-.134-.052-.186-.105l-.632-.95c-.052-.078-.08-.157-.052-.262.29.448.58.87.895 1.29zm16.37 3.61c1.183-.5 2.157-1.21 3.05-2.028.133-.132.264-.263.422-.37 1.106-.684 1.92-1.633 2.658-2.687.842-1.212 1.395-2.582 2.08-3.873a2.73 2.73 0 0 1 .157-.29c-.053 3.004.29 5.955.684 8.933-2.973.105-6 .21-9.052.316zm26.683-.79c-.026.053-.08.106-.105.16-.027-.054-.027-.133-.053-.24-.158.423-.5.212-.737.212-1.42.027-2.868.027-4.29.027-1.368 0-2.762 0-4.13.024-.448 0-.922.105-1.37.132-1.078.052-2.157.08-3.236.105-.08 0-.158-.13-.29-.236a1.81 1.81 0 0 1-.158.237c-.028-.052-.08-.104-.133-.183-.026.08-.053.158-.08.21H58c-.053-.368-.158-.71-.158-1.08 0-.79.08-1.58.105-2.372.027-.368 0-.71 0-1.054.106.08.185.133.29.21.052-.103.105-.182.158-.26 0 0-.053-.028-.106-.08.05-.027.104-.08.104-.106.026-.08.08-.158.08-.21 0-.185-.054-.343-.08-.5.026 0 .052 0 .08-.028l.157.79h.08c-.106-.183.236-.342-.053-.552-.026-.027.026-.185.026-.264-.08-.157-.13-.315-.21-.526.026-.026.105-.053.184-.08-.105-.052-.184-.104-.263-.13.263-.238.263-.37.026-.633.054-.025.106-.025.106-.05 0-.238 0-.475-.052-.71-.053-.266.08-.58-.316-.74a.79.79 0 0 0 .105.21s-.08.027-.158.08c-.342-.317-.13-.74-.21-1.213.184.053.316.106.447.16-.053-.186-.184-.397-.263-.634h-.107v-1.74c0 .027.184.027.29.054 0-.027.025-.053.025-.08-.08-.105-.185-.21-.29-.342l.053-.053c-.21-.262-.105-.63-.105-.71V39.4c.264.264-.13.606.264.764v-.263h-.027c-.026-.395-.026-.79-.052-1.186h-.052c-.027.054-.027.08-.054.133h-.052l.158-6.298c.263.342.552.66.736 1 .606 1.108 1.395 2.057 2.132 3.058.632.87 1.21 1.818 1.79 2.714.71 1.08 1.394 2.16 2.105 3.24a81.41 81.41 0 0 0 1.63 2.426c.5.71 1.028 1.396 1.554 2.082.446.606.92 1.212 1.367 1.818.527.738 1.053 1.475 1.58 2.187.262.368.552.737.84 1.106.16.21.396.37.554.5-.025 0-.052 0-.104-.026.08.105.13.184.184.237.29.158.316.316.158.554zM74 46.854v-.185c0 .052.026.13 0 .184zm.895-11.62c-.027 0-.184-.16-.21-.186-.027.08 0 .158-.053.264-.027-.078-.21-.052-.21-.13-.027.368.157.737.13 1.106.08-.053.395-.08.474-.158.027.026.08.052.106.052-.527.396-.395.79-.158 1.24.052.104.21.315.052.526-.052.053.027.21.053.343h.077v.05l-.237.08c-.052-.08-.367-.236-.367-.37v1.346c.263.08.263.448.368.633a.768.768 0 0 0 .107-.21l.027.024c-.027.158-.053.316-.106.475-.052.236-.105.447-.13.684 0 .026.05.08.05.105-.288.66-.13 1.396-.235 2.08-.08.5 0 1.03-.053 1.556-.054.448-.16.922-.264 1.37-.027.08-.08.105-.21.158.052-.316.026-.527-.027-.817-.028 0-.37-.184-.397-.184 0 .37.21.87.29 1.29-.08-.026-.395-.21-.42-.21-.054.316-.054.738-.08 1.08-.027.264-.263.5-.29.79 0 .16.184.264.158.528h.21c0-.526.238-1 .238-1.554h.078c.027.053.106.106.08.132-.053.29-.16.606-.132.896 0 .158.13.316.08.5-.054.16-.08.317-.107.554-.027-.132-.053-.184-.053-.263-.026 0-.263-.027-.29-.027-.026.158.185.316.158.448-.026.026-.052.026-.105.053l-.868-1.266c-.686-1-1.37-2.003-2.054-3.03a6.312 6.312 0 0 1-.475-.79 37.09 37.09 0 0 0-2.71-4.033c-.762-.974-1.37-2.03-2.08-3.055-.656-.975-1.314-1.924-1.972-2.9-.237-.315-.526-.605-.737-.948-.683-1.08-1.29-2.187-1.972-3.267-.58-.897-1.21-1.767-1.816-2.636-.21-.29-.42-.607-.632-.923a.37.37 0 0 1-.052-.182c-.053-.58-.106-1.16-.132-1.713 0-.527.053-1.054.053-1.608v-.474c0-.132.025-.237.025-.37.025-.025.052-.078.078-.104-.763 0-1.553-.028-2.316 0-.5.025-.763-.186-1.105-.555-1-1.133-1.737-2.424-2.605-3.636a162.42 162.42 0 0 0-2.5-3.427c-.685-.922-1.37-1.818-2.053-2.74-.764-1.054-1.5-2.108-2.29-3.162a381.983 381.983 0 0 0-2.895-3.794c-.45-.58-.95-1.133-1.45-1.74.343.054.66.106.975.133l1.264.08c.947.077 1.894.13 2.84.26.79.107 1.58.265 2.396.396 1.738.29 3.448.765 5.106 1.318.974.316 1.92.738 2.87 1.133 2.13.87 4.157 1.924 6.157 3.03.63.343 1 .896 1.472 1.397.685.712 1.37 1.423 2.027 2.16.762.87 1.472 1.766 2.21 2.662.657.79 1.34 1.58 2 2.372.21.237.37.527.552.79.42.633.895 1.24 1.263 1.924.262.502.42 1.082.604 1.635.262.817.526 1.607.79 2.424.183.606.34 1.24.472 1.87.106.423.08.87.21 1.29.16.556 0 1.16.16 1.715.025.053.05.132.078.185.105.104.184.21.026.368-.025.026-.025.13 0 .21.054-.052.08-.105.133-.184 0 .053.025.08.025.105 0 .104-.027.21 0 .315 0 .052.052.13.078.184.053-.054.105-.08.21-.16.237.897.264 1.793.264 2.715 0 .87.157 1.74-.21 2.583.078-.29-.106-.555-.027-.818z"/><path d="M58.08 45.482c.025 0 .052.027.052.027l-.027-.03c0-.025 0-.025-.026 0zm4.157 26.036c-.29.21-.58.395-.948.474-.028-.026-.028-.053-.054-.08.29-.184.605-.368.895-.553.027.05.08.104.106.157zM12.895 35.81c.29-.367.58-.736.894-1.105.025.026.235.08.262.105-.29.37-.685.87-.974 1.265-.054-.053-.133-.237-.185-.264zM5.42 48.725c-.21-.448-.42-.923-.63-1.37a.91.91 0 0 1 .236-.106c.29.42.42.92.632 1.37 0 0-.21.105-.237.105zm6.712-12.65c-.158.238-.316.502-.474.74-.026-.028-.316.104-.342.078.158-.237.552-.66.71-.896.027.026.053.053.106.08zM59.422 72.6c.025 0 .025-.026.052-.026.184.026.394.052.605.052-.344.237-.555.21-.66-.026zm-47.24-35.418c.028-.08.08-.158.133-.237.052 0 .13-.027.13-.027.107-.184.107-.316.212-.474-.026-.026-.053-.026-.08-.053-.157.108-.315.24-.473.345.053.052.053.08.053.132-.21-.027-.29.08-.395.368-.026.08-.158.106-.29.21-.026.054-.052.186-.105.317l.027.028c-.053.053-.132.08-.132.08-.158.157-.342.29-.5.447-.026.08-.052.158-.052.237.185-.184.5-.527.737-.738l.027.027c.105-.158.184-.316.29-.474.025.026.025.052.052.08-.08.21-.158.446-.237.657-.055.026-.134.08-.134.053-.105.08-.184.184-.29.263l-.473.316c-.263.237-.526.447-.816.685-.184.29-.368.553-.58.896.317-.08.396.053.37.317.368.052.395-.237.5-.448.026-.054.053-.16.105-.186.237-.21.5-.394.763-.605.053-.053.053-.16.053-.238 0-.026-.133-.026-.212-.053.237-.264.58-.71.816-1 .132-.08.263-.186.263-.265-.026-.29.158-.368.37-.474-.106-.08-.133-.157-.133-.183z"/><path d="M12.71 36.892c-.105.184-.21.342-.315.527l-.158-.08c-.105.605-.474 1.132-.842 1.237.105.053.21.106.29.08.078-.027.13-.16.183-.238l.71-1.028.238-.396-.105-.105zM3.948 48.46c.132 0 .264.026.42.026 0-.105.133-.08.133-.184h.08c0 .132.026.237.026.37h-.552c-.027-.027-.132-.186-.106-.212zm-.21-1.212c-.08-.08-.21-.158-.21-.237-.027-.104.052-.235.13-.367.054.184.08.342.132.527-.027.025-.053.052-.053.078zm.658-1.687c.105.266.21.556.316.82a.798.798 0 0 0-.21.105c-.105-.264-.237-.554-.342-.817a.652.652 0 0 1 .237-.106zm58.58 25.194c.13-.052.288-.08.5-.13-.238.183-.422.315-.58.473-.027-.026-.053-.053-.08-.053.053-.105.106-.184.16-.29zM30.63 15.074c.157-.106.29-.185.447-.29l.052.052c-.16.21-.29.42-.475.685-.026-.183-.026-.29-.053-.42-.026 0 0 0 .027-.026zm7.71 13.333c.237-.106.474-.21.763-.343-.026.158-.026.264-.026.37a.927.927 0 0 0-.264-.054c-.158.027-.448.238-.58.264-.025 0 .106-.21.106-.237zm19.74 22.346c.052.263.552.395.052.658.08.055.157.08.236.134a.2.2 0 0 1-.052.106c-.053.025-.158.078-.21.05-.027 0-.08-.104-.08-.157 0-.237.027-.474.053-.79z"/></g></symbol><symbol viewBox="0 0 24 24" id="powerpoint" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M8 11v2h1v6H8v1h4v-1h-1v-2h2a3 3 0 0 0 3-3 3 3 0 0 0-3-3H8m5 2a1 1 0 0 1 1 1 1 1 0 0 1-1 1h-2v-2h2z" fill="#d14524"/></symbol><symbol viewBox="0 0 67.47 70" id="powershell" xmlns="http://www.w3.org/2000/svg"><path d="M18.545 12.4c-3.014 0-6.08 2.34-6.873 5.248L1.91 53.438c-.793 2.908.996 5.248 4.01 5.248h42.887c3.014 0 6.08-2.34 6.873-5.248l9.761-35.79c.794-2.908-.993-5.248-4.007-5.248h-42.89zm4.848 6.243c.652.04 1.29.33 1.76.86l7.96 9.013-3.957 3.246 3.957-3.244 4.832 5.47c.037.042.06.088.094.131.026.034.057.06.082.096.02.028.032.057.05.086.057.087.105.176.15.267.028.06.055.117.08.178a2.546 2.546 0 0 1 .171.764c.005.073.01.146.008.219-.002.09-.01.178-.021.267a2.53 2.53 0 0 1-.036.217 2.56 2.56 0 0 1-.07.252c-.024.076-.048.15-.08.224a2.547 2.547 0 0 1-.111.22 2.503 2.503 0 0 1-.133.218 2.546 2.546 0 0 1-.147.187c-.058.07-.118.137-.185.202-.027.026-.048.057-.076.082-.037.032-.077.054-.116.084-.038.03-.07.065-.11.093L16.8 52.271a2.552 2.552 0 0 1-3.563-.626 2.553 2.553 0 0 1 .63-3.563l18.349-12.853-3.06-3.467-7.839-8.873a2.549 2.549 0 0 1 .225-3.608 2.546 2.546 0 0 1 1.85-.638zm22.441 28.214c1.377 0 2.255 1.083 1.969 2.43-.287 1.347-1.627 2.433-3.004 2.434l-9.957.006c-1.378 0-2.256-1.083-1.969-2.43.287-1.347 1.626-2.433 3.004-2.434l9.957-.006z" fill="#03a9f4" stroke-width="5.342" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 210 210" id="prettier" xmlns="http://www.w3.org/2000/svg"><title>prettier-icon-dark</title><g transform="matrix(.9 0 0 .9 10.5 10.5)" fill="none" fill-rule="evenodd"><rect fill="#56B3B4" x="165" y="40" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="200" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="135" y="120" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="75" y="120" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="120" width="50" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="160" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="80" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="65" y="20" width="110" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="20" width="40" height="10" rx="5"/><rect fill="#F7BA3E" x="55" y="180" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="55" y="60" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="180" width="30" height="10" rx="5"/><rect fill="#F7BA3E" x="15" y="60" width="30" height="10" rx="5"/><rect fill="#56B3B4" x="95" y="100" width="90" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="100" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="100" width="20" height="10" rx="5"/><rect fill="#BF85BF" x="105" y="40" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="40" width="80" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="140" width="100" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="140" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="135" y="60" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="135" y="80" width="60" height="10" rx="5"/><rect fill="#56B3B4" x="15" width="130" height="10" rx="5"/></g></symbol><symbol viewBox="0 0 80 80" id="protractor" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="hxa"><path transform="scale(1 -1)" fill="#564b55" stroke-width="27.224" d="M-2.983-69.251h69.412v67.108H-2.983z"/></clipPath></defs><g transform="matrix(1.13039 0 0 -1.13039 5.714 82.137)" clip-path="url(#hxa)"><g transform="scale(.1)"><path d="M1180.54 92.324c-5.53 0-9.93-1.797-13.23-5.39-3.29-3.614-5.22-8.594-5.81-14.97h36.02c0 6.583-1.47 11.622-4.4 15.126-2.93 3.496-7.12 5.234-12.58 5.234zm2.84-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.88 6.21-8.83 14.824-8.83 25.84 0 11.101 2.73 19.922 8.21 26.464 5.45 6.524 12.81 9.805 22.02 9.805 8.63 0 15.46-2.851 20.48-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.45c.21-8.086 2.26-14.22 6.12-18.418 3.89-4.18 9.34-6.29 16.38-6.29 7.42 0 14.76 1.563 22 4.669V34.14c-3.68-1.602-7.18-2.746-10.48-3.438-3.28-.684-7.24-1.035-11.89-1.035M1272.34 30.918v44.57c0 5.606-1.28 9.805-3.82 12.559-2.56 2.773-6.56 4.16-12.02 4.16-7.2 0-12.49-1.953-15.84-5.851-3.34-3.895-5.03-10.32-5.03-19.286V30.918h-10.42v68.887h8.47l1.71-9.422h.5c2.14 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.15 2.804 12.88 2.804 8.29 0 14.54-2.011 18.73-6.015 4.19-3.985 6.28-10.391 6.28-19.192V30.918h-10.43M1328.96 38.406c7.1 0 12.27 1.938 15.48 5.813 3.22 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.44 6.25-15.56 6.25-6.11 0-10.79-2.383-14.04-7.129-3.26-4.746-4.88-11.472-4.88-20.136 0-8.797 1.61-15.45 4.84-19.93 3.23-4.484 7.97-6.723 14.22-6.723zm20.85 1.762h-.56c-4.83-7.004-12.02-10.5-21.62-10.5-9.01 0-16.03 3.066-21.04 9.238-5 6.153-7.5 14.922-7.5 26.27 0 11.355 2.51 20.176 7.54 26.465 5.03 6.289 12.03 9.433 21 9.433 9.34 0 16.5-3.398 21.49-10.195h.81l-.43 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.38 9.25M1434.91 38.27c1.85 0 3.63.136 5.34.421 1.72.274 3.09.547 4.1.84v-7.976c-1.15-.559-2.81-.996-5.01-1.36-2.18-.351-4.17-.527-5.94-.527-13.32 0-19.97 7.012-19.97 21.055V91.71h-9.88v5.027l9.88 4.336 4.38 14.707h6.04V99.805h20V91.71h-20V51.16c0-4.15.98-7.333 2.96-9.56 1.97-2.206 4.67-3.331 8.1-3.331M1463.81 65.43c0-8.809 1.76-15.508 5.27-20.118 3.53-4.609 8.69-6.906 15.53-6.906s12.01 2.297 15.56 6.875c3.53 4.602 5.3 11.301 5.3 20.149 0 8.75-1.77 15.41-5.3 19.953-3.55 4.539-8.77 6.824-15.69 6.824-6.82 0-11.99-2.246-15.47-6.73-3.46-4.48-5.2-11.16-5.2-20.047zm52.47 0c0-11.23-2.83-20-8.48-26.309-5.66-6.309-13.47-9.453-23.44-9.453-6.17 0-11.64 1.445-16.42 4.336-4.78 2.89-8.46 7.031-11.06 12.45-2.59 5.401-3.88 11.73-3.88 18.976 0 11.23 2.8 19.968 8.41 26.242 5.61 6.258 13.4 9.402 23.38 9.402 9.64 0 17.3-3.222 22.97-9.62 5.69-6.415 8.52-15.087 8.52-26.024M1591.71 92.324c-5.54 0-9.94-1.797-13.23-5.39-3.3-3.614-5.24-8.594-5.81-14.97h36c0 6.583-1.46 11.622-4.39 15.126-2.93 3.496-7.13 5.234-12.57 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.89 6.21-8.83 14.824-8.83 25.84 0 11.101 2.74 19.922 8.2 26.464 5.46 6.524 12.81 9.805 22.04 9.805 8.62 0 15.45-2.851 20.48-8.523 5.03-5.676 7.54-13.157 7.54-22.461v-6.613h-47.45c.21-8.086 2.25-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.36-6.29 7.43 0 14.77 1.563 22.01 4.669V34.14c-3.69-1.602-7.17-2.746-10.46-3.438-3.3-.684-7.27-1.035-11.91-1.035M1683.5 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12.01 4.16-7.2 0-12.48-1.953-15.83-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M1740.11 38.406c7.12 0 12.28 1.938 15.49 5.813 3.21 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.43 6.25-15.56 6.25-6.12 0-10.8-2.383-14.05-7.129-3.24-4.746-4.88-11.472-4.88-20.136 0-8.797 1.64-15.45 4.85-19.93 3.22-4.484 7.96-6.723 14.21-6.723zm20.87 1.762h-.57c-4.82-7.004-12.03-10.5-21.62-10.5-9.01 0-16.02 3.066-21.03 9.238-5 6.153-7.52 14.922-7.52 26.27 0 11.355 2.52 20.176 7.55 26.465 5.02 6.289 12.02 9.433 21 9.433 9.34 0 16.5-3.398 21.48-10.195h.83l-.44 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.37 9.25M1846.07 38.27c1.85 0 3.64.136 5.36.421 1.7.274 3.07.547 4.08.84v-7.976c-1.13-.559-2.8-.996-5-1.36-2.2-.351-4.18-.527-5.94-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.86v5.027l9.86 4.336 4.4 14.707h6.04V99.805H1855V91.71h-19.98V51.16c0-4.15.98-7.333 2.95-9.56 1.97-2.206 4.68-3.331 8.1-3.331M1894.26 92.324c-5.53 0-9.94-1.797-13.22-5.39-3.31-3.614-5.25-8.594-5.83-14.97h36.01c0 6.583-1.45 11.622-4.38 15.126-2.95 3.496-7.13 5.234-12.58 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.1 9.297-5.9 6.21-8.84 14.824-8.84 25.84 0 11.101 2.73 19.922 8.2 26.464 5.47 6.524 12.81 9.805 22.03 9.805 8.63 0 15.46-2.851 20.49-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.46c.22-8.086 2.26-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.37-6.29 7.42 0 14.75 1.563 22 4.669V34.14c-3.7-1.602-7.17-2.746-10.47-3.438-3.28-.684-7.25-1.035-11.9-1.035M1983.36 49.727c0-6.426-2.4-11.368-7.18-14.844-4.77-3.477-11.47-5.215-20.11-5.215-9.13 0-16.26 1.445-21.37 4.336v9.687a51.32 51.32 0 0 1 10.65-3.964c3.79-.977 7.45-1.457 10.97-1.457 5.46 0 9.64.87 12.57 2.609 2.95 1.738 4.41 4.394 4.41 7.95 0 2.694-1.17 4.98-3.5 6.894-2.32 1.914-6.85 4.152-13.6 6.757-6.41 2.383-10.97 4.473-13.67 6.25-2.71 1.778-4.72 3.81-6.04 6.067-1.31 2.254-1.98 4.96-1.98 8.113 0 5.606 2.29 10.04 6.86 13.281 4.57 3.25 10.84 4.883 18.79 4.883 7.42 0 14.66-1.515 21.74-4.531l-3.71-8.496c-6.9 2.851-13.17 4.277-18.79 4.277-4.94 0-8.67-.77-11.18-2.324-2.52-1.543-3.78-3.691-3.78-6.406 0-1.844.48-3.418 1.42-4.707.95-1.309 2.46-2.54 4.56-3.711 2.09-1.184 6.11-2.871 12.07-5.086 8.16-2.98 13.69-5.98 16.55-8.996 2.87-3.02 4.32-6.809 4.32-11.367M2021.28 38.27c1.85 0 3.64.136 5.35.421 1.71.274 3.09.547 4.09.84v-7.976c-1.14-.559-2.81-.996-5.01-1.36-2.18-.351-4.18-.527-5.93-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.87v5.027l9.87 4.336 4.4 14.707h6.02V99.805h20V91.71h-20V51.16c0-4.15 1-7.333 2.97-9.56 1.98-2.206 4.67-3.331 8.1-3.331M2053.61 30.918h-10.42v68.887h10.42zm-11.31 87.559c0 2.39.59 4.14 1.76 5.253 1.18 1.106 2.65 1.661 4.42 1.661 1.67 0 3.1-.567 4.32-1.7 1.22-1.132 1.82-2.871 1.82-5.214 0-2.344-.6-4.09-1.82-5.247-1.22-1.16-2.65-1.726-4.32-1.726-1.77 0-3.24.566-4.42 1.726-1.17 1.157-1.76 2.903-1.76 5.247M2121.59 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.49l1.69-9.422h.5c2.15 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.16 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2159.29 77.742c0-4.812 1.35-8.465 4.08-10.926 2.72-2.48 6.51-3.71 11.37-3.71 10.19 0 15.28 4.953 15.28 14.831 0 10.344-5.16 15.532-15.47 15.532-4.9 0-8.67-1.32-11.31-3.965-2.63-2.649-3.95-6.555-3.95-11.762zm-5.67-58.387c0-3.73 1.58-6.55 4.72-8.488 3.14-1.922 7.65-2.879 13.52-2.879 8.75 0 15.24 1.309 19.45 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.15 6.32-3.45 7.754-2.31 1.457-6.65 2.168-13.01 2.168h-12.51c-4.74 0-8.43-1.12-11.06-3.386-2.65-2.266-3.97-5.508-3.97-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.23-3.39 3.15-5.754.91-2.371 1.37-5.039 1.37-8.02 0-6.746-2.29-12.128-6.91-16.152-4.61-4.012-10.93-6.023-18.98-6.023-2.05 0-3.98.156-5.78.5-4.45-2.356-6.67-5.305-6.67-8.871 0-1.883.77-3.282 2.34-4.176 1.54-.902 4.21-1.36 7.97-1.36h12.2c7.46 0 13.19-1.574 17.19-4.707 4-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2192.38 2.004 2183.46 0 2171.72 0c-9 0-15.95 1.68-20.82 5.027-4.88 3.352-7.34 8.079-7.34 14.211 0 4.18 1.35 7.813 4.03 10.88 2.68 3.046 6.45 5.116 11.32 6.21-1.77.8-3.24 2.031-4.44 3.711-1.19 1.68-1.78 3.633-1.78 5.84 0 2.52.66 4.707 2.01 6.602 1.34 1.882 3.44 3.71 6.34 5.468-3.56 1.465-6.46 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.26 13.37 6.79 17.452 4.52 4.082 10.93 6.133 19.22 6.133 3.6 0 6.86-.429 9.75-1.27h23.82M2284.61 91.71h-17.54V30.919h-10.43v60.793h-12.31v4.707l12.31 3.766v3.839c0 16.922 7.4 25.391 22.19 25.391 3.65 0 7.93-.73 12.82-2.195l-2.7-8.364c-4.03 1.301-7.46 1.946-10.31 1.946-3.93 0-6.85-1.309-8.73-3.926-1.89-2.617-2.84-6.816-2.84-12.598v-4.472h17.54V91.71M2302.87 65.43c0-8.809 1.76-15.508 5.28-20.118 3.52-4.609 8.7-6.906 15.52-6.906 6.84 0 12.02 2.297 15.57 6.875 3.54 4.602 5.3 11.301 5.3 20.149 0 8.75-1.76 15.41-5.3 19.953-3.55 4.539-8.78 6.824-15.69 6.824-6.83 0-11.99-2.246-15.46-6.73-3.48-4.48-5.22-11.16-5.22-20.047zm52.48 0c0-11.23-2.82-20-8.47-26.309-5.67-6.309-13.48-9.453-23.46-9.453-6.15 0-11.62 1.445-16.4 4.336-4.77 2.89-8.47 7.031-11.06 12.45-2.59 5.401-3.9 11.73-3.9 18.976 0 11.23 2.81 19.968 8.43 26.242 5.6 6.258 13.4 9.402 23.38 9.402 9.63 0 17.28-3.222 22.97-9.62 5.68-6.415 8.51-15.087 8.51-26.024M2403.79 101.074c3.07 0 5.8-.254 8.22-.761l-1.43-9.676c-2.86.633-5.37.933-7.55.933-5.58 0-10.33-2.261-14.3-6.785-3.95-4.531-5.94-10.156-5.94-16.902V30.918h-10.43v68.887h8.62l1.19-12.754h.5c2.56 4.48 5.63 7.949 9.23 10.37 3.61 2.423 7.56 3.653 11.89 3.653M2500.33 69.766l-10.68 28.476c-1.39 3.594-2.81 8.028-4.28 13.262-.93-4.024-2.24-8.438-3.96-13.262l-10.81-28.476zm14.77-38.848l-11.44 29.227h-36.83l-11.32-29.227h-10.81l36.34 92.273h8.98l36.13-92.273h-11.05M2583.07 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2620.76 77.742c0-4.812 1.36-8.465 4.08-10.926 2.73-2.48 6.53-3.71 11.37-3.71 10.2 0 15.28 4.953 15.28 14.831 0 10.344-5.15 15.532-15.45 15.532-4.91 0-8.68-1.32-11.32-3.965-2.64-2.649-3.96-6.555-3.96-11.762zm-5.66-58.387c0-3.73 1.57-6.55 4.71-8.488 3.15-1.922 7.65-2.879 13.53-2.879 8.75 0 15.23 1.309 19.44 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.14 6.32-3.45 7.754-2.31 1.457-6.64 2.168-13 2.168h-12.51c-4.74 0-8.43-1.12-11.07-3.386-2.63-2.266-3.96-5.508-3.96-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.22-3.39 3.14-5.754.92-2.371 1.38-5.039 1.38-8.02 0-6.746-2.3-12.128-6.92-16.152-4.61-4.012-10.92-6.023-18.97-6.023-2.05 0-3.99.156-5.78.5-4.46-2.356-6.67-5.305-6.67-8.871 0-1.883.78-3.282 2.33-4.176 1.55-.902 4.21-1.36 7.98-1.36h12.2c7.46 0 13.18-1.574 17.18-4.707 4.01-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2653.87 2.004 2644.94 0 2633.2 0c-9 0-15.95 1.68-20.83 5.027-4.88 3.352-7.33 8.079-7.33 14.211 0 4.18 1.35 7.813 4.02 10.88 2.69 3.046 6.47 5.116 11.32 6.21-1.77.8-3.23 2.031-4.43 3.711-1.19 1.68-1.79 3.633-1.79 5.84 0 2.52.66 4.707 2.01 6.602 1.35 1.882 3.45 3.71 6.35 5.468-3.56 1.465-6.47 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.25 13.37 6.79 17.452 4.52 4.082 10.92 6.133 19.21 6.133 3.62 0 6.86-.429 9.75-1.27h23.83M2692.7 99.805V55.117c0-5.605 1.27-9.805 3.83-12.566 2.56-2.766 6.57-4.145 12.01-4.145 7.2 0 12.47 1.965 15.81 5.903 3.33 3.945 4.99 10.379 4.99 19.304v36.192h10.44V30.918h-8.62l-1.5 9.25h-.58c-2.13-3.41-5.1-5.988-8.88-7.793-3.8-1.809-8.13-2.707-12.99-2.707-8.37 0-14.65 1.992-18.81 5.977-4.18 3.964-6.26 10.351-6.26 19.101v45.059h10.56M2760.61 30.918h10.43v97.805h-10.43zM2810.67 38.27c6.5 0 11.6 1.789 15.31 5.343 3.71 3.575 5.56 8.555 5.56 14.961v6.23l-10.44-.448c-8.3-.286-14.27-1.583-17.94-3.868-3.66-2.273-5.5-5.82-5.5-10.644 0-3.781 1.14-6.64 3.42-8.613 2.29-1.973 5.48-2.961 9.59-2.961zm23.57-7.352l-2.07 9.805h-.51c-3.44-4.305-6.86-7.227-10.27-8.77-3.42-1.523-7.68-2.285-12.8-2.285-6.83 0-12.17 1.758-16.05 5.273-3.87 3.528-5.81 8.536-5.81 15.032 0 13.906 11.12 21.199 33.37 21.875l11.7.359v4.277c0 5.418-1.17 9.395-3.5 11.985-2.32 2.566-6.03 3.855-11.15 3.855-5.74 0-12.24-1.758-19.49-5.273l-3.21 7.988c3.4 1.836 7.11 3.281 11.16 4.324a47.81 47.81 0 0 0 12.16 1.575c8.23 0 14.3-1.817 18.27-5.461 3.96-3.66 5.93-9.5 5.93-17.54V30.919h-7.73M2893.6 101.074c3.07 0 5.8-.254 8.25-.761l-1.46-9.676c-2.84.633-5.35.933-7.54.933-5.56 0-10.33-2.261-14.3-6.785-3.96-4.531-5.93-10.156-5.93-16.902V30.918h-10.44v68.887h8.61l1.19-12.754h.5c2.57 4.48 5.65 7.949 9.25 10.37 3.6 2.423 7.56 3.653 11.87 3.653M2901.63 6.727c-3.94 0-7.04.558-9.31 1.691v9.121c2.97-.84 6.08-1.25 9.31-1.25 4.14 0 7.3 1.25 9.45 3.77 2.16 2.507 3.24 6.132 3.24 10.859v91.895h10.69V31.797c0-7.95-2.01-14.121-6.04-18.496-4.02-4.383-9.8-6.574-17.34-6.574M2999.96 55.371c0-8.086-2.93-14.394-8.8-18.918-5.87-4.52-13.83-6.785-23.88-6.785-10.9 0-19.27 1.406-25.14 4.219v10.3c3.77-1.59 7.88-2.847 12.31-3.765 4.45-.93 8.85-1.399 13.21-1.399 7.12 0 12.49 1.36 16.09 4.063 3.59 2.695 5.4 6.465 5.4 11.277 0 3.196-.63 5.805-1.91 7.832-1.29 2.024-3.42 3.907-6.42 5.625-2.99 1.711-7.56 3.664-13.67 5.84-8.55 3.059-14.66 6.692-18.32 10.871-3.66 4.2-5.51 9.668-5.51 16.407 0 7.089 2.68 12.714 7.99 16.914 5.32 4.191 12.36 6.289 21.12 6.289 9.13 0 17.54-1.68 25.2-5.032l-3.32-9.304c-7.59 3.183-14.96 4.785-22.13 4.785-5.66 0-10.07-1.223-13.26-3.652-3.19-2.43-4.78-5.809-4.78-10.118 0-3.191.59-5.8 1.76-7.832 1.17-2.031 3.14-3.886 5.95-5.597 2.78-1.688 7.04-3.563 12.79-5.625 9.63-3.426 16.26-7.118 19.89-11.063 3.62-3.937 5.43-9.043 5.43-15.332M741.648 375.406h30c28.965 0 50.227 5.039 63.774 15.117 13.531 10.079 20.32 25.821 20.32 47.247 0 19.832-6.074 34.628-18.191 44.402-12.141 9.758-31.028 14.641-56.692 14.641h-39.211zm172.192 64.246c0-36.062-11.809-63.691-35.434-82.898-23.621-19.219-57.234-28.82-100.847-28.82h-35.911V198.73h-56.445v345.329h99.438c43.14 0 75.457-8.829 96.961-26.465 21.496-17.637 32.238-43.614 32.238-77.942M1099.26 464.691c11.17 0 20.39-.789 27.63-2.371l-5.43-51.718c-7.88 1.894-16.07 2.832-24.57 2.832-22.2 0-40.19-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.514v261.227h43.464l7.32-46.055h2.83c8.66 15.594 19.96 27.95 33.9 37.09 13.93 9.141 28.93 13.699 45 13.699M1206.88 329.82c0-60.308 22.28-90.465 66.85-90.465 44.08 0 66.13 30.157 66.13 90.465 0 59.688-22.21 89.512-66.61 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.95-75.972-32.83-99.898-21.89-23.945-52.35-35.918-91.41-35.918-24.41 0-45.97 5.508-64.7 16.543-18.75 11.016-33.16 26.836-43.23 47.48-10.08 20.625-15.11 44.551-15.11 71.793 0 42.364 10.86 75.43 32.58 99.2 21.73 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.05-24.328 33.06-57.121 33.06-98.379M1558.11 238.887c13.54 0 27.07 2.129 40.62 6.386v-41.816c-6.13-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.59 0-78.88 27.715-78.88 83.144v140.778h-35.68v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.48-9.57 26.34-9.57M1783.44 464.691c11.17 0 20.38-.789 27.62-2.371l-5.43-51.718c-7.88 1.894-16.06 2.832-24.56 2.832-22.2 0-40.2-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.52v261.227h43.46l7.34-46.055h2.82c8.66 15.594 19.95 27.95 33.9 37.09 13.92 9.141 28.93 13.699 45 13.699M1925.05 236.523c20.15 0 36.32 5.625 48.52 16.895 12.21 11.25 18.31 27.051 18.31 47.344v22.676l-33.54-1.407c-26.13-.937-45.16-5.312-57.04-13.105-11.89-7.793-17.82-19.727-17.82-35.781 0-11.661 3.45-20.665 10.39-27.051 6.91-6.387 17.32-9.571 31.18-9.571zm82.66-37.793l-11.11 36.387h-1.87c-12.62-15.918-25.29-26.738-38.04-32.48-12.74-5.742-29.13-8.633-49.13-8.633-25.67 0-45.7 6.934-60.1 20.801-14.41 13.847-21.62 33.457-21.62 58.808 0 26.934 10 47.246 30 60.934 19.99 13.691 50.45 21.172 91.41 22.441l45.09 1.414v13.938c0 16.699-3.88 29.16-11.68 37.441-7.79 8.262-19.88 12.383-36.25 12.383-13.39 0-26.23-1.953-38.5-5.891a294.638 294.638 0 0 1-35.44-13.933l-17.94 39.668c14.17 7.41 29.68 13.035 46.52 16.894 16.85 3.868 32.77 5.789 47.72 5.789 33.22 0 58.31-7.246 75.22-21.726 16.94-14.492 25.4-37.246 25.4-68.262V198.73h-39.68M2220.04 194.004c-39.52 0-69.55 11.543-90.1 34.609-20.55 23.067-30.82 56.172-30.82 99.321 0 43.925 10.74 77.707 32.23 101.339 21.5 23.614 52.56 35.418 93.18 35.418 27.56 0 52.35-5.117 74.41-15.359l-16.78-44.641c-23.46 9.133-42.82 13.704-58.1 13.704-45.19 0-67.79-29.993-67.79-89.981 0-29.293 5.63-51.305 16.89-66.031 11.26-14.707 27.76-22.09 49.48-22.09 24.72 0 48.11 6.152 70.15 18.437v-48.417c-9.92-5.84-20.5-10-31.76-12.52-11.26-2.52-24.93-3.789-40.99-3.789M2451.52 238.887c13.54 0 27.08 2.129 40.63 6.386v-41.816c-6.15-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.6 0-78.9 27.715-78.9 83.144v140.778h-35.66v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.47-9.57 26.33-9.57M2585.92 329.82c0-60.308 22.28-90.465 66.84-90.465 44.09 0 66.15 30.157 66.15 90.465 0 59.688-22.22 89.512-66.62 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.94-75.972-32.83-99.898-21.89-23.945-52.36-35.918-91.4-35.918-24.42 0-45.98 5.508-64.72 16.543-18.74 11.016-33.14 26.836-43.22 47.48-10.07 20.625-15.12 44.551-15.12 71.793 0 42.364 10.87 75.43 32.59 99.2 21.74 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.04-24.328 33.06-57.121 33.06-98.379M2972.33 464.691c11.18 0 20.38-.789 27.63-2.371l-5.43-51.718c-7.87 1.894-16.05 2.832-24.57 2.832-22.2 0-40.19-7.246-53.96-21.731-13.78-14.48-20.67-33.301-20.67-56.453V198.73h-55.51v261.227h43.46l7.33-46.055h2.83c8.66 15.594 19.96 27.95 33.89 37.09 13.94 9.141 28.94 13.699 45 13.699" fill="#100f0d"/><path d="M610.11 372.83c0-170.584-138.257-308.862-308.846-308.862-170.602 0-308.846 138.278-308.846 308.863 0 170.576 138.244 308.846 308.846 308.846 170.59 0 308.846-138.27 308.846-308.846" fill="#e53935" stroke-width="1.029"/><path d="M460.694 521.792l-105.04.958-61.415 61.415-72.096-47.883 12.445-12.438-29.207.26-99.129-166.817H67.357l24.39-24.402-24.57-41.363L294.66 64.049c2.192-.04 4.399-.08 6.603-.08 170.416 0 308.585 138.055 308.846 308.408L460.694 521.792" fill="#d51c2f" stroke-width="1.029"/><path d="M149.093 350.258c0 84.048 68.13 152.151 152.171 152.151 84.028 0 152.139-68.103 152.139-152.151zm342.063-7.017v14.046h44.015c-1.75 59.337-25.556 113.104-63.54 153.419L438.75 477.81l-9.925 9.94 32.875 32.887c-40.314 37.983-94.081 61.79-153.41 63.527l-.015-44.003h-14.035v44.003c-59.34-1.737-113.096-25.556-153.41-63.527l32.887-32.887-9.945-9.92-32.883 32.875c-37.975-40.315-61.781-94.082-63.53-153.419h44.002l-.008-14.034H67.176v-51.511h468.176v51.5h-44.196" fill="#f5f5f5" stroke-width="1.029"/></g></g></symbol><symbol id="pug" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#c1272d}.hyst1{fill:#efcca3}.st2{fill:#ed1c24}.hyst3{fill:#ccac8d}.hyst4{fill:#fff}.st5{fill:#ff931e}.st6{fill:#ffb81e}.hyst7{fill:#56332b}.hyst8{fill:#442823}.hyst9{fill:#7f4a41}.hyst10{fill:#331712}.st11{fill:#fc6}.st12{fill:#ccc}.st13{fill:#b3b3b3}.st14{fill:#989898}.st15{fill:#323232}.st16{fill:#1e1e1e}.st17{fill:#4c4c4c}.st18{fill:#e6e6e6}.st19{fill:#606060}</style><path class="hyst1" d="M107.4 50.9c-.2-4.4.4-8.3-1.6-11.6-4.8-8.2-16.8-13-40.8-13v.7h-.5.5v-.7c-24 0-36.6 4.8-41.4 13.1-1.9 3.4-1.7 7.2-2 11.6-.2 3.5-1.8 7.2-1.1 11.2.8 5.2 1.1 10.4 1.9 15.2.6 3.9 6 7.2 6.5 10.9 1.4 10.2 12 14.9 36 14.9v.8h-.6.7v-.8c24 0 34.2-4.7 35.5-14.9.5-3.8 5.5-7 6.1-10.9.8-4.8 1.1-10 1.9-15.2.7-4-.9-7.8-1.1-11.3z"/><path class="hyst3" d="M64.6 54.5c4.3.1 7.3 2.8 10.1 5.3 3.3 2.9 8.9 4.9 11.2 7.4 2.3 2.5 5.3 5 6.4 8.9 1.1 3.9 1.4 8.9 1.4 10.2 0 1.3.7 1 2.7 0 4.7-2.3 9.9-8.5 9.9-8.5-.6 3.9-5.7 7.4-6.2 11.1C98.9 99.1 89 104 64.5 104h-.1.6"/><path class="hyst3" d="M80.4 46.7c.9 3.1 4.1 13.6-2.1 10.1 0 0 2.6 1.5 4.2 7.2 1.7 5.7 5.8 6.4 5.8 6.4s6.7 1.3 11.7-3c4.2-3.6 4.9-10 3.1-14.9-1.8-4.8-5-6.3-9.7-7.3-4.7-1.1-14.1-2-13 1.5z"/><circle cx="92.3" cy="58.1" r="8.8"/><circle class="hyst4" cx="90" cy="54.2" r="2.3"/><path class="hyst1" d="M78.9 57.7s7.9 5.4 12.2 10.7c4.3 5.3 4.2 6.3 4.2 6.3l-3.1 1.4s-4.4-8.3-9.8-11.4c-5.5-3.1-6.1-5.7-6.1-5.7l2.6-1.3z"/><path class="hyst3" d="M64.9 54.5c-4.3.1-7.5 2.8-10.4 5.3-3.3 2.9-9.1 4.9-11.4 7.4-2.3 2.5-5.4 5-6.5 8.9-1.1 3.9-1.5 8.9-1.5 10.2 0 1.3.2 1.4-2.7 0-4.7-2.2-9.9-8.5-9.9-8.5.6 3.9 5.7 7.4 6.2 11.1C30.1 99.1 40 104 64.5 104h.5"/><path class="hyst7" d="M88.1 71.4C83.3 65.5 75.6 60 64.9 60h-.1c-10.7 0-18.4 5.5-23.2 11.4-5 6.1-4.6 8.5-4.6 14.3 0 21 7.4 15 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.7 12.3-17.3.1-5.8.4-8.4-4.6-14.5z"/><path class="hyst8" d="M64.4 65.2s-.7 9.7-2.1 11.6l2.6-.6-.5-11z"/><path class="hyst8" d="M65.1 65.2s.7 9.7 2.1 11.6l-2.6-.6.5-11z"/><path class="hyst7" d="M56.7 62.9c-1-2.3 2.6-6 8.3-6.1 5.7 0 9.3 3.7 8.3 6.1-1 2.4-4.6 3.1-8.3 3.2-3.6-.1-7.3-.8-8.3-3.2z"/><path d="M65 65.2c0-.4 3.4-.5 5.2-1.7 0 0-3.7 1.2-4.5.7-.8-.4-1-1.6-1-1.6s-.3 1.2-.9 1.6c-.7.4-4.9-.7-4.9-.7s5.6 1.4 5.6 1.7c0 .3-.1 1.3-.1 2 0 2.5 0 8.7.4 9.2.6.9.4-6.7.4-9.2-.1-.8-.1-1.6-.2-2z"/><path class="hyst9" d="M65.2 78.6c1.7 0 4.7 1.2 7.4 3.1-2.6-2.9-5.7-4.9-7.4-4.9-1.8 0-5.6 2.2-8.3 5.4 2.8-2.2 6.4-3.6 8.3-3.6z"/><path class="hyst8" d="M64.5 96.3c-3.8 0-7.5-1.2-10.9-2.1-.7-.2-1.4.3-2.1.1-6.3-2-11.4-5.4-14.5-9.7v1c0 21 7.4 15.1 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.6 12.3-17.4 0-.8 0-1.6.1-2.3-2.9 4.7-8.2 8.4-14.8 10.6-.6.2-2-.3-2.6-.2-3.6 1.2-6.8 2.5-10.9 2.5z"/><path class="hyst8" d="M55 85s-2.5 7.5-.8 10.8l-2.3-1s1.7-7.6 3.1-9.8zM74.8 85s2.5 7.5.8 10.8l2.3-1s-1.8-7.6-3.1-9.8z"/><path class="hyst3" d="M48.6 46.7c-.9 3.1-4.1 13.6 2.1 10.1 0 0-2.6 1.5-4.2 7.2s-5.8 6.4-5.8 6.4-6.7 1.3-11.7-3c-4.2-3.6-4.9-10-3.1-14.9s5-6.3 9.7-7.3c4.7-1.1 14-2 13 1.5z"/><path d="M64.9 76.8c2.7 0 11.1 5.8 11.2 12.9v-.4c0-7.4-6.8-13.3-11.2-13.3-4.4 0-11.2 6-11.2 13.3v.4c.1-7.1 8.5-12.9 11.2-12.9z"/><ellipse transform="rotate(-14.465 66.712 61.468)" class="hyst10" cx="66.7" cy="61.5" rx=".8" ry="1.5"/><ellipse transform="rotate(17.235 62.371 61.462)" class="hyst10" cx="62.4" cy="61.5" rx=".8" ry="1.5"/><circle cx="37.2" cy="58.1" r="8.8"/><circle class="hyst4" cx="39.5" cy="54.2" r="2.3"/><path class="hyst9" d="M67.5 58.2c0-.1-2.3 1-2.9 1.1-.6-.1-2.9-1.2-2.9-1.1h5.8z"/><path class="hyst1" d="M50 57.7s-7.9 5.4-12.2 10.7c-4.3 5.3-4.2 6.3-4.2 6.3l3.1 1.4s4.4-8.3 9.8-11.4 6.1-5.7 6.1-5.7L50 57.7z"/><path class="hyst3" d="M32.7 41.7S30 49.1 24 52.2c0 0 9.4-1.1 8.7-10.5zM95.8 41.7s2.7 7.4 8.7 10.5c0 0-9.4-1.1-8.7-10.5zM78.7 55.5s-5.9-6.2-13.8-6.4h.1.1c-8 .2-13.8 6.4-13.8 6.4 6.9-4.8 12.8-4.7 13.8-4.7-.1 0 6.7-.1 13.6 4.7zM71.8 42.5s-3-4.2-7-4.3h.2c-3 .1-6.9 4.3-6.9 4.3 3.4-3.3 6.9-3.2 6.9-3.2s3.3-.1 6.8 3.2zM37.2 73.2s-4.7 2.3-8.1.9H29c-3-1.7-4.5-6.8-4.5-6.8s3 9 12.7 5.9zM92 73.2s4.7 2.3 8.1.9c4-1.7 4.6-6.8 4.6-6.8s-3 9-12.7 5.9z"/><path class="hyst3" d="M42.6 41.2c2.6-.5 6.9-.6 10.3.5 4.3 1.5.8 7 1.7 7.3.9.3 2.1-3.8 10.1-3.4 8.1.4 9 4 10.1 3.4s-1.1-10 11-7.8c0 0-12.7-3.4-12.1 5.8 0 0-7.3-5.6-17.5-.6.1 0 2.7-8.6-13.6-5.2zM86.9 41.2c.2 0 .3.1.4.1.1 0-.1-.1-.4-.1zM86.9 41.2zM39.1 28.9S28.3 42.5 26.7 47.7c-1.6 5.3-2.8 27-4.2 30.1l-5-21.4 9.2-22.3 12.4-5.2zM89.9 28.9s10.8 13.6 12.4 18.8c1.6 5.3 2.8 27 4.2 30.1l5-21.4-9.2-22.3-12.4-5.2z"/><path class="hyst7" d="M89.4 28.9s11.6 9.7 15 20.9c3.4 11.2 2 24.8 4.6 26.5 3.7 2.4 7.9-11.9 9.3-13.4 2.2-2.4 9.5-8.5 10-9.6.5-1.1-14.8-17.8-21.5-21.1-8.1-3.8-18.1-4.1-17.4-3.3z"/><path class="hyst8" d="M99.3 34.9s13.7 17.5 13.5 39.3l5.5-11.2c-.1 0-4.9-14.3-19-28.1z"/><path class="hyst7" d="M39.1 28.9s-11.6 9.7-15 20.9-2 24.8-4.6 26.5c-3.7 2.4-7.9-11.9-9.3-13.4C8 60.5.7 54.4.2 53.3-.3 52.2 15 35.5 21.7 32.2c8.1-3.8 18.1-4.1 17.4-3.3z"/><path class="hyst8" d="M29.2 34.9S15.5 52.4 15.7 74.2L10.3 63s4.8-14.3 18.9-28.1z"/><path class="hyst3" d="M21.8 74.6s1 5.4 2.6 7.1.5-1.3.5-1.3-1.7-.9-1.4-7.8-1.7 2-1.7 2zM107.1 74.6s-1 5.4-2.6 7.1-.5-1.3-.5-1.3 1.7-.9 1.4-7.8 1.7 2 1.7 2z"/><g><circle class="hyst8" cx="54.5" cy="70.5" r=".8"/><circle class="hyst8" cx="49.9" cy="75.3" r=".8"/><circle class="hyst8" cx="48.4" cy="70.5" r=".8"/></g><g><circle class="hyst8" cx="74" cy="70.5" r=".8"/><circle class="hyst8" cx="78.6" cy="75.3" r=".8"/><circle class="hyst8" cx="80.1" cy="70.5" r=".8"/></g></symbol><symbol viewBox="0 0 50 50" id="puppet" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" fill="#fbc02d"><path stroke-width=".283" d="M11.559 249.467h13.587v13.587H11.559zM27.435 265.056h13.587v13.587H27.435zM11.559 281.074h13.587v13.587H11.559z"/><path stroke-width=".256" d="M16.62 251.615l18.305 18.305-3.236 3.236-18.305-18.305z"/><path stroke-width=".256" d="M37.834 271.331L19.53 289.636l-3.237-3.237 18.305-18.304z"/></g></symbol><symbol viewBox="0 0 100 99.999997" id="purescript" xmlns="http://www.w3.org/2000/svg"><path clip-path="url(#SVGID_2_)" d="M98.079 38.548L79.22 19.68l-5.087 5.088L90.447 41.09 74.134 57.41l5.087 5.087 18.858-18.86a3.59 3.59 0 0 0 1.055-2.55 3.578 3.578 0 0 0-1.055-2.54M25.483 42.794l-5.09-5.089L1.53 56.568a3.566 3.566 0 0 0-1.05 2.545c0 .961.373 1.863 1.05 2.542L20.394 80.52l5.089-5.086L9.162 59.113z" fill="#42a5f5" stroke-width="1.192"/><path clip-path="url(#SVGID_2_)" transform="matrix(1.19175 0 0 1.19175 -306.84 -629.047)" fill="#42a5f5" d="M281.841 551.736l6.461 6.037h28.379l-6.461-6.037zM288.302 566.861l-6.463 6.035h28.381l6.463-6.035zM281.838 581.982l6.464 6.035h28.381l-6.463-6.035z"/></symbol><symbol viewBox="0 0 24 24" id="python" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 7.5A2.86 2.86 0 0 1 22 10.36v3.78A2.86 2.86 0 0 1 19.14 17H12c0 .39.32.96.71.96H17v1.68a2.86 2.86 0 0 1-2.86 2.86H9.86A2.86 2.86 0 0 1 7 19.64v-3.75a2.85 2.85 0 0 1 2.86-2.85h5.25a2.85 2.85 0 0 0 2.85-2.86V7.5h1.18m-4.28 11.79c-.4 0-.72.3-.72.89 0 .59.32.71.72.71a.71.71 0 0 0 .71-.71c0-.59-.32-.89-.71-.89m-10-1.79A2.86 2.86 0 0 1 2 14.64v-3.78A2.86 2.86 0 0 1 4.86 8H12c0-.39-.32-.96-.71-.96H7V5.36A2.86 2.86 0 0 1 9.86 2.5h4.28A2.86 2.86 0 0 1 17 5.36v3.75a2.85 2.85 0 0 1-2.86 2.85H8.89a2.85 2.85 0 0 0-2.85 2.86v2.68H4.86M9.14 5.71c.4 0 .72-.3.72-.89 0-.59-.32-.71-.72-.71-.39 0-.71.12-.71.71s.32.89.71.89z"/><path d="M9.264 22.379c-.895-.24-1.581-.799-1.947-1.582-.228-.489-.237-.606-.238-2.957-.001-2.745.057-3.074.666-3.785.193-.226.568-.517.833-.648.47-.23.579-.239 3.839-.288 3.131-.048 3.386-.065 3.814-.264.626-.291 1.07-.687 1.4-1.247.27-.46.278-.522.311-2.29l.034-1.82.932.051c1.075.058 1.504.211 2.098.748.853.77.869.841.869 3.957 0 2.434-.02 2.783-.18 3.075a3.365 3.365 0 0 1-1.337 1.33l-.517.273-3.95.031-3.951.031.068.274c.037.151.164.377.282.503.209.224.262.229 2.433.229h2.22v1.05c0 1.653-.394 2.437-1.54 3.072l-.545.302-2.644.018c-1.455.01-2.782-.018-2.95-.063zm6.12-1.692c.22-.222.253-.325.206-.675-.07-.523-.278-.73-.732-.73-.467 0-.672.217-.735.78-.042.372-.012.496.163.672.3.3.77.28 1.097-.047z" fill="#fc0" stroke="#fc0" stroke-width=".102"/><path d="M9.349 22.38c-.911-.15-1.936-1.074-2.176-1.963-.073-.273-.101-1.279-.079-2.868.033-2.317.047-2.473.27-2.926.13-.263.401-.623.603-.8.674-.592.87-.63 3.484-.675 4.399-.076 4.927-.166 5.705-.967.642-.662.706-.9.774-2.883l.061-1.784.951.055c.523.031 1.11.122 1.304.204.54.225 1.358 1.042 1.472 1.47.153.572.243 3.18.16 4.617-.071 1.23-.093 1.327-.395 1.78-.193.288-.577.647-.966.903l-.647.425-3.922.008c-2.157.004-3.942.028-3.966.052-.115.115.354.82.587.883.14.038 1.181.073 2.314.079l2.06.01v.91c0 1.739-.326 2.446-1.454 3.162l-.631.4-2.543-.011c-1.398-.007-2.733-.043-2.966-.081zm5.98-1.718c.285-.256.313-.328.251-.658-.09-.483-.301-.682-.722-.682-.436 0-.625.193-.715.73-.065.384-.044.453.2.663.358.308.595.295.985-.053z" fill="#fdd835" stroke-width=".102"/><path d="M4.281 17.396c-.88-.215-1.714-.935-2.024-1.747-.149-.389-.168-.804-.142-3.041.027-2.26.054-2.638.215-2.962.259-.519.851-1.092 1.392-1.346.437-.206.632-.217 4.408-.245l3.95-.03-.067-.275a1.367 1.367 0 0 0-.282-.504c-.21-.224-.263-.23-2.433-.23h-2.22l.002-1.143c.003-1.338.157-1.795.84-2.493.746-.763 1.103-.838 4.025-.838 2.961 0 3.28.06 4.067.768.37.333.572.621.728 1.037.201.539.213.735.183 3.072-.035 2.777-.045 2.824-.78 3.598-.787.829-.76.824-4.59.883-3.812.06-3.797.057-4.61.806-.765.706-.917 1.2-.964 3.133l-.04 1.653-.677-.01c-.371-.007-.813-.045-.98-.086zM9.59 5.551c.237-.204.286-.326.286-.72 0-.547-.201-.763-.71-.763-.502 0-.765.248-.765.724 0 .492.141.782.439.902.345.14.444.12.75-.143z" fill="#3c78aa"/></symbol><symbol viewBox="0 0 24 24" id="r" xmlns="http://www.w3.org/2000/svg"><path d="M11.956 4.05c-5.694 0-10.354 3.106-10.354 6.947 0 3.396 3.686 6.212 8.531 6.813v2.205h3.53V17.82c.88-.093 1.699-.259 2.475-.497l1.43 2.692h3.996l-2.402-4.048c1.936-1.263 3.147-3.034 3.147-4.97 0-3.841-4.659-6.947-10.354-6.947m1.584 2.712c4.349 0 7.558 1.45 7.558 4.753 0 1.77-.952 3.013-2.505 3.779a1.081 1.081 0 0 1-.228-.156c-.373-.165-.994-.352-.994-.352s3.085-.227 3.085-3.302-3.23-3.127-3.23-3.127h-7.092v7.413c-2.64-.766-4.462-2.392-4.462-4.255 0-2.63 3.52-4.753 7.868-4.753m.156 4.12h2.143s.983-.05.983.974c0 1.004-.983 1.004-.983 1.004h-2.143v-1.977m-.031 4.566h.952c.186 0 .28.052.445.207.135.103.28.3.404.476-.57.073-1.17.104-1.801.104z" fill="#1976d2" stroke-width="1.035"/></symbol><symbol viewBox="0 0 24 24" id="raml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="razor" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 11.91c-.11-2.21-1.75-3.54-3.73-3.54h-.08c-2.29 0-3.55 1.8-3.55 3.84 0 2.29 1.53 3.74 3.54 3.74 2.25 0 3.72-1.65 3.83-3.59m-3.81-5.97c1.53 0 2.97.68 4.02 1.74 0-.51.33-.89.83-.89h.11c.74 0 .89.7.89.92v7.9c-.04.52.54.78.87.44 1.27-1.29 2.78-6.69-.79-9.81-3.33-2.92-7.8-2.44-10.18-.8-2.52 1.74-4.14 5.61-2.57 9.22 1.71 3.95 6.61 5.13 9.52 3.95 1.48-.59 2.15 1.4.65 2.05-2.34.99-8.77.89-11.78-4.32-2.03-3.52-1.93-9.71 3.46-12.92C10.81 1.42 16.24 2.1 19.5 5.5c3.45 3.6 3.25 10.3-.1 12.91-1.51 1.18-3.76.03-3.74-1.7l-.02-.56a5.611 5.611 0 0 1-3.99 1.66C8.63 17.81 6 15.15 6 12.13c0-3.05 2.63-5.74 5.65-5.74z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="react" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51m6.54-.76l.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C13.17 9 12.6 9 12 9c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47L8.57 12l.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47M12 6.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72m0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72M16.62 4c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96m-.7 5.74l.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51m1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95M17.08 12c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26M6.92 12c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26m9 2.26l-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51m-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9M8.08 9.74l.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51m2.89-4.04C9.38 4.2 8 3.62 7.37 4c-.63.35-.82 1.82-.31 3.96a22.7 22.7 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="readme" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="reason" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm5.119 8.993h2.798c.382 0 .71.025.985.075.275.05.534.159.774.326.244.168.435.386.577.654.145.265.218.598.218 1 0 .552-.112 1.001-.335 1.35-.22.348-.536.638-.947.87l2.16 3.203H12.31l-1.763-2.742h-.77v2.742H8.12v-7.478zm6.594 0h4.676v1.447h-3.018v1.29h2.802v1.447h-2.802v1.848h3.018v1.446h-4.676v-7.478zM9.778 13.37v2.014h.513c.266 0 .49-.014.67-.044.18-.03.329-.1.45-.207a.96.96 0 0 0 .253-.34c.055-.128.082-.297.082-.508 0-.187-.034-.35-.1-.483a.698.698 0 0 0-.343-.317 1.086 1.086 0 0 0-.395-.095 6.012 6.012 0 0 0-.526-.02h-.604z" fill="#f44336" stroke-width="1.067"/></symbol><symbol viewBox="0 0 172 193" id="restql" xmlns="http://www.w3.org/2000/svg"><title>Group</title><g transform="translate(14.767 16.713) scale(.82795)" fill="none"><path d="M171.39 55.799c-.975-6.147-4.673-11.642-10.15-14.805L96.381 3.546C93.217 1.72 89.615.756 85.964.756s-7.253.964-10.415 2.788L10.69 40.992A20.896 20.896 0 0 0 .272 59.035v74.89a20.894 20.894 0 0 0 10.416 18.042l64.859 37.446c3.165 1.827 6.767 2.791 10.417 2.791s7.252-.964 10.415-2.79l64.859-37.445c5.479-3.166 9.178-8.66 10.152-14.808zm-16.516 85.147L90.017 178.39a8.104 8.104 0 0 1-8.108 0l-64.857-37.444a8.109 8.109 0 0 1-4.053-7.021v-74.89a8.109 8.109 0 0 1 4.053-7.021l64.857-37.446c1.254-.725 2.654-1.086 4.054-1.086s2.8.361 4.054 1.086l64.857 37.446a8.106 8.106 0 0 1 4.053 7.021v74.89a8.109 8.109 0 0 1-4.053 7.021z" fill="#83e8c2"/><path d="M158.93 59.035a8.109 8.109 0 0 0-4.053-7.021L90.02 14.568c-1.254-.725-2.654-1.086-4.054-1.086s-2.8.361-4.054 1.086L17.055 52.014a8.106 8.106 0 0 0-4.053 7.021v74.89a8.109 8.109 0 0 0 4.053 7.021l64.857 37.444a8.104 8.104 0 0 0 8.108 0l64.857-37.444a8.109 8.109 0 0 0 4.053-7.021zm-46.766 31.681c.119-.069.242-.118.365-.149.044-.012.088-.01.131-.018.076-.012.152-.029.228-.029l.015.001c.02.001.038.005.059.006.093.005.184.019.273.04l.1.03c.077.025.15.057.223.095.028.014.057.027.084.043.094.057.184.122.263.199.007.008.013.017.021.024.07.071.133.15.188.235.018.029.033.059.05.09.04.072.072.148.099.229a1.512 1.512 0 0 1 .081.46v16.209l-3.278 1.893a1.548 1.548 0 0 0-.678.83 1.533 1.533 0 0 0-.098.514v3.785l-14.038 8.104-.01.004a1.55 1.55 0 0 1-.354.146c-.045.012-.09.011-.135.018-.074.012-.15.029-.225.029l-.014-.001c-.02-.001-.039-.005-.059-.006a1.463 1.463 0 0 1-.273-.041c-.034-.008-.066-.019-.1-.03a1.318 1.318 0 0 1-.223-.094c-.029-.015-.057-.027-.084-.044a1.45 1.45 0 0 1-.263-.198c-.009-.008-.015-.019-.023-.027a1.495 1.495 0 0 1-.185-.232c-.019-.029-.034-.06-.051-.09a1.422 1.422 0 0 1-.098-.229 1.702 1.702 0 0 1-.033-.101 1.487 1.487 0 0 1-.048-.358l-.001-.002v-20.053a1.446 1.446 0 0 1 .727-1.255zM85.24 31.369a1.449 1.449 0 0 1 1.452 0l45.741 26.41a1.45 1.45 0 0 1 0 2.512l-17.366 10.027a1.457 1.457 0 0 1-1.452 0l-15.49-8.943 1.727-.996a1.552 1.552 0 0 0 0-2.688l-13.111-7.57c-.239-.139-.508-.207-.775-.207s-.535.068-.775.207l-3.278 1.893-14.038-8.104a1.451 1.451 0 0 1 0-2.513zM57.59 47.558c.251 0 .501.065.726.194l15.489 8.942-1.727.997a1.552 1.552 0 0 0 0 2.688l1.727.996-15.488 8.943a1.457 1.457 0 0 1-1.452 0L39.499 60.291a1.45 1.45 0 0 1 0-2.512l17.366-10.027c.225-.129.475-.194.725-.194zm-9.56 92.328c-.241 0-.489-.062-.724-.196l-17.365-10.026a1.45 1.45 0 0 1-.726-1.256V75.59c0-.847.694-1.453 1.452-1.453.242 0 .49.062.724.197l17.366 10.025c.449.26.726.738.726 1.257v17.886l-1.727-.997a1.552 1.552 0 0 0-2.327 1.344v15.139c0 .555.295 1.067.775 1.344l3.278 1.894v16.209a1.45 1.45 0 0 1-1.452 1.451zm29.828 14.929a1.452 1.452 0 0 1-2.177 1.257l-17.365-10.026a1.452 1.452 0 0 1-.726-1.257v-17.885l1.726.996c.25.145.515.211.773.211.811 0 1.554-.648 1.554-1.555v-1.993l15.489 8.942c.449.26.726.738.726 1.257zm0-32.768c0 .127-.02.246-.049.36-.009.035-.021.067-.032.101-.026.08-.059.157-.099.229-.017.03-.032.061-.05.09a1.48 1.48 0 0 1-.188.235l-.021.025a1.51 1.51 0 0 1-.264.199c-.026.016-.055.028-.082.043a1.597 1.597 0 0 1-.324.124 1.362 1.362 0 0 1-.278.041c-.018.001-.036.006-.055.006l-.015.001c-.077 0-.155-.018-.233-.03-.043-.007-.084-.005-.125-.017a1.484 1.484 0 0 1-.366-.149l-14.035-8.104v-3.784a1.545 1.545 0 0 0-.776-1.343l-3.276-1.892V91.976c0-.127.02-.246.049-.361.009-.034.021-.066.032-.1a1.33 1.33 0 0 1 .099-.229c.017-.03.032-.062.051-.091.054-.084.116-.163.187-.234l.021-.025c.079-.076.168-.142.263-.199.027-.016.056-.029.084-.043a1.476 1.476 0 0 1 .601-.166c.019 0 .036-.005.055-.005l.015-.001c.078 0 .157.018.236.03.04.007.081.005.122.017.124.031.246.08.366.149l17.361 10.023a1.456 1.456 0 0 1 .726 1.259zm-9.984-45.373a1.448 1.448 0 0 1-.544-.55 1.466 1.466 0 0 1 0-1.413c.121-.219.303-.41.544-.55l14.038-8.104 3.277 1.892c.48.276 1.071.276 1.551 0l3.278-1.893 14.038 8.105a1.45 1.45 0 0 1 0 2.513L86.691 86.7a1.447 1.447 0 0 1-1.452 0zm74.842 51.733c0 .518-.276.997-.726 1.256l-45.741 26.409a1.452 1.452 0 0 1-2.177-1.257v-20.053c0-.519.277-.997.727-1.257l15.488-8.941v1.992c0 .906.743 1.555 1.553 1.555.26 0 .523-.066.774-.21l13.11-7.57a1.55 1.55 0 0 0 .776-1.344v-3.784l14.038-8.105a1.452 1.452 0 0 1 2.177 1.257v20.052zm0-32.764c0 .519-.276.997-.726 1.256l-15.489 8.943v-1.993c0-.906-.744-1.554-1.554-1.554a1.519 1.519 0 0 0-.773.21l-1.727.996V85.616c0-.519.277-.997.727-1.257l17.365-10.025c.234-.135.482-.197.724-.197.758 0 1.453.606 1.453 1.453z" fill="#111d5a"/><g fill="#83e8c2"><path d="M59.402 90.568zM94.485 123.06zM94.771 123.29zM77.775 122.51zM77.072 123.33zM77.418 123.09zM77.856 122.05zM76.749 123.45zM94.119 122.41zM77.131 133.51l-15.489-8.942v1.993c0 .906-.743 1.555-1.554 1.555a1.53 1.53 0 0 1-.773-.211l-1.726-.996v17.885c0 .519.276.997.726 1.257l17.365 10.026a1.452 1.452 0 0 0 2.177-1.257v-20.053a1.454 1.454 0 0 0-.726-1.257zM94.25 122.74zM110.28 111.42zM94.494 100.98c.088-.089.189-.168.303-.232l17.365-10.026-17.365 10.026a1.392 1.392 0 0 0-.303.232zM77.627 122.83zM58.027 90.936zM58.374 90.693zM59.044 90.521l-.015.001c.083-.001.167.015.251.029-.079-.012-.158-.03-.236-.03zM57.819 91.195zM58.696 90.568zM57.589 91.977zM76.043 123.46zM57.67 91.516zM75.677 123.31l-14.035-8.11zM76.401 123.5l.015-.001c-.082.001-.166-.016-.248-.029.078.012.156.03.233.03zM112.16 90.716zM77.662 101.27zM113.64 90.734zM96.237 123.31zM113.33 90.597zM112.89 90.52c-.075 0-.151.018-.228.029.081-.014.162-.029.242-.028l-.014-.001zM141.26 74.137c-.241 0-.489.062-.724.197l-17.365 10.025c-.449.26-.727.738-.727 1.257v17.885l1.727-.996c.25-.145.515-.211.773-.21.81 0 1.554.647 1.554 1.554v1.993l15.489-8.943a1.45 1.45 0 0 0 .726-1.256V75.59c0-.847-.695-1.453-1.453-1.453zM112.96 90.526zM95.523 123.5c.074 0 .15-.018.225-.029-.08.013-.159.028-.238.028l.013.001zM95.451 123.5zM85.238 86.7zM95.078 123.43zM141.26 106.9c-.241 0-.489.062-.724.196l-14.038 8.105v3.784c0 .555-.296 1.067-.776 1.344l-13.11 7.57c-.251.144-.515.21-.774.21-.81 0-1.553-.648-1.553-1.555v-1.992l-15.488 8.941c-.449.26-.727.738-.727 1.257v20.053a1.452 1.452 0 0 0 2.177 1.257l45.741-26.409a1.45 1.45 0 0 0 .726-1.256v-20.053a1.454 1.454 0 0 0-1.454-1.452zM67.871 41.396a1.451 1.451 0 0 0 0 2.513l14.038 8.104 3.278-1.893c.24-.139.508-.207.775-.207s.536.068.775.207l13.111 7.57a1.552 1.552 0 0 1 0 2.688l-1.727.996 15.49 8.943a1.457 1.457 0 0 0 1.452 0l17.366-10.027a1.45 1.45 0 0 0 0-2.512l-45.741-26.41a1.449 1.449 0 0 0-1.452 0zM39.497 57.779a1.45 1.45 0 0 0 0 2.512l17.366 10.027a1.457 1.457 0 0 0 1.452 0l15.488-8.943-1.727-.996a1.552 1.552 0 0 1 0-2.688l1.727-.997-15.489-8.942a1.458 1.458 0 0 0-1.451 0zM49.481 138.43v-16.209l-3.278-1.894a1.55 1.55 0 0 1-.775-1.344v-15.139c0-.906.743-1.555 1.554-1.554.259 0 .523.065.773.21l1.727.997V85.611a1.45 1.45 0 0 0-.726-1.257L31.39 74.33a1.436 1.436 0 0 0-.724-.197c-.758 0-1.452.606-1.452 1.453v52.817c0 .518.276.997.726 1.256l17.365 10.026a1.45 1.45 0 0 0 2.176-1.255zM114.34 108.18l-3.278 1.893 3.278-1.893V91.971zM114.11 91.193zM114.16 91.283z"/></g><g fill="#de5941"><path d="M94.494 100.98a1.45 1.45 0 0 0-.424 1.023v20.053l.001.002c0 .126.02.244.048.358.01.034.021.066.033.101.026.08.059.156.098.229.017.03.032.061.051.09.055.084.115.162.185.232.009.009.015.02.023.027.079.077.169.142.263.198.027.017.055.029.084.044a1.46 1.46 0 0 0 .596.165c.02.001.039.005.059.006.079 0 .158-.016.238-.028.045-.007.09-.006.135-.018.119-.031.238-.08.354-.146l.01-.004 14.038-8.104v-3.785c0-.18.04-.35.098-.514.122-.343.353-.643.678-.83l3.278-1.893V91.977c0-.127-.021-.246-.049-.361-.009-.033-.021-.065-.032-.099a1.266 1.266 0 0 0-.099-.229c-.017-.031-.032-.061-.05-.09a1.425 1.425 0 0 0-.188-.235l-.021-.024a1.41 1.41 0 0 0-.263-.199c-.027-.016-.056-.029-.084-.043a1.509 1.509 0 0 0-.323-.125 1.591 1.591 0 0 0-.273-.04c-.021-.001-.039-.005-.059-.006-.08-.001-.161.015-.242.028-.043.008-.087.006-.131.018-.123.031-.246.08-.365.149l-17.365 10.026a1.447 1.447 0 0 0-.302.233zM77.13 100.74L59.769 90.717a1.424 1.424 0 0 0-.366-.149c-.041-.012-.082-.01-.122-.017-.084-.015-.168-.03-.251-.029-.019 0-.036.005-.055.005-.095.005-.188.02-.278.041-.034.009-.065.02-.099.03a1.406 1.406 0 0 0-.224.095c-.028.014-.057.027-.084.043a1.515 1.515 0 0 0-.263.199l-.021.025c-.07.071-.133.15-.187.234-.019.029-.034.061-.051.091-.04.073-.072.149-.099.229a1.463 1.463 0 0 0-.081.461v16.206l3.276 1.892a1.547 1.547 0 0 1 .776 1.343v3.784l14.035 8.104c.119.068.242.117.366.149.041.012.082.01.125.017.082.014.166.03.248.029.019 0 .037-.005.055-.006.095-.004.188-.019.278-.041.034-.008.065-.019.099-.029.077-.025.152-.058.225-.095.027-.015.056-.027.082-.043.095-.058.185-.123.264-.199l.021-.025c.07-.071.133-.15.188-.235.018-.029.033-.06.05-.09.04-.072.072-.149.099-.229a1.448 1.448 0 0 0 .081-.461v-20.047a1.456 1.456 0 0 0-.726-1.259zM86.689 86.7l17.365-10.026a1.45 1.45 0 0 0 0-2.513l-14.038-8.105-3.278 1.893a1.556 1.556 0 0 1-1.551 0l-3.277-1.892-14.038 8.104c-.241.14-.423.331-.544.55a1.466 1.466 0 0 0 0 1.413c.121.218.303.41.544.55L85.238 86.7a1.447 1.447 0 0 0 1.451 0z"/></g></g></symbol><symbol viewBox="0 0 24 24" id="riot" xmlns="http://www.w3.org/2000/svg"><defs><path d="M13.26 3.04l.58.05.54.07.52.09.49.11.46.13.44.14.41.16.39.17.36.19.33.21.32.22.29.23.26.25.22.22.2.22.19.24.17.24.15.25.15.26.12.27.12.28.1.29.08.31.07.31.05.32.04.34.02.35.01.37v.05l-.02.51-.05.49-.09.48-.13.45-.15.43-.19.4-.22.39-.26.37-.28.34-.31.33-.33.3-.37.28-.39.27-.41.24-.44.22L21 21h-7.04l-3.48-5.14H9.17V21H3V3h9.01l.64.01.61.03zm-4.09 8.52h2.66l.99-.11.75-.35.47-.55.16-.74v-.05l-.17-.75-.47-.54-.74-.32-.96-.11H9.17v3.52z" id="ija"/></defs><use xlink:href="#ija" fill="#ff1744"/><use xlink:href="#ija" fill-opacity="0" stroke="#000" stroke-opacity="0"/></symbol><symbol viewBox="0 0 24 24" id="robot" xmlns="http://www.w3.org/2000/svg"><path d="M12.05 2.804a1.787 1.787 0 0 1 1.788 1.788c0 .661-.357 1.242-.893 1.546v1.135h.893a6.256 6.256 0 0 1 6.256 6.256h.894a.894.894 0 0 1 .893.893v2.681a.894.894 0 0 1-.893.894h-.894v.894a1.787 1.787 0 0 1-1.787 1.787H5.795a1.787 1.787 0 0 1-1.787-1.787v-.894h-.894a.894.894 0 0 1-.894-.894v-2.68a.894.894 0 0 1 .894-.894h.894a6.256 6.256 0 0 1 6.255-6.256h.894V6.138a1.773 1.773 0 0 1-.894-1.546 1.787 1.787 0 0 1 1.788-1.788m-4.022 9.83a2.234 2.234 0 0 0-2.234 2.235 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.234-2.234 2.234 2.234 0 0 0-2.234-2.234m8.043 0a2.234 2.234 0 0 0-2.234 2.234 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.235-2.234 2.234 2.234 0 0 0-2.235-2.234z" fill="#ff5722" stroke-width=".894"/></symbol><symbol viewBox="100 100 800 800" id="rollup" xmlns="http://www.w3.org/2000/svg"><style>.ilst0{fill:url(#ilXMLID_4_)}.ilst1{fill:url(#ilXMLID_5_)}.ilst2{fill:url(#ilXMLID_8_)}.ilst3{fill:url(#ilXMLID_9_)}.ilst4{fill:url(#ilXMLID_11_)}.ilst5{opacity:.3;fill:url(#ilXMLID_16_)}</style><g id="ilXMLID_14_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_4_" x1="444.47" x2="598.47" y1="526.05" y2="562.05" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_15_" class="ilst0" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_4_)"/></g><g id="ilXMLID_2_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_5_" x1="420.38" x2="696.38" y1="475" y2="689" gradientUnits="userSpaceOnUse"><stop stop-color="#BF3338" offset="0"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_10_" class="ilst1" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_5_)"/></g><linearGradient id="ilXMLID_8_" x1="429.39" x2="469.39" y1="517.16" y2="559.16" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_3_" class="ilst2" d="M329.82 813.46c15.58-8.903 122.41-220.34 227.02-320.5s117.96-66.771 60.094-175.83c0 0-221.46 310.49-301.58 464.06" fill="url(#ilXMLID_8_)" stroke-width="1.113"/><g id="ilXMLID_7_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_9_" x1="502.11" x2="490.11" y1="589.46" y2="417.46" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_12_" class="ilst3" d="M373 537c134.4-247.1 152-272 222-272 36.8 0 73.9 16.6 97.9 46.1-32.7-52.7-90.6-88-156.9-89H307.7c-4.8 0-8.7 3.9-8.7 8.7V691c13.6-35.1 36.7-85.3 74-154z" fill="url(#ilXMLID_9_)"/></g><linearGradient id="ilXMLID_11_" x1="450.12" x2="506.94" y1="514.21" y2="552.85" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FBB040" offset="0"/><stop stop-color="#FB8840" offset="1"/></linearGradient><path id="ilXMLID_6_" class="ilst4" d="M556.84 492.96c-104.61 100.16-211.44 311.6-227.02 320.5s-41.732 10.016-55.643-5.564c-14.801-16.582-37.837-43.401 86.802-272.65 149.57-274.99 169.15-302.7 247.05-302.7 40.953 0 82.24 18.473 108.95 51.302 1.447 2.337 2.893 4.785 4.34 7.233-45.738-47.074-145.23-57.98-169.93-.222-25.373 59.204 42.622 125.08 72.335 119.85 37.837-6.677-6.677-93.48-6.677-93.48 57.757 108.95 44.403 75.563-60.205 175.72z" fill="url(#ilXMLID_11_)" stroke-width="1.113"/><linearGradient id="ilXMLID_16_" x1="508.33" x2="450.33" y1="295.76" y2="933.76" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF" offset="0"/><stop stop-color="#FFF" stop-opacity="0" offset="1"/></linearGradient><path id="ilXMLID_13_" class="ilst5" d="M373.22 547.49c149.57-274.99 169.15-302.7 247.05-302.7 33.719 0 67.661 12.575 93.48 35.277-26.708-30.492-66.326-47.519-105.72-47.519-77.9 0-97.486 27.71-247.05 302.7-124.64 229.25-101.6 256.07-86.802 272.65 2.114 2.337 4.563 4.34 7.122 6.01-13.02-18.919-18.807-62.877 91.922-266.42z" fill="url(#ilXMLID_16_)" opacity=".3" stroke-width="1.113"/></symbol><symbol viewBox="0 0 24 24" id="ruby" xmlns="http://www.w3.org/2000/svg"><path d="M16 9h3l-5 7m-4-7h4l-2 8M5 9h3l2 7m5-12h2l2 3h-3m-5-3h2l1 3h-4M7 4h2L8 7H5m1-5L2 8l10 14L22 8l-4-6H6z" fill="#f44336"/></symbol><symbol viewBox="0 0 144 144" id="rust" xmlns="http://www.w3.org/2000/svg"><path d="M68.252 26.206a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0M25.766 58.451a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m84.97.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m-74.661 4.88a3.252 3.252 0 0 0 1.651-4.29l-1.58-3.574h6.214v28.01H29.823a43.847 43.847 0 0 1-1.42-16.738zm25.994.688v-8.256h14.798c.764 0 5.397.883 5.397 4.347 0 2.877-3.553 3.908-6.475 3.908zm-20.203 44.452a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m52.769.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m1.101-8.076a3.246 3.246 0 0 0-3.856 2.498l-1.787 8.342a43.847 43.847 0 0 1-36.566-.175l-1.787-8.342a3.246 3.246 0 0 0-3.854-2.497l-7.365 1.581a43.847 43.847 0 0 1-3.808-4.488h35.834c.406 0 .676-.074.676-.443V84.527c0-.369-.27-.442-.676-.442h-10.48V76.05h11.335c1.035 0 5.532.296 6.97 6.045.45 1.768 1.44 7.519 2.116 9.36.674 2.065 3.417 6.19 6.34 6.19h18.501a43.847 43.847 0 0 1-4.06 4.7zm19.898-33.468a43.847 43.847 0 0 1 .093 7.612h-4.499c-.45 0-.631.296-.631.737v2.066c0 4.863-2.742 5.92-5.145 6.19-2.288.258-4.825-.958-5.138-2.358-1.35-7.593-3.6-9.214-7.152-12.016 4.409-2.8 8.996-6.93 8.996-12.457 0-5.97-4.092-9.729-6.881-11.572-3.914-2.58-8.246-3.096-9.415-3.096H39.336A43.847 43.847 0 0 1 63.867 28.52l5.484 5.753a3.243 3.243 0 0 0 4.59.105l6.137-5.869a43.847 43.847 0 0 1 30.017 21.38l-4.201 9.487a3.256 3.256 0 0 0 1.652 4.29zm10.477.154l-.143-1.467 4.327-4.036c.88-.82.55-2.472-.574-2.891l-5.532-2.068-.433-1.428 3.45-4.792c.704-.974.058-2.53-1.127-2.724l-5.833-.949-.7-1.31 2.45-5.38c.502-1.095-.43-2.496-1.636-2.45l-5.92.206-.935-1.135 1.36-5.766c.275-1.17-.913-2.36-2.084-2.085l-5.765 1.359-1.136-.935.207-5.92c.046-1.198-1.357-2.135-2.45-1.637l-5.379 2.452-1.31-.703-.95-5.833c-.193-1.183-1.75-1.83-2.723-1.128l-4.796 3.45-1.425-.432-2.068-5.532c-.42-1.127-2.072-1.452-2.89-.576l-4.036 4.33-1.467-.143-3.117-5.036c-.63-1.02-2.318-1.02-2.946 0l-3.117 5.036-1.467.143-4.037-4.33c-.819-.876-2.47-.551-2.89.576l-2.069 5.532-1.426.432-4.795-3.45c-.974-.703-2.53-.055-2.723 1.128l-.951 5.833-1.31.703-5.379-2.452c-1.093-.5-2.496.439-2.45 1.637l.206 5.92-1.136.935-5.765-1.36c-1.171-.272-2.36.915-2.086 2.086l1.358 5.766-.933 1.135-5.92-.206c-1.193-.035-2.134 1.355-1.637 2.45l2.453 5.38-.703 1.31-5.832.949c-1.185.192-1.827 1.75-1.128 2.724l3.45 4.792-.433 1.428-5.532 2.068c-1.123.42-1.452 2.07-.574 2.891l4.328 4.036-.143 1.467-5.035 3.116c-1.02.63-1.02 2.318 0 2.946l5.035 3.117.143 1.467-4.328 4.037c-.878.818-.549 2.468.574 2.89l5.532 2.068.433 1.428-3.45 4.793c-.701.976-.056 2.532 1.129 2.723l5.831.948.703 1.312-2.453 5.378c-.5 1.093.444 2.5 1.638 2.451l5.917-.207.935 1.136-1.358 5.768c-.275 1.168.915 2.355 2.086 2.08l5.765-1.357 1.137.932-.207 5.921c-.046 1.199 1.357 2.136 2.45 1.636l5.379-2.45 1.31.702.95 5.83c.193 1.187 1.75 1.829 2.725 1.13l4.792-3.453 1.427.435 2.069 5.53c.42 1.123 2.072 1.454 2.89.574l4.037-4.328 1.467.146 3.117 5.035c.628 1.016 2.316 1.018 2.946 0l3.117-5.035 1.467-.146 4.036 4.328c.818.88 2.47.549 2.89-.574l2.068-5.53 1.428-.435 4.793 3.453c.974.699 2.53.055 2.722-1.13l.952-5.83 1.31-.703 5.378 2.451c1.093.5 2.493-.435 2.45-1.636l-.206-5.92 1.135-.933 5.765 1.357c1.171.275 2.36-.912 2.085-2.08l-1.358-5.768.932-1.136 5.92.207c1.194.048 2.138-1.358 1.636-2.451l-2.45-5.378.7-1.312 5.833-.948c1.187-.19 1.831-1.747 1.127-2.723l-3.45-4.793.433-1.428 5.532-2.068c1.125-.422 1.454-2.072.574-2.89l-4.327-4.037.143-1.467 5.035-3.117c1.02-.628 1.021-2.315.001-2.946z" fill="#ff7043" stroke-width="1.146"/></symbol><symbol viewBox="0 0 500 500" id="sass" xmlns="http://www.w3.org/2000/svg"><path d="M422.676 96.573c-12.192-47.839-91.508-63.557-166.575-36.892-44.68 15.877-93.029 40.786-127.81 73.311-41.349 38.675-47.943 72.328-45.216 86.395 9.583 49.622 77.585 82.069 105.535 106.126v.144c-8.246 4.05-68.565 34.584-82.684 65.799-14.893 32.932 2.372 56.556 13.804 59.742 35.424 9.859 71.764-7.866 91.311-37.01 18.853-28.12 17.28-64.422 9.086-82.487 11.3-2.976 24.476-4.314 41.218-2.36 47.248 5.52 56.517 35.017 54.747 47.366-1.77 12.35-11.681 19.14-14.998 21.186-3.317 2.045-4.326 2.766-4.05 4.287.405 2.215 1.94 2.137 4.758 1.652 3.894-.656 24.804-10.042 25.709-32.828 1.14-28.933-26.587-61.302-75.684-60.45-20.216.354-32.933 2.268-42.123 5.69-.681-.774-1.363-1.547-2.084-2.307-30.35-32.382-86.46-55.285-84.088-98.824.866-15.823 6.372-57.5 107.817-108.052 83.104-41.415 149.637-30.009 161.135-4.76 16.427 36.08-35.554 103.137-121.858 112.812-32.88 3.684-50.198-9.059-54.498-13.804-4.536-4.995-5.204-5.218-6.909-4.287-2.753 1.533-1.01 5.938 0 8.574 2.583 6.712 13.15 18.603 31.176 24.515 15.863 5.205 54.459 8.063 101.156-9.99 52.283-20.255 93.12-76.523 81.125-123.548zM200.213 340.34c3.92 14.5 3.487 28.016-.564 40.248a65.289 65.289 0 0 1-3.225 7.97c-3.12 6.477-7.316 12.534-12.442 18.132-15.653 17.069-37.507 23.532-46.88 18.092-10.122-5.874-5.048-29.944 13.083-49.11 19.52-20.636 47.602-33.903 47.602-33.903l-.039-.079 2.465-1.35z" fill="#ec407a" stroke="#ec407a" stroke-width="16.286552999999998"/></symbol><symbol viewBox="0 0 300 300" id="sbt" xmlns="http://www.w3.org/2000/svg"><path d="M105.46 209.517c-7.875 0-13.452-7.521-13.452-15.37v-.327c0-7.848 5.578-13.735 13.452-13.735h164.05c1.476-4.905 2.625-11.446 3.281-17.986h-137.81c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h137.31c-.82-6.54-1.969-13.081-3.773-17.986h-104.01c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h91.87c-21.327-37.607-60.864-61.315-106.14-61.315-67.918 0-123.04 54.448-123.04 122.3 0 67.856 55.122 123.28 123.04 123.28 46.59 0 87.112-25.507 107.95-63.114h-152.73z" fill="#0277bd" stroke-width="1.638"/></symbol><symbol viewBox="0 0 256 256" id="scala" xmlns="http://www.w3.org/2000/svg"><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M59.607 50.647l149.097-21.982v49.488L59.607 100.135zM59.593 114.08L208.69 92.098v49.488L59.593 163.568zM59.587 177.358l149.097-21.982v49.488L59.587 226.846z"/><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M62.425 91.414l95.605 30.923-2.832 8.757-95.605-30.922zM113.084 61.13l95.604 30.922-2.832 8.757-95.605-30.922zM62.425 154.79l95.605 30.922-2.833 8.758-95.604-30.923zM113.097 124.408l95.604 30.923-2.832 8.757-95.605-30.922z"/></symbol><symbol viewBox="0 0 24 24" id="settings" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="shaderlab" xmlns="http://www.w3.org/2000/svg"><path d="M9.11 17H6.5l-4.91-5L6.5 7h2.61l1.31-2.26L17.21 3l1.87 6.74L17.77 12l1.31 2.26L17.21 21l-6.79-1.74L9.11 17m.14-.25l5.13 1.38L11.42 13H5.5l3.75 3.75m6.87.38L17.5 12l-1.38-5.13L13.15 12l2.97 5.13M9.25 7.25L5.5 11h5.92l2.96-5.13-5.13 1.38z" fill="#1976d2"/></symbol><symbol viewBox="0 0 24 24" id="slim" xmlns="http://www.w3.org/2000/svg"><path d="M6.959 2.5a4.605 4.605 0 0 0-4.615 4.615v9.957a4.605 4.605 0 0 0 4.615 4.615h9.957a4.605 4.605 0 0 0 4.615-4.615V7.115A4.605 4.605 0 0 0 16.916 2.5zm4.938 2.691a6.811 6.811 0 0 1 6.81 6.813H13.43L9.938 7.287l.699 4.717H5.086a6.811 6.811 0 0 1 6.81-6.813z" fill="#f57f17"/></symbol><symbol id="smarty" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.iust0{fill:#ffce00}</style><path class="iust0" d="M9.14 20.606c0 .556.398.953.954.953h3.812c.556 0 .953-.397.953-.953v-.953H9.141zM12 2.5c-3.653 0-6.671 3.018-6.671 6.671 0 2.303 1.112 4.289 2.859 5.48v2.144c0 .556.397.953.953.953h5.718c.556 0 .953-.397.953-.953V14.65c1.747-1.191 2.86-3.177 2.86-5.48 0-3.653-3.019-6.671-6.672-6.671zm2.7 10.563l-.794.555v2.224h-3.812v-2.224l-.794-.555A4.712 4.712 0 0 1 7.235 9.17 4.78 4.78 0 0 1 12 4.405a4.78 4.78 0 0 1 4.765 4.765 4.712 4.712 0 0 1-2.065 3.892z"/></symbol><symbol viewBox="0 0 200 200" id="snyk" xmlns="http://www.w3.org/2000/svg"><title>Group 2</title><g transform="translate(15.255 18.22) scale(1.8477)" fill="none" fill-rule="evenodd"><path d="M65.161 24.997c-1.656 5.974-5.255 23.587-5.255 23.587s-6.618-2.464-14.148-2.476h-.055c-.413.002-.822.012-1.23.026v41.649h6.677v.003h5.815v-.003h20.858c.111-8.177-2.036-27.066-2.036-27.066-1.088-2.279.46-7.668.46-7.668-8.869-9.092-11.086-28.051-11.086-28.051zm-3.357 43.958c5.476 0 1.381 4.64.9 5.168H52.35c.944-1.18 4.504-5.168 9.453-5.168z" fill="#607d8b" stroke-width="1.6"/><path d="M26.366 24.995s-2.217 18.961-11.087 28.053c0 0 1.548 5.391.46 7.669 0 0-2.15 18.895-2.038 27.066h19.273v.003h7.079v-.003h5.744V46.107h-.025c-7.532.013-14.151 2.478-14.151 2.478s-3.6-17.615-5.255-23.59zm3.264 43.96c4.95 0 8.51 3.987 9.452 5.168H28.73c-.479-.528-4.573-5.168.9-5.168z" fill="#90a4ae" stroke-width="1.6"/><g transform="translate(23.76 77.45) scale(1.5998)"><g transform="translate(17.526)"><path d="M7.357.06H.177v.075C.177 2.64 2.345 4.67 4.89 4.67 7.431 4.67 9.6 2.64 9.6.135V.059z" fill="#455a64"/><path d="M1.972.06v.075a2.692 2.692 0 1 0 5.386 0V.059z" fill="#fff"/><path d="M5.496.06H4.234c-.012 0-.023.005-.034.007.157.033.243.388.21.624a.721.721 0 0 1-.71.617c.102.471.487.85.997.922a1.188 1.188 0 0 0 1.35-1.007C6.112.743 5.881.06 5.495.06z" fill="#37474f"/></g><path d="M7.552.06H.372v.075c0 2.505 2.17 4.535 4.712 4.535 2.544 0 4.712-2.03 4.712-4.535V.059z" fill="#455a64"/><path d="M2.168.06v.075a2.692 2.692 0 1 0 5.385 0V.059z" fill="#fff"/><path d="M5.692.06H4.428c-.01 0-.022.005-.032.007.156.033.242.388.21.624a.72.72 0 0 1-.712.617c.104.471.488.85.999.922A1.187 1.187 0 0 0 6.24 1.223C6.308.743 6.078.06 5.69.06z" fill="#37474f"/></g><path d="M25.514-.27l-4.202 7.697C19.838 10.17 6.858 34.465 6.858 43.243v.516L12.8 59.573c-.8 7.258-2.203 21.643-1.78 28.21h5.73c-.354-3.787.648-17.008 1.903-28.25l.076-.677-1.075-2.892c3.694-3.868 6.285-9.193 8.073-14.261l.174 1.235 5.869 9.629 2.291-.983c.058-.024 5.935-2.523 11.643-2.523 5.672 0 11.646 2.5 11.702 2.525l2.29.976 5.86-9.626.23-1.608c1.769 5.117 4.358 10.536 8.07 14.49l-1.127 3.035.076.678c1.259 11.286 2.266 24.564 1.916 28.252h5.677c.406-6.567-1.05-20.952-1.848-28.208l5.838-15.817v-.514c0-8.779-12.876-33.074-14.347-35.816L65.923-.27l-5.897 41.229-2.723 4.478c-2.628-.882-7.1-2.11-11.603-2.11-4.498 0-8.94 1.225-11.557 2.108l-2.722-4.476-2.07-14.452a.832.832 0 0 0 .006-.071l-.016-.004zm-3.166 18.39l1.206 8.407c-.46 3.143-2.561 15.47-8.198 23.24l-2.598-6.99c.325-4.554 5.067-15.462 9.59-24.656zm46.763 0c4.523 9.194 9.267 20.104 9.592 24.657L76.166 49.6c-6.09-8.553-8-22.459-8.166-23.73z" fill="#607d8b" stroke-width="1.6"/></g></symbol><symbol viewBox="0 0 24 24" id="solidity" xmlns="http://www.w3.org/2000/svg"><path d="M5.8 14.05l6.253 8.61 6.252-8.61-6.254 3.807z" fill="#0288d1" stroke-width="4.553" stroke-linejoin="round"/><path d="M12.051 1.347L5.8 11.833l6.252 3.807 6.254-3.807z" fill="#0288d1" stroke-width="5.025" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 120 120" id="sonar" xmlns="http://www.w3.org/2000/svg"><style>.a,.b{fill:#fff}.b{stroke:#fff;stroke-miterlimit:10}</style><path d="M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422.426-.427.709-.853.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957c0-.995.141-1.991.425-2.986-4.265-2.702-8.53-3.838-14.787-3.838z" fill="#1e88e5" stroke-width="1.422"/></symbol><symbol viewBox="0 0 412 395" id="stylelint" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-white</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#cfd8dc" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 412 395" id="stylelint_light" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-black</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#546e7a" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 200.00001 200.00001" id="stylus" xmlns="http://www.w3.org/2000/svg"><path d="M126.814 155.9c14.64-17.51 16.362-35.595 5.024-69.18-7.177-21.24-19.09-37.602-10.334-50.807 9.329-14.065 29.135-.43 12.63 18.371l3.301 2.297c19.806 2.296 29.566-24.83 14.783-32.58C113.179 3.621 79.02 42.803 94.09 88.156c6.458 19.232 15.5 39.613 8.18 55.83-6.314 13.923-18.514 22.103-26.695 22.39-17.079.862-5.74-38.32 13.922-48.08 1.722-.861 4.162-2.01 1.866-4.88-24.256-2.727-38.464 8.468-46.645 24.112-23.825 45.497 45.21 62.29 82.095 18.371z" fill="#c0ca33" stroke-width="1.435"/></symbol><symbol viewBox="0 0 24 24" id="swc" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jba"><stop offset="0" stop-color="#791223"/><stop offset="1" stop-color="#d92f3c"/></linearGradient><linearGradient xlink:href="#jba" id="jbb" x1="12.356" y1="21.559" x2="12.356" y2="2.949" gradientUnits="userSpaceOnUse"/></defs><path d="M6 3c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.17-.93-.46-1.27l-1.39-1.68C18.88 3.21 18.47 3 18 3H6zm-.07 1h12l.94 1H5.12l.81-1z" fill="url(#jbb)"/><path style="line-height:125%" d="M11.053 11.918h-.008c-.244.022-.475.054-.676.11a2.9 2.9 0 0 0-.856.412 3.399 3.399 0 0 0-.67.683 9.36 9.36 0 0 0-.586.95c-.07.131-.134.244-.201.365v.001h-.002l-.768 1.372-.003-.001c-.136.253-.264.485-.38.686-.123.212-.26.39-.411.539a1.599 1.599 0 0 1-.52.34c-.04.016-.092.024-.138.036h-.567v1.383H5.834v-.001c.245-.02.477-.053.679-.11a2.9 2.9 0 0 0 .856-.411c.245-.185.469-.413.67-.683.195-.275.39-.591.585-.95.07-.131.135-.244.202-.366l.004.001.002-.002.02-.038H10.948v-1.378h-.19v-.001H9.624c.125-.234.246-.452.355-.64.123-.21.259-.39.41-.538.152-.148.325-.26.52-.34.04-.015.091-.024.136-.035h.57V13.3h-.002v-1.381h-.56v-.001z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol viewBox="0 0 24 24" id="swift" xmlns="http://www.w3.org/2000/svg"><path d="M17.09 19.72c-2.36 1.36-5.59 1.5-8.86.1A13.807 13.807 0 0 1 2 14.5c.67.55 1.46 1 2.3 1.4 3.37 1.57 6.73 1.46 9.1 0-3.37-2.59-6.24-5.96-8.37-8.71-.45-.45-.78-1.01-1.12-1.51 8.28 6.05 7.92 7.59 2.41-1.01 4.89 4.94 9.43 7.74 9.43 7.74.16.09.25.16.36.22.1-.25.19-.51.26-.78.79-2.85-.11-6.12-2.08-8.81 4.55 2.75 7.25 7.91 6.12 12.24-.03.11-.06.22-.05.39 2.24 2.83 1.64 5.78 1.35 5.22-1.21-2.39-3.48-1.65-4.62-1.17z" fill="#fe5e2f"/></symbol><symbol viewBox="0 0 24 24" id="table" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5m4 7.5h-4v2h1l-2 1.67L10 13h1v-2H7v2h1l3 2.5L8 18H7v2h4v-2h-1l2-1.67L14 18h-1v2h4v-2h-1l-3-2.5 3-2.5h1v-2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 200 200" id="terraform" xmlns="http://www.w3.org/2000/svg"><g transform="translate(177.03 -58.705) scale(.92881)" fill="#5c6bc0" stroke="#b0aff5" stroke-linejoin="round"><g stroke-width=".288"><path transform="skewY(26.439) scale(.89541 1)" d="M-203.8 170.95h64.714v51.88H-203.8zM-124.37 171.04h64.714v51.88h-64.714zM-124.37 236.09h64.714v51.88h-64.714z"/></g><path transform="skewY(-22.59) scale(-.92328 1)" stroke-width=".284" d="M-19.172 128.27h62.76v51.88h-62.76z"/></g></symbol><symbol viewBox="0 0 24 24" id="test-js" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="test-jsx" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="test-ts" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tex" xmlns="http://www.w3.org/2000/svg"><g font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-linejoin="miter"><text style="line-height:125%" x="9.914" y="364.919"><tspan x="9.914" y="364.919" font-size="287.5">T</tspan></text><text style="line-height:125%" x="136.374" y="435.558"><tspan x="136.374" y="435.558" font-size="287.5">E</tspan></text><text style="line-height:125%" x="307.819" y="361.201"><tspan x="307.819" y="361.201" font-size="287.5">X</tspan></text></g></symbol><symbol viewBox="0 0 24 24" id="todo" xmlns="http://www.w3.org/2000/svg"><path d="M3 5h6v6H3V5m2 2v2h2V7H5m6 0h10v2H11V7m0 8h10v2H11v-2m-6 5l-3.5-3.5 1.41-1.41L5 17.17l4.59-4.58L11 14l-6 6z" fill="#42a5f5"/></symbol><symbol id="travis" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style id="jkstyle2">.jkst0{fill:#cb3349}.jkst1{fill:#f4edae}.jkst2{fill:#e6ccad}.jkst3{fill:#656c67}.jkst4{fill:#e5caa3}.jkst5{fill:#c7b39a}.jkst6{fill:#ebd599}.jkst7{fill:#2d3136}.jkst8{fill:#edf6fa}.jkst9{opacity:.8}.jkst10{opacity:.75;fill:#ebd599}</style><g id="jkg99" transform="translate(11.017 12.484) scale(.8858)"><g id="jkg10"><path class="jkst0" d="M47.781 86.572s-31.118 21.903-32.335 30.247l2.335-.48S55.045 91.64 84.584 88.628l.669-3.749z" id="jkpath4" fill="#cb3349"/><path class="jkst0" d="M96.629 83.442l-24.511 17.385 1.325 1.063c.999-.806 43.539-13.798 43.539-13.798l8.969-5.623c-6.018.749-29.322.973-29.322.973z" id="jkpath6" fill="#cb3349"/><path class="jkst0" d="M117.932 104.469c17.405 0 43.495-17.046 43.495-17.046l-8.434-1.605c-.417.417-13.6-.462-13.6-.462l-6.258-1.738-14.951 17.036-1.217 2.956c1.075-.437.965.859.965.859z" id="jkpath8" fill="#cb3349"/></g><path class="jkst0" d="M174.728 158.832l-5.377 1.514-24.843-.537-15.541-12.085-18.784 4.7-21.726-1.88-12.166 13.294-22.828 6.819-11.398-3.534-.574-.494 5.116 12.527s11.588 12.424 18.061 13.885c6.472 1.461 18.165-.105 26.935-1.463 8.769-1.357 15.764-4.489 18.582-9.603 2.818-5.117 3.236-6.578 3.236-6.578s8.353 11.797 15.556 13.155c7.203 1.357 28.605-5.952 28.605-5.952s13.051-3.549 15.346-8.038c2.297-4.489 8.353-19.209 8.353-19.209zM44.456 169.038l-.361-.166-2.013-1.736z" id="jkpath12" fill="#cb3349"/><g id="jkg97"><path class="jkst1" d="M195.832 70.085a48.125 48.125 0 0 0-.21-2.009 26.472 26.472 0 0 0-.215-1.424c-1.793-1.509-3.831-2.851-5.952-4.071-2.299-1.343-4.704-2.546-7.159-3.663-2.438-1.15-4.942-2.191-7.461-3.207a134.313 134.313 0 0 0-3.798-1.477c-1.269-.495-2.55-.956-3.835-1.424 2.697.447 5.366 1.059 8.015 1.741 1.723.446 3.437.945 5.14 1.477-12.112-31.655-41.07-52.27-72.687-52.27-31.622 0-60.577 20.615-72.686 52.27a109.044 109.044 0 0 1 5.137-1.477c2.653-.682 5.323-1.294 8.018-1.741-1.289.468-2.567.929-3.84 1.424-1.267.472-2.536.967-3.798 1.477-2.519 1.016-5.016 2.057-7.46 3.207-2.45 1.117-4.857 2.32-7.156 3.663-2.121 1.219-4.157 2.562-5.957 4.071-.075.457-.151.951-.21 1.424a51.768 51.768 0 0 0-.21 2.009 51.354 51.354 0 0 0-.177 4.061 59.216 59.216 0 0 0 .5 8.11c.37 2.692.864 5.366 1.595 7.951.36 1.295.768 2.572 1.24 3.808.237.617.495 1.225.764 1.816.134.294.274.585.413.864l.172.328c.199.101.408.204.607.3l1.204.575c.671.305 1.6.746 2.368 1.09.043-.037.086-.075.123-.114l-2.235-8.513c.474-.13 4.718-1.225 12.032-2.617a38.816 38.816 0 0 1-1.772-.381c-1.665-.414-3.309-.919-4.899-1.564a22.415 22.415 0 0 1-2.309-1.115c-.742-.426-1.472-.908-2.037-1.548 8.036 2.622 24.64 1.434 39.399-.091 13.499-1.391 27.029-2.293 40.63-2.32 13.602.027 27.137.929 40.63 2.32 14.766 1.525 31.37 2.713 39.405.091-.564.64-1.293 1.123-2.035 1.548a22.5 22.5 0 0 1-2.308 1.115c-1.592.645-3.234 1.15-4.899 1.564-.247.059-.496.113-.743.166 8.02 1.488 12.689 2.697 13.188 2.831l-2.138 8.11c.43-.194.864-.381 1.29-.574l1.202-.575c.2-.097.403-.199.607-.3l.166-.328c.146-.279.286-.57.419-.864.27-.591.528-1.199.764-1.816a42.235 42.235 0 0 0 1.241-3.808c.731-2.585 1.225-5.259 1.595-7.951.345-2.685.526-5.398.501-8.11a50.874 50.874 0 0 0-.179-4.059z" id="jkpath14" fill="#f4edae"/><path class="jkst2" d="M116.787 182.661c-1.064.16-2.128.295-3.186.375-.682.033-1.404.102-2.059.102l-.242.005c.822-1.837 1.446-3.26 1.919-4.339.963 1.08 2.188 2.417 3.568 3.857z" id="jkpath16" fill="#e6ccad"/><path class="jkst2" d="M119.101 185.018c3.304 3.272 7.398 5.146 11.904 5.479-7.569 3.074-14.702 4.26-20.197 4.63-5.478.367-11.032-.279-16.474-1.771.456-.082.79-.14 1.193-.189.447-.054 10.206-1.327 14.605-7.868l.413.009 1.08-.009c.731 0 1.395-.06 2.094-.087a43.69 43.69 0 0 0 4.878-.703c.167.171.333.338.504.509z" id="jkpath18" fill="#e6ccad"/><path class="jkst3" d="M128.464 87.071a98.82 98.82 0 0 1-1.048 1.343c-1.933 2.444-4.614 5.57-7.794 8.627a369.585 369.585 0 0 0-11.404-.177c-6.46 0-12.655.171-18.537.457 8.311-3.449 18.296-6.818 29.109-8.842a113.323 113.323 0 0 1 9.674-1.408z" id="jkpath20" fill="#656c67"/><path class="jkst3" d="M79.821 90.792c-2.966 2.084-6.317 4.744-9.566 7.971a360.155 360.155 0 0 0-21.567 2.81c9.207-4.232 19.713-8.127 31.133-10.781z" id="jkpath22" fill="#656c67"/><path class="jkst3" d="M181.48 107.969l-3.384 23.679-16.212 11.355-42.283-4.807-6.365-20.961a1.383 1.383 0 0 0-1.108-.971c-1.567-.253-2.953-.382-4.108-.382-1.16 0-2.541.129-4.115.382-.522.086-.95.461-1.106.971l-6.209 20.45-42.047 9.357-16.662-11.672-3.283-26.572c.715-.404 1.441-.806 2.176-1.209 1.031-.222 2.191-.457 3.475-.704l3.094 25.073c.048.392.264.741.586.967l11.462 8.032a1.425 1.425 0 0 0 1.101.213l34.57-7.692c.119-.027.237-.069.344-.124a1.39 1.39 0 0 0 .682-.827l6.225-20.498c1.67-.43 5.947-1.429 9.706-1.429 3.749 0 8.03.999 9.701 1.429l6.225 20.498c.161.532.624.912 1.176.977l34.57 3.927c.335.037.677-.05.952-.242l11.469-8.025c.31-.22.52-.566.573-.946l3.062-21.421c2.301.444 4.224.846 5.733 1.172z" id="jkpath24" fill="#656c67"/><path class="jkst3" d="M185.751 93.119l-2.976 11.29c-6.086-1.342-19.456-3.975-37.654-5.747 5.946-2.535 12-5.715 17.531-9.69 10.829 1.53 18.78 3.169 23.099 4.147z" id="jkpath26" fill="#656c67"/><g id="jkg32"><path class="jkst4" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath28" fill="#e5caa3"/><path class="jkst4" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath30" fill="#e5caa3"/></g><g id="jkg38"><path class="jkst5" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath34" fill="#c7b39a"/><path class="jkst5" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath36" fill="#c7b39a"/></g><path class="jkst2" d="M187.481 115.502c.508.419.911 1.504.456 6.558-.559 6.188-3.16 17.049-4.771 18.8-1.778.344-5.505-.064-7.778-.595.393-1.559.505-2.306.822-3.9l3.975-2.781c.317-.22.526-.566.58-.941l2.778-19.466c1.686.912 3.421 1.899 3.938 2.325z" id="jkpath40" fill="#e6ccad"/><path class="jkst2" d="M40.937 140.908c.199.704.408 1.407.624 2.1-2.139.628-6.495 1.23-8.465.886-1.633-1.645-4.679-12.966-5.345-18.978-.543-4.871-.162-5.924.333-6.334.575-.483 2.728-1.708 4.593-2.707l2.519 20.449c.048.393.257.741.586.967z" id="jkpath42" fill="#e6ccad"/><path class="jkst2" d="M121.347 141.194l-.151 1.305s-4.581 4.248-11.956 5.199c-7.375.95-13.171-3.582-13.171-3.582.242.788.586 2.567 2.256 4.086a53.184 53.184 0 0 0-6.313-.393c-.804 0-1.616.023-2.401.061-4.539.237-10.924 7.1-15.414 14.014-2.203.697-9.089 2.883-17.06 5.237-7.44-10.309-11.098-20.842-11.469-21.932l.005-.006c-.15-.419-.301-.839-.441-1.268l1.913 1.338v.005l4.726 3.309 1.58 1.101c.236.167.515.253.794.253.102 0 .204-.011.305-.031l43.435-9.67a1.385 1.385 0 0 0 1.025-.95l6.194-20.39c1.069-.145 2.008-.22 2.814-.22.801 0 1.746.075 2.815.22l6.374 20.997c.162.532.624.919 1.171.977z" id="jkpath44" fill="#e6ccad"/><path class="jkst2" d="M170.926 140.066l1.402-.984c-.232.973-.484 1.94-.747 2.896-1.949 6.248-4.25 11.774-6.805 16.656-.565.039-1.161.061-1.8.061-1.972 0-3.986-.167-6.215-.371-3.868-.355-10.007-1.058-11.946-1.283-1.67-1.332-7.385-5.873-12.14-9.615-.187-.151-.348-.291-.505-.42-.837-.708-1.789-1.513-3.717-1.513-1.751 0-4.308.638-10.489 2.508 3.212-2.401 3.233-5.5 3.233-5.5l.151-1.305 40.748 4.629a1.41 1.41 0 0 0 .955-.241l4.094-2.868z" id="jkpath46" fill="#e6ccad"/><path class="jkst6" d="M140.937 54.337c.124 3.625.033 10.194-1.655 16.345a1.335 1.335 0 0 0 0 .704 259.298 259.298 0 0 0-6.446-.591c2.412-5.054 2.938-10.436 3.052-12.332 1.852-1.317 3.696-2.896 5.049-4.126z" id="jkpath48" fill="#ebd599"/><path class="jkst6" d="M79.456 58.462c.112 1.896.638 7.267 3.046 12.317-2.149.171-4.297.37-6.441.596a1.328 1.328 0 0 0 0-.694c-1.686-6.139-1.772-12.714-1.654-16.345 1.353 1.231 3.19 2.81 5.049 4.126z" id="jkpath50" fill="#ebd599"/><path class="jkst7" d="M151.835 125.675c-2.89-1.396-6.059.377-11.828.484-4.292.151-7.896.198-8.132-6.543-.237-6.747 2.513-12.326 6.805-12.478 4.292-.15 8.207 5.2 8.735 11.931.145 1.854-.06 3.207-.521 4.21 3.996-.477 4.899 2.235 4.941 2.396zm-13.488-9.878a2.203 2.203 0 0 0 2.154-2.235 2.186 2.186 0 0 0-2.235-2.153 2.194 2.194 0 0 0 .081 4.388z" id="jkpath52" fill="#2d3136"/><circle transform="rotate(-1.049 138.093 113.428)" class="jkst8" cx="138.307" cy="113.602" id="jkellipse54" r="2.194" fill="#edf6fa"/><path class="jkst7" d="M83.484 120.953c.063 6.747-3.509 6.339-7.806 6.381-.435.011-.848.016-1.258.022-.482.011-.944.016-1.39.005-4.168-.005-6.833-.194-9.19 1.079.058-.145 1.09-2.461 4.835-3.4-.414-.914-.673-2.181-.742-3.937-.257-6.741 3.9-12.269 8.197-12.306 4.292-.042 7.289 5.411 7.354 12.156zm-6.634-3.529a2.195 2.195 0 1 0-.122-4.388 2.195 2.195 0 0 0 .122 4.388z" id="jkpath56" fill="#2d3136"/><circle transform="rotate(-1.473 76.78 115.216)" class="jkst8" cx="76.79" cy="115.23" id="jkellipse58" r="2.195" fill="#edf6fa"/><g class="jkst9" id="jkg64" opacity=".8"><path class="jkst6" d="M50.691 75.155s.667-8.692 2.03-12.023c.702-1.717 4.996-2.81 8.276-3.591 3.278-.78 8.508-2.342 9.524 2.264 1.015 4.606 2.653 7.963 3.746 9.446l-1.404-18.97-22.562 5.464-1.484 16.786.703 1.327 1.171-.703" id="jkpath60" fill="#ebd599"/><path class="jkst6" d="M164.855 75.155s-.666-8.692-2.029-12.023c-.703-1.717-4.997-2.81-8.275-3.591-3.28-.78-8.51-2.342-9.526 2.264-1.013 4.606-2.654 7.963-3.748 9.446l1.407-18.97 22.562 5.464 1.483 16.786-.703 1.327-1.171-.703" id="jkpath62" fill="#ebd599"/></g><path class="jkst10" d="M132.965 18.378s-.598 45.49-11.224 45.49h-14.875-12.752c-10.626 0-11.484-45.47-11.484-45.47l-5.22 15.438.085 21.183 3.707 2.947 1.685 9.096 2.357 5.307 45.482.084 2.105-3.791 1.769-6.4.254-4.043 5.023-14.341z" id="jkpath66" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M166.429 60.794s2.187 15.692 7.974 18.522c5.788 2.829 0 0 0 0l-8.103-2.444z" id="jkpath68" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M48.908 60.794s-2.187 15.692-7.975 18.522c-5.788 2.829 0 0 0 0l8.104-2.444z" id="jkpath70" opacity=".75" fill="#ebd599"/><path class="jkst7" d="M167.987 76.8c2.755.902 5.526 1.858 8.036 3.325-1.343-.532-2.729-.913-4.126-1.257a70.385 70.385 0 0 0-4.201-.924c-2.82-.531-5.65-.982-8.498-1.327-2.841-.37-5.687-.682-8.546-.924-2.858-.241-5.709-.483-8.573-.65-11.446-.704-22.924-.88-34.41-.892-11.483.006-22.962.221-34.409.897-2.862.166-5.715.409-8.572.651-2.857.241-5.71.548-8.546.923-2.847.345-5.678.796-8.498 1.327-1.407.264-2.81.57-4.206.919-1.391.344-2.783.725-4.126 1.257 2.509-1.466 5.28-2.427 8.041-3.331.232-.075.467-.139.703-.214-.015-.059-.032-.113-.043-.177-.048-.317-1.069-7.859.709-18.645.086-.516.456-.935.962-1.075l2.917-.831c.634-22.625 9.952-33.266 10.243-33.594-8.326 13.397-8.25 29.286-8.106 32.986l18.128-5.152c.016-.005.026-.005.042-.01.076-.016.151-.027.226-.032.021 0 .049-.006.075-.006a1.19 1.19 0 0 1 .297.027c.015 0 .031.011.053.016.075.016.145.042.224.075.033.016.054.033.086.049.058.033.119.07.177.112.016.011.034.016.049.033l.032.032c.016.016.037.027.054.044.012.016.494.493 1.262 1.209-.182-5.973.102-23.108 8.262-37.31-.172.498-6.646 19.428-4.415 40.645.724.58 1.486 1.149 2.229 1.649.359.247.58.655.585 1.09.006.07.161 6.833 3.148 12.586.042.086.074.177.102.268 7.429-.505 14.878-.709 22.312-.714 7.436.005 14.88.22 22.307.731.027-.097.06-.193.109-.285 2.986-5.753 3.142-12.516 3.142-12.586.01-.436.231-.843.591-1.09.741-.5 1.493-1.069 2.224-1.649 2.234-21.217-4.24-40.147-4.411-40.645 8.153 14.201 8.444 31.336 8.262 37.31a62.536 62.536 0 0 0 1.261-1.209c.016-.016.039-.027.053-.044.012-.01.018-.021.033-.032.016-.016.033-.022.049-.033.06-.042.119-.079.177-.118.028-.01.054-.027.081-.043.081-.033.155-.059.236-.08.016 0 .033-.011.049-.011.096-.021.2-.032.296-.027.027 0 .049.006.07.006.075.005.156.016.231.032.012.006.028.006.042.01l18.129 5.152c.146-3.7.221-19.59-8.104-32.986.289.328 9.609 10.969 10.237 33.594l2.922.831c.499.14.875.559.962 1.075 1.777 10.786.752 18.328.708 18.645-.01.065-.026.124-.042.182.239.07.47.139.707.215zm-3.297-.968c.14-1.207.789-7.809-.591-16.801l-20.52-5.833c.184 3.475.265 11.012-1.707 18.199a1.619 1.619 0 0 1-.101.258c.203.021.408.037.606.064 5.769.661 11.511 1.584 17.189 2.83 1.712.398 3.426.823 5.124 1.283zm-25.409-5.151c1.688-6.15 1.779-12.72 1.655-16.345-1.353 1.23-3.197 2.809-5.049 4.125-.114 1.896-.64 7.278-3.052 12.332 2.149.173 4.298.366 6.446.591a1.33 1.33 0 0 1 0-.703zm-56.78.098c-2.408-5.05-2.934-10.422-3.046-12.317-1.858-1.316-3.696-2.895-5.049-4.125-.119 3.631-.032 10.206 1.654 16.345.065.237.058.473 0 .694 2.145-.227 4.292-.425 6.441-.597zm-8.933.864a1.65 1.65 0 0 1-.098-.247c-1.975-7.187-1.889-14.723-1.712-18.199L51.244 59.03c-1.38 8.982-.736 15.583-.597 16.797 1.703-.462 3.411-.887 5.131-1.284 2.835-.628 5.693-1.154 8.556-1.638 2.869-.478 5.747-.843 8.626-1.192.205-.027.404-.042.608-.07z" id="jkpath72" fill="#2d3136"/><g id="jkXMLID_1_"><g id="jkg78"><path class="jkst7" d="M129.293 18.973v17.025h-12.068v-4.974h-2.72v22.981h4.109v12.85H97.505v-12.85h4.092v-22.98h-2.711v4.974h-12.06V18.973zm-3.626 13.408v-9.789H90.443v9.789h4.816v-4.974h9.964v30.225h-4.1v5.606h13.865v-5.606h-4.1V27.407h9.964v4.974z" id="jkpath74" fill="#2d3136"/><path class="jkst0" id="jkpolygon76" fill="#cb3349" d="M101.123 57.632h4.1V27.407h-9.964v4.974h-4.816v-9.79h35.224v9.79h-4.816v-4.974h-9.964v30.225h4.1v5.606h-13.864z"/></g></g><path class="jkst3" d="M30.694 93.119c1.759-.399 4.136-.907 7.051-1.47a104.37 104.37 0 0 0-6.222 4.597z" id="jkpath83" fill="#656c67"/><path class="jkst5" d="M95.111 139.78s.492 3.165-3.938 4.519c-4.428 1.355-32.482 9.716-35.682 9.263-3.199-.451-11.319-5.874-11.319-5.874l-1.969-7.004 12.016 7.492z" id="jkpath85" fill="#c7b39a"/><path class="jkst5" d="M120.242 139.167s-.354 3.182 4.131 4.345c4.484 1.161 32.875 8.295 36.05 7.704 3.176-.591 11.053-6.361 11.053-6.361l1.663-7.084-11.045 6.588z" id="jkpath87" fill="#c7b39a"/><path class="jkst5" d="M28.412 133.956s3.887 7.775 10.166 5.083l4.485 1.645-.448 3.29-9.419 1.195-2.541-1.494z" id="jkpath89" fill="#c7b39a"/><path class="jkst5" d="M187.551 131.822s-6.353 8.115-12.632 5.424l-2.019 1.302.448 3.289 9.419 1.196 2.54-1.495z" id="jkpath91" fill="#c7b39a"/><path class="jkst5" d="M89.279 192.904s23.03 11.611 49.106-4.188l-8.374-.571s-18.272 7.232-32.738 3.235z" id="jkpath93" fill="#c7b39a"/><path class="jkst7" d="M112.626 171.509l1.594 1.899c.036.046 3.577 4.26 7.906 8.552 2.879 2.853 6.357 4.297 10.343 4.297 1.361 0 2.791-.175 4.235-.523 1.34-.326 2.796-.673 4.287-1.03 5.384-1.287 11.482-2.749 14.438-3.577.585-.166 1.238-.315 1.925-.472 3.935-.909 9.329-2.163 12.187-7.889 2.149-4.297 5.047-9.874 7.197-13.961-1.863.859-3.816 1.79-5.203 2.52-2.138 1.123-4.938 1.667-8.558 1.667-2.152 0-4.266-.181-6.605-.389-4.675-.43-12.586-1.361-12.667-1.372l-.606-.067-.478-.383c-.071-.052-7.003-5.575-12.606-9.981-.227-.186-.434-.358-.621-.513-.59-.503-.59-.503-.942-.503-1.797 0-7.02 1.62-18.462 5.167l-.703.223-.689-.26c-.078-.026-7.585-2.81-16.581-2.81-.736 0-1.47.019-2.185.056-.901.046-5.958 2.448-12.425 12.68l-.419.657-.741.238c-.107.037-11.238 3.63-23.042 7.005l-.766.218-.725-.337c-.077-.031-4.696-2.174-9.091-4.194 2.397 3.541 5.462 7.958 8.159 11.422 4.711 6.067 10.649 11.674 22.034 11.674 1.428 0 2.945-.088 4.503-.265 11.581-1.309 14.563-1.837 16.168-2.117.543-.092.973-.171 1.522-.238.088-.011 9.571-1.237 12.232-7.206 2.744-6.134 3.298-7.595 3.319-7.651l.968-2.583s.12-.669.317-.877c0 .005 0 .005.005.005l.019.016c.305.219.757.902.757.902zM40.499 55.71c-2.516 1.014-5.016 2.06-7.46 3.209-2.449 1.119-4.856 2.32-7.155 3.66-2.121 1.222-4.157 2.563-5.954 4.076-.077.455-.149.952-.211 1.423a51.357 51.357 0 0 0-.388 6.068c-.026 2.713.16 5.426.502 8.112.372 2.692.864 5.369 1.594 7.952a41.963 41.963 0 0 0 1.243 3.804c.233.623.492 1.228.762 1.818.134.294.274.585.413.864l.172.326c.201.104.409.207.605.3l1.206.574c.673.311 1.6.751 2.366 1.093.046-.037.088-.078.124-.114l-2.231-8.511c.471-.129 4.717-1.227 12.032-2.619a33.744 33.744 0 0 1-1.775-.379 36.704 36.704 0 0 1-4.898-1.563 22.857 22.857 0 0 1-2.309-1.119c-.741-.425-1.471-.905-2.035-1.547 8.035 2.624 24.637 1.433 39.398-.088 13.501-1.393 27.028-2.293 40.628-2.325 13.6.031 27.138.931 40.63 2.325 14.77 1.522 31.374 2.713 39.406.088-.564.642-1.293 1.122-2.034 1.547-.739.42-1.522.782-2.309 1.119a36.965 36.965 0 0 1-4.903 1.563c-.244.056-.492.114-.741.166 8.02 1.486 12.689 2.697 13.186 2.832l-2.138 8.107c.43-.192.864-.377 1.288-.574l1.207-.574c.196-.094.404-.196.606-.3l.166-.326c.144-.279.284-.57.419-.864.27-.591.528-1.196.767-1.818.471-1.231.879-2.51 1.236-3.804.731-2.583 1.228-5.26 1.595-7.952.346-2.686.528-5.4.502-8.112a52.755 52.755 0 0 0-.176-4.059 51.573 51.573 0 0 0-.213-2.009 29.83 29.83 0 0 0-.213-1.423c-1.797-1.513-3.831-2.853-5.954-4.076-2.299-1.34-4.704-2.541-7.159-3.66-2.438-1.149-4.943-2.195-7.46-3.209a140.105 140.105 0 0 0-3.801-1.476c-1.267-.491-2.552-.956-3.835-1.423 2.696.445 5.369 1.06 8.013 1.739 1.724.446 3.444.948 5.141 1.481-12.11-31.658-41.07-52.272-72.685-52.272-31.622 0-60.576 20.614-72.684 52.272a107.832 107.832 0 0 1 5.135-1.481c2.651-.678 5.322-1.294 8.02-1.739-1.29.466-2.568.931-3.842 1.423-1.268.47-2.535.967-3.799 1.475zm159.43 18.316a53.972 53.972 0 0 1-.258 8.733 55.462 55.462 0 0 1-1.619 8.605c-.4 1.414-.86 2.811-1.404 4.198a38.295 38.295 0 0 1-.89 2.071c-.161.341-.331.678-.523 1.025l-.284.512a8.975 8.975 0 0 1-.348.574l-.294.457-.461.237c-.492.254-.895.445-1.342.653l-1.298.585a88.22 88.22 0 0 1-2.62 1.065c-.611.239-1.15.457-1.662.674l-1.444 5.487c-.036-.009-.471-.12-1.283-.315l-.078.574c1.594.833 4.726 2.522 5.793 3.403 2.148 1.775 2.299 4.587 1.823 9.841-.244 2.697-1.139 7.946-2.381 12.767-2.144 8.298-3.283 9.273-4.753 9.649-.746.192-1.894.383-3.008.383-2.266 0-5.353.063-7.429-.439-.533 1.888-2.055 6.812-5.068 12.962.151-.073.3-.135.435-.207 3.717-1.952 10.861-5.064 11.162-5.199l5.643-2.452-2.89 5.435c-.067.118-6.264 11.773-10.059 19.383-3.769 7.538-10.835 9.179-15.065 10.151-.637.151-1.241.291-1.733.425-3.035.854-9.18 2.319-14.599 3.623-.064.016-.13.033-.197.042a64.057 64.057 0 0 1-10.955 5.411c-14.568 5.518-29.923 5.208-43.844.092a647.05 647.05 0 0 1-9.193 1.097 45.12 45.12 0 0 1-4.985.291c-13.264 0-20.294-6.736-25.425-13.331-5.493-7.062-12.212-17.546-12.497-17.985L31 158.426l6.585 2.961c3.152 1.419 12.524 5.757 15.205 7 .217-.061.43-.124.642-.186-4.457-6.357-8.112-13.605-10.695-21.634-2.195.662-5.576 1.175-8.206 1.175-.961 0-1.822-.072-2.484-.228-1.471-.336-3.148-1.754-5.431-9.795-1.325-4.668-2.314-9.764-2.603-12.387-.57-5.121-.466-7.864 1.662-9.636 1.283-1.071 5.611-3.344 6.507-3.809l-.192-1.58c-13.75 8.08-21.991 15.22-22.157 15.366L0 134.302l7.005-11.047c5.544-8.755 11.948-15.832 17.84-21.284-.244-.098-.471-.196-.71-.294l-1.299-.585a34.907 34.907 0 0 1-1.34-.653l-.461-.237-.295-.457c-.166-.249-.238-.388-.347-.574l-.29-.512c-.181-.347-.358-.684-.518-1.025a30.878 30.878 0 0 1-.89-2.071 44.74 44.74 0 0 1-1.404-4.198 54.745 54.745 0 0 1-1.62-8.605 54.664 54.664 0 0 1-.259-8.733c.078-1.455.218-2.909.419-4.354.104-.725.213-1.45.358-2.17.15-.734.296-1.418.518-2.221l.155-.564.404-.317c2.294-1.802 4.768-3.163 7.284-4.369a78.87 78.87 0 0 1 6.311-2.616c5.943-16.493 16.162-31.118 29.591-41.311C74.337 5.57 90.664 0 107.671 0s33.334 5.57 47.218 16.106c13.43 10.193 23.649 24.819 29.588 41.307a78.282 78.282 0 0 1 6.316 2.62c2.515 1.206 4.99 2.567 7.283 4.369l.404.317.156.564c.227.803.372 1.487.517 2.221.146.72.26 1.445.357 2.17.203 1.443.348 2.897.419 4.352zm-11.995 48.031c.456-5.052.058-6.139-.455-6.554-.513-.43-2.247-1.412-3.935-2.329l-2.779 19.464a1.39 1.39 0 0 1-.58.942l-3.977 2.781c-.315 1.593-.429 2.345-.817 3.903 2.273.528 5.999.938 7.775.595 1.612-1.748 4.214-12.61 4.768-18.802zm-5.161-17.648l2.977-11.29c-4.318-.978-12.27-2.615-23.1-4.148-5.53 3.976-11.582 7.155-17.53 9.691 18.199 1.771 31.57 4.406 37.653 5.747zm-4.68 27.237l3.385-23.676a240.127 240.127 0 0 0-5.731-1.169l-3.059 21.422a1.415 1.415 0 0 1-.575.943l-11.472 8.023c-.27.192-.616.28-.947.243l-34.572-3.929a1.391 1.391 0 0 1-1.176-.973l-6.227-20.5c-1.668-.431-5.949-1.43-9.696-1.43-3.764 0-8.041.999-9.708 1.43l-6.228 20.5a1.388 1.388 0 0 1-1.025.947l-34.572 7.692a1.483 1.483 0 0 1-.306.033 1.36 1.36 0 0 1-.792-.25l-11.467-8.029a1.396 1.396 0 0 1-.585-.968l-3.091-25.072c-1.284.249-2.443.487-3.479.703-.734.405-1.46.809-2.174 1.213l3.281 26.568 16.666 11.675 42.047-9.354 6.207-20.449a1.389 1.389 0 0 1 1.108-.975c1.574-.253 2.95-.382 4.116-.382 1.153 0 2.536.129 4.105.382.528.083.957.461 1.108.975l6.366 20.956 42.282 4.808zm-8.07-4.411l2.992-20.948c-8.439-1.536-20.78-3.394-35.897-4.554-13.647 4.707-25.077 6.108-25.766 6.155l-.797.057c4.353.374 8.454 1.544 8.66 1.605.452.135.804.481.944.933l6.186 20.366 33.138 3.764zm2.303 11.845l-1.404.983-3.779 2.651-4.095 2.868c-.279.192-.621.28-.954.243l-40.746-4.633-2.966-.337a1.39 1.39 0 0 1-1.171-.977l-6.377-20.998c-1.066-.145-2.014-.219-2.81-.219-.809 0-1.751.073-2.817.219l-6.192 20.392a1.383 1.383 0 0 1-1.025.946l-43.435 9.672c-.103.02-.206.03-.305.03-.279 0-.559-.083-.798-.253l-1.578-1.098-4.726-3.307v-.011l-1.91-1.335c.135.43.289.85.441 1.268l-.006.006c.368 1.092 4.028 11.622 11.467 21.929a873.96 873.96 0 0 0 17.057-5.234c4.488-6.917 10.877-13.777 15.418-14.014a51.12 51.12 0 0 1 2.402-.061c2.221 0 4.344.16 6.31.393-1.671-1.517-2.013-3.298-2.256-4.085 0 0 5.793 4.53 13.17 3.584 7.378-.953 11.959-5.204 11.959-5.204s-.021 3.102-3.236 5.503c6.182-1.869 8.739-2.511 10.489-2.511 1.931 0 2.883.808 3.717 1.519.161.129.322.268.507.419a3519.302 3519.302 0 0 1 12.141 9.614c1.936.227 8.075.926 11.943 1.283 2.23.201 4.245.372 6.217.372.637 0 1.233-.026 1.797-.063 2.558-4.88 4.857-10.411 6.808-16.653.261-.96.516-1.928.743-2.901zm-15.034-51.593c-.01-.006-.02-.012-.031-.012a551.624 551.624 0 0 0-9.826-.651 905.6 905.6 0 0 0-13.667-.668 72.95 72.95 0 0 1-1.574 2.225c-2.479 3.355-7.398 9.51-13.704 14.729 8.926-1.6 24.409-5.56 37.803-14.905.336-.238.668-.486.999-.718zm-29.876.926c.377-.471.729-.926 1.044-1.34-3.281.331-6.512.808-9.67 1.408-10.814 2.024-20.801 5.389-29.11 8.837a383.259 383.259 0 0 1 18.54-.455c3.908 0 7.708.067 11.404.176 3.179-3.056 5.861-6.182 7.792-8.626zm3.587 102.085c-4.503-.332-8.598-2.205-11.903-5.477a271.86 271.86 0 0 0-.502-.512 44.25 44.25 0 0 1-4.881.704c-.698.026-1.361.087-2.091.087l-1.083.011-.413-.011c-4.396 6.539-14.159 7.813-14.605 7.87-.403.046-.734.103-1.191.186 5.442 1.491 10.996 2.138 16.474 1.77 5.492-.367 12.627-1.558 20.195-4.628zm-17.4-7.461a45.604 45.604 0 0 0 3.184-.378 138.958 138.958 0 0 1-3.568-3.857 398.441 398.441 0 0 1-1.92 4.339h.243c.658.001 1.378-.071 2.061-.104zm-3.354-78.632c1.827-1.103 3.582-2.366 5.249-3.712a422.33 422.33 0 0 0-7.278-.072c-10.137 0-19.606.415-28.189 1.061-8.61 4.209-13.875 7.672-13.998 7.76l-8.268 5.514 5.679-8.149a52.452 52.452 0 0 1 2.956-3.857c-9.536 1.066-17.477 2.329-23.41 3.422l3.038 24.632 10.453 7.321 33.184-7.378 6.212-20.464c.104-.337.331-.621.627-.793.098-.063.202-.109.315-.14.192-.052 3.51-.999 7.336-1.465zm3.816-18.788c-2.31-.036-4.623-.057-6.933-.062h-.005c-3.39.005-6.787.041-10.189.109l-6.269 2.971c-.005.005-.041.021-.088.048-.942.46-9.174 4.613-16.919 12.021 6.943-3.65 17.146-8.418 29.153-12.115a144.186 144.186 0 0 1 11.25-2.972zM70.251 98.761c3.251-3.225 6.605-5.886 9.567-7.967-11.415 2.651-21.923 6.543-31.128 10.778a360.846 360.846 0 0 1 21.561-2.811zm2.159-9.949a150.122 150.122 0 0 1 11.813-2.796c-5.798.212-11.6.481-17.393.808-3.366.186-6.715.414-10.065.667-1.678.129-3.345.263-5.007.445-.476.046-.942.098-1.418.16-4.369 2.614-21.127 13.134-32.631 26.889 11.179-7.769 30.654-19.443 54.701-26.173zm-30.85 54.197a68.861 68.861 0 0 1-.621-2.102l-5.162-3.612a1.391 1.391 0 0 1-.586-.969l-2.516-20.449c-1.864.999-4.017 2.225-4.592 2.707-.497.409-.875 1.46-.336 6.332.668 6.01 3.712 17.333 5.348 18.979 1.968.347 6.327-.258 8.465-.886zm-3.815-51.36a229.005 229.005 0 0 0-7.051 1.47l.829 3.127a103.93 103.93 0 0 1 6.222-4.597z" id="jkpath95" fill="#2d3136"/></g></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tune" xmlns="http://www.w3.org/2000/svg"><path d="M6.85 2.852h-2v6h2v-6m12 0h-2v10h2v-10m-16 10h2v8h2v-8h2v-2h-6v2m12-6h-2v-4h-2v4h-2v2h6v-2m-4 14h2v-10h-2v10m4-6v2h2v4h2v-4h2v-2h-6z" fill="#fbc02d" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 50 50" id="twig" xmlns="http://www.w3.org/2000/svg"><path d="M9.727 47.556c-.125-.223-.297-2.168-.183-2.087.034.025.171.267.304.537.132.27.282.487.332.482.123-.011.075-1.196-.1-2.454-.331-2.398-1.176-4.435-2.358-5.69-.2-.212-.344-.4-.319-.419.093-.067 1.327.843 1.842 1.359.293.293.735.825.981 1.181.328.474.465.618.51.534.078-.147-.21-9.903-.376-12.701-.074-1.255.063-1.023.61 1.035 1.064 4.006 1.858 7.922 2.342 11.55.086.637.173 1.172.195 1.19.022.016.092.001.157-.034.888-.483 1.524-.667 2.55-.736.727-.048.945.062.35.178-1.15.222-1.99 1.013-2.344 2.201-.315 1.061-.327 2.707-.024 3.434.152.366.037.426-1.067.56-.716.088-.977.096-1.202.037-.356-.092-1.118-.098-1.195-.008-.031.036-.243.066-.47.066-.38 0-.423-.017-.535-.215zm1.974-3.233c.152-.205.072-.41-.204-.522-.225-.09-.263-.088-.437.025-.21.137-.252.43-.08.554.18.13.607.096.72-.057zm1.248.086a.763.763 0 0 0 .214-.203c.241-.33-.352-.622-.745-.366-.406.265.08.785.531.569zm2.288 3.094c-.033-.039.117-.387.334-.775.216-.387.411-.665.433-.618.07.152-.201 1.28-.33 1.372-.15.108-.354.117-.437.02zM8.2 47.092c-.29-.343-.221-.434.14-.182.176.123.321.263.321.31 0 .165-.279.087-.46-.128zm8.649-.145c0-.053.102-.18.227-.282.25-.204.312-.113.143.207-.095.18-.37.236-.37.075zm8.065-.827c-.243-.025-.48-.088-.527-.141-.11-.125-.114-3.043-.004-3.043.045 0 .132.149.193.331.127.38.228.42.31.124.094-.337.065-3.472-.039-4.297-.449-3.55-1.865-6.124-4.342-7.89-1.086-.774-2.653-1.436-4.047-1.711-.764-.15-.522-.224.598-.182 2.364.089 4.167.706 5.847 2.001a11.046 11.046 0 0 1 2.32 2.502c.453.682.64.854.64.584 0-.07.063-.882.139-1.805.679-8.26 2.396-15.1 4.984-19.86 1.86-3.422 5.108-6.817 7.885-8.244 1.397-.718 2.539-.988 4.02-.952.933.023 1.01.036 1.77.307a6.822 6.822 0 0 1 1.363.662c.612.407 1.309 1.004 1.235 1.058-.026.018-.343-.165-.705-.407-2.657-1.771-5.062-1.52-7.12.742-1.108 1.22-2.651 3.53-3.634 5.443-2.828 5.503-4.541 11.464-5.291 18.413-.163 1.509-.282 3.76-.195 3.703.032-.022.266-.52.518-1.108 1.597-3.723 3.578-6.428 5.79-7.908.672-.449 1.612-.904 1.715-.83.022.016-.172.22-.432.454-1.957 1.754-3.248 3.76-4.232 6.572-.938 2.68-1.366 5.588-1.368 9.3-.002 1.741.188 4.385.366 5.101.125.505.08.546-.585.546-.55 0-2.306.138-3.416.27-.414.05-.817.04-1.609-.036-.58-.056-1.129-.119-1.218-.14-.165-.037-.18-.014-.2.302-.01.186-.098.203-.728.139zm2.507-6.725c.294-.11.375-.22.375-.517 0-.63-1.309-.706-1.524-.088-.074.211.13.51.42.616.297.108.413.106.73-.011zm2.369-.052c.277-.222.318-.364.174-.611-.4-.691-1.755-.307-1.428.404.121.266.299.35.738.354.227 0 .387-.045.516-.147zm3.011 6.681c-.027-.05.088-.268.256-.484.879-1.135 1.22-1.544 1.284-1.544.04 0 .056.037.036.082l-.423.964c-.212.485-.445.924-.519.977-.169.122-.57.125-.634.005zm2.446-.596c0-.121.853-.683.896-.59.018.04-.056.209-.166.376-.168.259-.238.305-.464.305-.164 0-.266-.035-.266-.091zm-13.04-.124c-.177-.159-.493-.656-.462-.725.018-.038.248.1.512.309.264.207.457.405.428.438-.075.088-.371.074-.478-.022z" fill="#9bb92f" stroke-width=".078"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript" xmlns="http://www.w3.org/2000/svg"><path d="M49 51h408v408H49V51zm246.669 314.879l19.463-1.702c.922 7.8 3.067 14.199 6.435 19.198 3.368 4.998 8.597 9.04 15.688 12.124 7.09 3.085 15.067 4.627 23.93 4.627 7.87 0 14.819-1.17 20.845-3.51 6.027-2.34 10.512-5.548 13.455-9.625 2.942-4.077 4.413-8.526 4.413-13.348 0-4.892-1.418-9.164-4.254-12.816-2.836-3.651-7.516-6.718-14.039-9.2-4.183-1.63-13.436-4.165-27.759-7.604s-24.355-6.683-30.099-9.732c-7.445-3.899-12.993-8.739-16.644-14.517-3.652-5.779-5.478-12.249-5.478-19.41 0-7.871 2.234-15.227 6.701-22.069 4.467-6.842 10.99-12.036 19.569-15.581 8.58-3.546 18.116-5.318 28.61-5.318 11.557 0 21.75 1.861 30.577 5.584 8.828 3.722 15.617 9.199 20.368 16.432 4.75 7.232 7.303 15.421 7.657 24.568l-19.782 1.489c-1.064-9.856-4.662-17.301-10.795-22.335-6.133-5.034-15.191-7.551-27.174-7.551-12.479 0-21.573 2.286-27.281 6.86-5.707 4.573-8.561 10.086-8.561 16.538 0 5.602 2.021 10.21 6.062 13.826 3.971 3.617 14.34 7.321 31.109 11.115 16.769 3.793 28.273 7.108 34.513 9.944 9.076 4.183 15.776 9.483 20.101 15.9 4.325 6.417 6.488 13.809 6.488 22.175 0 8.296-2.375 16.113-7.126 23.452-4.751 7.338-11.575 13.046-20.474 17.123-8.898 4.077-18.913 6.116-30.045 6.116-14.11 0-25.933-2.056-35.47-6.169-9.537-4.112-17.017-10.299-22.441-18.559-5.424-8.26-8.278-17.602-8.562-28.025zm-65.728 50.094V278.454h51.583v-18.399H157.938v18.399h51.37v137.519h20.633z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript-def" xmlns="http://www.w3.org/2000/svg"><path d="M457 459H49V51h408v408zM69 71v368h368V71H69z" fill="#0288d1"/><text x="342.219" y="344.544" font-family="ArialMT" font-size="12" fill="#0288d1" transform="translate(-6058.94 -5838) scale(18.1514)"><tspan style="-inkscape-font-specification:sans-serif" font-family="sans-serif" font-weight="400">TS</tspan></text></symbol><symbol viewBox="0 0 24 24" id="url" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h-3v1.9h3a4.1 4.1 0 0 1 4.1 4.1 4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6c0-3.32-2.69-6-6-6M3.9 12A4.1 4.1 0 0 1 8 7.9h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8c-2.26 0-4.1-1.84-4.1-4.1M8 13h8v-2H8v2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="verilog" xmlns="http://www.w3.org/2000/svg"><path d="M17.282 17.08H6.718V6.513h10.564m4.226 4.226V8.627h-2.113V6.514c0-1.173-.95-2.113-2.113-2.113H15.17V2.288h-2.113v2.113h-2.112V2.288H8.83v2.113H6.718c-1.173 0-2.113.94-2.113 2.113v2.113H2.492v2.113h2.113v2.113H2.492v2.113h2.113v2.113a2.113 2.113 0 0 0 2.113 2.113H8.83v2.113h2.113v-2.113h2.112v2.113h2.113v-2.113h2.113a2.113 2.113 0 0 0 2.113-2.113v-2.113h2.113v-2.113h-2.113V10.74m-6.339 2.113h-2.112V10.74h2.112m2.113-2.113H8.831v6.34h6.338z" fill="#ff7043" stroke-width="1.056"/></symbol><symbol viewBox="0 0 24 23.999999" id="vfl" xmlns="http://www.w3.org/2000/svg"><defs><style>.jra{fill:#f05223}.jrb{fill:url(#jra)}</style><radialGradient id="jra" cx="205.45" cy="208.29" r="225.35" gradientTransform="matrix(.04556 0 0 .0456 2.888 2.88)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffd104" offset="0"/><stop stop-color="#faa60e" offset=".35"/><stop stop-color="#f05023" offset="1"/></radialGradient></defs><title>houdinibadge</title><g stroke-width=".046"><path class="jra" d="M19.97 3H4.03A1.03 1.031 0 0 0 3 4.031v4.135C4.548 6.977 6.563 6.21 8.948 6.21c5.107.003 8.35 3.574 8.348 8.081 0 3.13-1.46 5.485-3.746 6.71h6.42A1.03 1.031 0 0 0 21 19.968V4.031a1.03 1.031 0 0 0-1.03-1.03z" fill="#f4511e"/><path class="jrb" d="M3 17.722v2.247A1.03 1.031 0 0 0 4.03 21h1.837C4.474 20.21 3.49 19 3 17.722z" fill="url(#jra)"/><path class="jra" d="M8.948 8.231c-2.586-.09-4.598.86-5.948 2.264v3.163c.918-2.654 3.447-3.87 5.565-3.85 2.647.027 4.689 2.025 4.7 4.284.012 2.159-.892 3.748-3.33 4.14-1.33.213-3.411-.567-3.318-2.578.046-1.037.854-1.622 1.777-1.58-.905 1.213.293 2.102 1.139 1.921 1.048-.224 1.475-1.156 1.475-1.878 0-.762-.718-1.994-2.498-1.951-2.204.052-3.591 1.639-3.638 3.602-.056 2.468 2.253 4.091 4.622 4.121 3.48.046 5.543-2.24 5.539-5.586-.005-3.029-2.434-5.946-6.085-6.072z" fill="#f05223"/></g></symbol><symbol viewBox="0 0 24 24" id="virtual" xmlns="http://www.w3.org/2000/svg"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h7l-2 3v1h8v-1l-2-3h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 281.25 281.25" id="visualstudio" xmlns="http://www.w3.org/2000/svg"><path d="M196.18 101.74l-52.778 42.444 52.778 40.889V101.74m-136.67 110l-30-18.889v-100L62.843 81.74l47.778 37 96.666-89.222 44.444 27.778v172.22l-55.555 22.222-85.111-81.555-51.555 41.555m3.333-48.889l20.667-19.111-20.667-19.778z" fill="#ab47bc" stroke-width="11.111"/></symbol><symbol viewBox="0 0 300 300" id="vscode" xmlns="http://www.w3.org/2000/svg"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0}.icon-white{fill:#fff}</style></defs><title>BrandVisualStudioCode</title><path d="M218.62 29.953l-105.41 96.92L54.301 82.47 29.955 96.64l58.068 53.359-58.068 53.359 24.346 14.212 58.909-44.402 105.41 96.878 51.424-24.976V54.93zm0 63.744v112.6l-74.719-56.302z" fill="#2196f3" stroke-width="17.15"/></symbol><symbol viewBox="0 0 24 24" id="vue" xmlns="http://www.w3.org/2000/svg"><path d="M1.821 4.15l10.21 17.618L22.24 4.235V4.15h-7.692L12.113 8.33 9.691 4.15H1.82z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179H5.937z" fill="#35495e"/></symbol><symbol viewBox="0 0 420 419" id="watchman" xmlns="http://www.w3.org/2000/svg"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="bevel"><path d="M166.95 145.32a93.935 123.23 0 0 1 92.934 3.263" fill="none" stroke-width="18.467"/><path d="M162.92 137.96L44.63 256.25a174.07 173.93 0 0 0 5.705 16.486l123.68-123.68-11.096-11.096zM266.54 144.04l-11.096 11.096 117.16 117.16a174.07 173.93 0 0 0 5.691-16.5l-111.76-111.76zm170.65 170.65v22.193l17.1 17.1 11.096-11.098-28.195-28.195z" fill="#fff" stroke-width="1.963"/><path d="M167.52 273.36a93.935 123.23 0 0 1 92.934-3.263" fill="none" stroke-width="18.467"/><path d="M49.516 144.56a174.07 173.93 0 0 0-.809 2.213 174.07 173.93 0 0 0-4.757 14.344 174.07 173.93 0 0 0-.016.055l119.56 119.56 11.098-11.096-125.07-125.07zM454.87 64.703l-17.668 17.668v22.191l28.764-28.764-11.096-11.096zm-80.984 80.984l-117.86 117.86 11.098 11.096 112.18-112.18a174.07 173.93 0 0 0-5.416-16.777z" fill="#fff" stroke-width="1.963"/></g><image x="21.229" y="20.262" width="378" height="377.1" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGjCAYAAABjSWGNAAAgAElEQVR4AeydB3hUVdrH/+fOpNMF JAFUmivFXtZuIBRRQUUTil1RV3et6Lr6rSu6rg3B1dXVtXeBCCioCASIimLDhkFsgAIJSAkhPZmZ 8z3vjReGMJlMueeWmfc88MzMLaf8zp3855zznvcV4MQEkoxA7sVr01PrUrv4pdZNBNA1AHQRkJ0g ZAcJrYOA7Aipv8/Q6JgUKdBkG0iZISDSDVwSaAfAY3z+/bVSAD56L+mfwA5INAqgSkrUCKA2IOQO IcQOIUU5pNwhNZQLia2Q2OyH3OJBxqaiwk4VzfLlj0wg4QmIhG8hNzDJCEgxJH9DjgfevgGBAyCx vxSyh5DoAWA/CHQH0MEFUOoBbJbAeiHEBiED6wMSv0DgF02In1J2Vq+ZP78fXcOJCSQMARakhOnK 5GrICaO3tE3P8B0kERgkgP6Qop8E+gqgL4Bdo5gEphKAwHpIrIHAjxL4DpCrAo1yVfHsHhsSuN3c tAQmwIKUwJ2bGE2TYsjY33qLQOBwCHk4JA6HhgE08kmM9ilpRQUgvwPE1xD4MiC0r2R67dfFz/eq U1IaZ8oETCLAgmQSSM7GHAIj8td38sN7HAROkpDHATgMTWs15hSQvLn4JLBaQHwqEPgQQiwvmpHz XfLi4JY7kQALkhN7JYnqlDd2w4GQnuMk5AkCOBHAQQD4ubTgGZDAFg1ieQCBj6Dhg7SKmhW8LmUB eC6iRQL8xW8RDZ8wn4AUQ8eVHib9YjCELj4nAOhqfjmcY4wE6iGxAkIsCwDvSel5v7iwa1WMefFt TCBqAixIUSPjG6IhkDtmQw/N4xkKTQ4TEkNZgKKhZ/u1DZD4CAKLIMSik/p3WzF5sgjYXiuuQMIS YEFK2K61p2GTJ0tt2Xebj5EyMArAab+vAdlTGS7VVAI0xSeAd6QQb2uBtIW8V8pUvJwZz9XzM2AG AdpoKqpTh3uEdlYA8gwBdDEjX87D0QQaALlMCLzha5Rz2NTc0X3lmsrxCMk1XeWsig6/YFOWr0Ge JoBzICWNhNo6q4ZcGwsJkFOKT4TUZiOgzSqate8aC8vmohKIAAtSAnWm6qaMHPljWl2bzBGapk2A lKMBZKguk/N3JYHPpMCrHuGbuWj6fqWubAFX2hYCLEi2YHdToVIMKSg7SQAXABgDoJObas91tZWA H5DvCeCV2rrUwg/ndqm0tTZcuOMJsCA5vovsqeDwszd19af4LwbEZQAOtKcWXGoCEaiCwAwB7emi Gd0+TqB2cVNMJMCCZCJMt2dFFnIflJQNh8BEgKbkRIrb28T1dySBlULIpzyBwCsLCntud2QNuVK2 EGBBsgW7swqlvUIer+cSQE7UPWI7q3pcm8QlUAtgtibFU4sKu70PCJm4TeWWRUKABSkSSgl5DTkt 3XSqJuVfJDAiRFyfhGw1N8qxBH4AxJP+hrpnit/otcOxteSKKSXAgqQUr/My18216wMXCOB6AH9w Xg25RklOoEoI8bzw+R5ZNKvnj0nOIumaz4KUJF0+YsyWbL/Xdx0gr5BAxyRpNjfTvQQCkPIdDdqD iwqz33NvM7jm0RBgQYqGlguvbfKmrd2MJrPtNBc2gauc5ASElJ8EIO4/eWD2m+xLL7EfBhakBO3f wfmlR2oCtwI4G4CWoM3kZiURASmwGpAPVLTPeXnFk6IxiZqeNE1lQUqwrs4bW3Y0AoF/QIjT2Vdh gnUuN8cgsFYK3LNPoPzFwsKBDcZBfnU/ARYk9/eh3oIh+WXHaELeIZs8bCdIq7gZTCAsgXUQuK9T oPw5FqawnFxzkgXJNV0VuqJD8zcdDCHvlvpG1tDX8FEmkOAE1gohJp/Yv9vLvMbk7p5mQXJp/w0Z u7mPkP7JACbwGpFLO5GrbS4BiRII/H3xzOw3eZOtuWityo0FySrSJpWTO760s8cv7wQEeVVINSlb zoYJJBAB+bGQ4uaiwpxlCdSopGgKC5JLuvm4/PUZmcJ7NSD/DqCDS6rN1WQCdhKY45f+vxYX9vzJ zkpw2ZETYEGKnJVNV1L4h9JxAuI+9jNnUxdwsW4mQFZ4//E31N/NLomc340sSA7uo2H5Gw7zC+3f AjjFwdXkqjEBNxDYKgVuPbl/9rNs+ODc7mJBcmDf5J61toMnNfVfgLgCgNeBVeQqMQF3EpD4XGja NRyTyZndx4LkqH6RYkj+pouEkFMAdHZU1bgyTCBxCEgIPFvvabx52av7lydOs9zfEhYkh/Th0HM2 95Ye//8ADHVIlbgaTCDRCWwWENcWzcyemegNdUv7WJBs7qncXOnVum66XoBMuZFpc3W4eCaQdAQE xFyfz//n4tk9NiRd4x3WYBYkGzuEjBak0J6WwJE2VoOLZgJMANgJIW49qX+3J9jowb7HgQXJBva0 pyhLeO+QkJPYaMGGDuAimUDLBJYJgSuKZuR81/IlfEYVARYkVWRbyHfouLJcBPCUhOzbwiV8mAkw AXsJ1EOKe3Z07HYvh7mwtiNYkCzinXvx2nRvTdq9EriOw0JYBJ2LYQLxEfgCUlywuDB7VXzZ8N2R EmBBipRUHNcNGVt6hJB4CcCAOLLhW5kAE7CeQB1tqF0yI/thdtiqHj4LkkLG+fnSUy7KbpWQ/wBE isKiOGsmwASUEpBLPBouXji9+3qlxSR55ixIih6A3DEbemhe7WV2+6MIMGfLBKwnsA3AxMUzc96w vujkKJEFSUE/D87fOFoT4lkA+yjInrNkAkzAVgLyv/7MhknFz/eqs7UaCVg4C5KJnTpy5I9p9W3b TBGQf2HDBRPBclZMwHkEVnr8KFg4K2e186rm3hqxIJnUd0PGlO4vPHgdAkeZlCVnwwSYgLMJVEHI yxfP6D7d2dV0T+1YkEzoq6H5ZadLIV/gKToTYHIWTMBlBIQQj6bsrLpp/vx+9S6ruuOqy4IUR5fo VnRa2Z1S4jaeoosDJN/KBNxP4DMhcW5RYc6v7m+KfS1gQYqRvR6zKCXtVQiMjDELvo0JMIEEIiCB LR4p8hcVZr+XQM2ytCmapaUlSGF5+WUDvKnpn7EYJUiHcjOYgAkEBNAlIAKLho7deI0J2SVlFjxC irLbh+SXni0EXgTQJspb+XImwASShICAeC6lsuoqXleKrsNZkKLglVdQeiuAf/F6URTQ+FImkLwE lnkatXMWzun2W/IiiK7lLEgR8KL9RQ1tsyia60URXM6XMAEmwAQMAmsDHjFq6WvZJcYBfm2ZAAtS y2z0M7njSzt7/JgD4MRWLuXTTIAJMIFQBCqkkAVLZnRfGOokH9tNgI0adrPY611u/vq+Xr9YzmK0 Fxo+wASYQOQE2gsp3h4ytnRi5Lck55UsSC30+5D8smM8wvMhB9JrARAfZgJMIBoCXiHxVN7YjXcC kmemWiDHYEKAyRu76QzIALkDyQpxmg8xASbABGImQBZ4vt+6XVFcLHwxZ5KgN7IgNevYvPyNl0EI MmDwNDvFH5kAE2AC5hCQmJ9Zh3PnzcupMSfDxMiFp+yC+nFIQdlNEOIpFqMgKPyWCTAB8wkIjKxJ xyLy+GJ+5u7NkUdIet9JkVdQRvuLaJ8RJybABJiAVQS+9kvvqcWFXTdZVaCTy0l6QZo8WWrLVpU9 JoE/ObmjuG5MgAkkJgEB8ZNPw7Di6dnrErOFkbcqqQWJvHVvF2UU2fXCyJHxlUyACTAB0wn86pf+ vOLCnj+ZnrOLMkzaNaTcXOndLja9zGLkoqeVq8oEEpfAfh7hfW/4OaUHJW4TW29ZUo6Q8vNLUreh 42tCYEzriPgKJsAEmIBlBDZDiiGLC7NXWVaigwpKOkEiMSoXnQol5GgH9QNXhQkwASagE6C4SprU 8ooKu61MNiRJNWVH03Q0MmIxSrbHnNvLBNxDgOIqSREoGjq2tL97am1OTZNmhEQGDNtE6asCosAc dJwLE2ACTEAlAVkGIXMXz+jxg8pSnJR3UoyQfreme57FyEmPHteFCTCB8ARENqS2mJw8h78ucc4m gSBJUS7KHgVwfuJ0G7eECTCBJCHQwyM8i3LHbOiRDO1NeEEaMrbsHt70mgyPMreRCSQsgQM8Hu3d kfllXRK2hb83LKEFaUjBxluExN8SvRO5fUyACSQ4AYGBjQjMG5q/vX0itzRhBSlvbOmVAuLeRO48 bhsTYALJQ0AK8UcpamfT1pVEbXVCCtLg/I2jIfEYgKSxIkzUB5TbxQSYQDABMWS76Phsogb5SzhB Gjxu0x81IV7jEBLBDzG/ZwJMIIEInJc3tuyhBGrPrqYklCCReaQIBOYByNzVQn7DBJgAE0g0AhLX Dc0vTbj18YSZ0iILlEaBjyRk0tjsJ9p3jNvDBJhAVAQkpBy/uLD7jKjucvDFCTFCokW+Bsg3WIwc /KRx1ZgAEzCbgIAQz9IyhdkZ25VfAgiSFPoin8DxdkHkcpkAE2ACNhHI1AKBuYmycdb1gjS0oOz/ AJxn08PAxTIBJsAE7CbQ1ePV3kmEPUquFqS8/LJzJXCX3U8Dl88EmAATsJnAwVLUv+R2c3CPzRBj Ln5o/qaDIeRbABJ2k1jMcPhGJsAEkpHAH3oPrNTWlkxb6tbGu9LKLu/sDfsgRfsEQB+3gud6MwEm wAQUEJBC4NyiGTmzFeStPEvXTdlRKAmkaLTxlcVI+ePBBTABJuAyAkJKvKDPILms4lRd1wlSudj0 LwDDXMiaq8wEmAATsIJAG6kFZrnRyMFVgkQ+6iTkX63oUS6DCTABJuBaAhL9Aqhznc871wjS0HM2 99aEeIEdprr2K8IVZwJMwEICQmBMXsGmGy0sMu6iXGHUkHvx2nRPddpHEDg87hZzBkyACTCBpCEg G4UUQ4oKc5a5ocmuGCF5a9IeYjFyw+PEdWQCTMBZBESKFGKGbpnsrIqFrI3jBWno2NIxHII8ZN/x QSbABJhABARkDlI8z7lh06yjBWnImNL9IfF0BMT5EibABJgAE2iRgByVl192bYunHXLCsWtItN9o O8reBztNdcijwtVgAkzA5QTqhSaPK5re/UuntsOxI6RyUXo7i5FTHxuuFxNgAi4kkBaQ4tVRo0od G8DUkYI0JL/sGAlBXrw5MQEmwASYgEkEhMRBtZnifpOyMz0bx03ZDb9gU5a/PvAFgANNby1nyASY ABNgAlIKeeqSGd0XOg2F40ZI/nr/AyxGTntMuD5MgAkkEAEhpHh2RP76Tk5rk6MEacjYjcMBcZXT IHF9mAATYAIJRqC7X3gedVqbHDNld8LoLW3T0xu/BbCf0yBxfZgAE2ACiUhASoxZUpgzxyltc8wI KT29cQqLkVMeC64HE2ACyUBACPnYiRN+6eiUtjpCkIbll50C4AqnQOF6MAEmwASSg4DITvN7pzml rbZP2ZFNfE0GvuGAe055JLgeTIAJJB0BiZGLC3Petbvdto+QajPEP1iM7H4MuHwmwASSmoDA407Y MGurIOWNX3+IRMBV8TqS+qHlxjMBJpCoBA6oyRB32N042wRp8mSpwe99AhApdkPg8pkAE2ACTEDe OHjshkPt5GCbIC0r2XQlII+zs/FcNhNgAkyACewi4NWkeEIfLOw6ZO0bWwRpZH5ZFynkPdY2lUtj AkyACTCB8ATEse9/V3Zp+GvUnbVFkBqaxKiDumZxzkyACTABJhATAYl7cs9aa8vfZ8sFadi40qMA 2KbAMXUQ38QEmAATSBICAuiipabfZUdzLRYkKQIB+R8AFpdrB1oukwkwASbgTgIC8uph+RsOs7r2 lgrDkPxNFwHiWKsbyeUxASbABJhAVAQ8AWgPRXWHCRdb5qnhd48MPwDobkK9OQsmwASYABNQTEAI eVbRjO5vKi5mV/aWjZBq0sVNLEa7uPMbJsAEmIDjCUgpHjjyCmnZXlFLBGnEmC3ZEPJmx9PnCjIB JsAEmEAwgQM7lJddHXxA5XtLBMmX0vhPAG1UNoTzZgJMgAkwAQUEBG63KkSFckEafk7pQZC4SAEm zpIJMAEmwATUE9gn1Z/yV/XFWGB+7feARkdeKxrDZTABJsAEmID5BITENXnjN+9rfs575qh0hPT7 Jthz9iySPzEBJsAEmIDLCGSJQODvquusVJACAZC/OstMy1XD4vyZABNgAslKQEp5xfD8Tb1Utl+Z IA0pKD0ZwDCVlee8mQATYAJMwDICqX7NTwFVlSVlgiQgbQ/2pIwaZ8wEmAATSEYCUpw/ZOzmPqqa rkSQ8saVngSIIaoqzfkyASbABJiALQS8Aj5la0lKBAkBOdkWVFwoE2ACTIAJqCUgxflDz9ncW0Uh pgvS0PzSE3l0pKKrOE8mwASYgCMIeOFRY3FnuiBB4FZHIONKMAEmwASYgBICEvK84eM29jQ7c1MF KW/8+kMkMNLsSnJ+TIAJMAEm4CgCqb6AuMHsGpkqSPB7yL0E7zsyu5c4PybABJiAwwgI4PIR+es7 mVkt0wQpd1zZAQDGmlk5zosJMAEmwAQcS6CNT3j/bGbtTBMkLYAb2WedmV3DeTEBJsAEnE5AXntc /voMs2ppitPT3LPWdhCQl5hVKc6HCdhNwOsVaN9WQ1amQFaGQEaGhox0gdQUAaEBaali19x0Q6PU j1OdG30S/gDQ0CDh9wPVtQFUVUlUVgdQXRNATa20u2lcPhMwk0DnLOE5H8BTZmRqiiBpqekTAcnx jszoEc7DUgIkLL3396JvrxT03T8FPXK8yNnXgy77eJTUo7pG4tdSH9Zv9OGXDT78vK4Rq39uRFV1 QEl5nCkTUE1AAtcD8mlAxP1rK24DhNxc6fV0LfsZwH6qG875M4F4CXTqoOHQAWk4dGAqBv0hFQf0 NOU3WbzV0gXqu58asXJ1A75YWY/NW/xx58kZMAGrCAjg1KKZOQviLS/ub6O3a9mZksUo3n7g+xUR SEkROKR/Kv54eJr+v3t23I+8kpr27O4F/R9+StN0/MZNPnzxTQM++7oeK76pR31D3D8+ldSbM2UC REBKXAUgbkGKe4SUl1+6FAK53C1MwCkEaK3n2CPSMfiEdBx/VLpTqhVXPZZ9WocPP6vDxyvq9fWo uDLjm5mA+QQCwu/pVzRr3zXxZB2XIOXllw2AkCXxVIDvZQJmEKCR0PFHpWHw8Rk48ZjEEKGWuJAw LXq/Fp98WY/GRh45tcSJj1tLQEDcXzQz+2/xlBqfIBVsfBQQptqhx9MYvjf5CPQ5IAUjB2fgrFOz kq/xAN4qqsE7i2vww5rGpGw/N9pRBLZ2kuXdCwsHNsRaq5gFadSo0syaDGwE0CHWwvk+JhALAbKM yzsxA6OGZ6Jfr5RYski4e0iQ5i2qwdIPa1FXz6OmhOtglzRISHleUWH3V2OtbsyCNLRg46US4plY C+b7mEC0BPbt4sGoYZkYdybvMAjHbtbb1Zi7qAYby3zhLuNzTMB8AhIfLC7MoWjhMaWYBElKeeCM udULXp1TeQDtq+DEBFQSGHRQKkYPy8SQE03bEK6yuo7J+73ldZi7sBpfr4p5BsUxbeGKuIOApkE+ PLnTjQMOSv93LDWOWpBIjAB8T4XNW1iDp1/bCRalWNDzPa0ROO7IdH1a7pjD0lq7lM+HIfDNqgZ9 xFT8UW2Yq/gUE4iPAE2fP35fZyOTvwsh/mV8iPQ1FkG6F8AuSwoWpUhR83WREsg9PkMfER0yIDXS W/i6CAjQxtt5C6t1Cz3JExsREONLIiXQe78UPDlllxgZt3mFEFHt8I5KkKSU5E9lr4lpFiWDP7/G Q+CUY2lElIXDBrIQxcMxknsfeqoCbxfVRHIpX8MEwhJoQYzontOEEPPD3tzsZLSCdCqAkAWQKP3v 5Z1s4dMMMH9snQBNyZHFHE3RcbKOwKofGnTLPNrTxIkJxEIgjBhRdjOFEFGFJIpWkF4EcEFLFSdR eviZipZO83EmsAeBA3unYPTwTJw6OHOP4/zBWgLkmohMxskbBCcmECkB8gP59INdWru8vRBiZ2sX GecjFiQpJf3VqDZubOmVRaklMnzcINCxvaabbp9zuns3s1IYCWMdJjNDoLau6XNKCnaFojDa65ZX GimRVd53P/ImW7f0mV31pC0Y40a30Wc2WqnDJUKI51u5ZtfpaASJhl7Td90Z5g2LUhg4SXxKCGD8 WW1w6bi2jqZAISHWl/qwcZMf5OR0yzY/tu8IoKIygIqdgV1CFK4RXg/QsYMHnTtq6NTRg25dPdi/ hxf75XhBZuxOTrSPqfCtamzdHtV6tJObxHUzkUAUYkSlLhJCDI+0+GgE6U0AoyPNmEUpUlLJcR15 Vrj1Guc59aA/urRP59vVDbr7nTW/+pT7hyNh7pHtxYADU3BQ31TdiKNnjvO8kD8/sxKvzK6KSICT 4ynmVkYpRgawbCHEJuNDuNeIBElK2RHA9nAZhTrHohSKSnIdoz+8Z52a6Shfcx9/UY9PvqjDFysb 9BGQE3qka2cPjjg4DUcflgayNnRSuu/RHSj6gA0fnNQndtQlRjGiql4jhHg0kjpHKkgUnvzZSDJs fg2LUnMiyfP5vDFtcMlYZ0zP0R/UpR/V4qtvGxwfW4g8lx91SBpOOCYNp+Y6w+CDnLjSd/nnX3h9 KXm+wbtb2r6thosL2kayZrT7pt3vioUQg3d/bPldpIL0NtmUt5xN+DMsSuH5JNpZp3hYIKuxhe/V 6kHu3BqmgcSJggtS4D4nxHZ66fUqvPh6JU/jJdqXNkx7sjIFJo5vF6sYGTnvK4T4zfjQ0murgiSl bA9gR0sZRHqcRSlSUu69zuMBLhvXDgWj7bOeW7fBh/lLavQpJjJASKTUrq2GYSdl4KqL2tnerH8+ VI73PmYzcds7QnEFTBIjquWVQognW6tuJII0HkDM7sSDK8CiFEwjsd4POzkDt/zZPqMFGg3NXViD L1bWJxbYEK0ho4hDB6TqXi3sXG+a/U41XplTpVsehqgmH3I5ARPFiEgsFEKMaA1JJII0E0B+axlF ep5FKVJS7riOwoVfPLYtzjnNnlHRzHnVmLeoGmWbk9NEOWdfjy5M+WfYw5+e0keeqdB/DLjjieVa RkKA9tZdPiHuabrmRXUQQoT1nNCqIAUCcqYQ5gkS1ZBFqXk/ufOznaOiFwsrMXt+DaqqE2taLtYn gf6AnD0yyzYjkjcX1GD2/GqOwRRrBzrsvusuax/vmlGoFh0phPgi1AnjWKuCNOutquvHnJ71kHGD Wa8sSmaRtD4fWiuiX0/n2vCr/IXCSsycW+14Sznre6WpRK9XYMxpmbjiPHvWmZ54aSdef6tVhy52 4eFyIyCgSIyoZE0IEdbPPHnvDpu2+f5y7uqfGo/PO8nc4Gh/6JOCju09+OTLxJ/zDwvYZSfJ0uvZ aV0w4EBrvQ2QEP3ffeX63iF/cs7ORfSkBAJAyfeN+tpOba3EkYdYG0vqqEPT0KG9B6Wb/dhZyaPX iDrNQRepEqM/3bK14sWnMqesWnVnfILUa9Ckh0s3+Tuv/qkRLEoOenJsqArtKbpuIhldWpdmzK3C P6aU47Ov6sFCFDl38rNX8kMjXn2jGo0+icMHWSdM9GPzrFOzUF0r2S9e5F1m+5WqxOiav28lLyjp dahatGbV1F/DNTTsCGnI2M19hJR3UQbk14tFKRzKxD3X94AUfVOclc5Q58yvxgOPV2Dph3U8PRfH o0UjppXfNWDGvGpoAjjYQj96Rx+ahvbtPPh5nU93PhtHM/hWxQRUidH1d2zDqh9+30wtsGltydQl 4ZoSVpB6DbxxrADOMDIgUfplgw+nHMfTdwaTRH+lX7p33dwR/XqnWNLU5Svq8PgLO/HGuzU85WMi cRpdfvltAxYva9o71L+fNVOuB/VNAVkA/rYtgJ/WsZcHE7vUtKyuubQ9Ro8w3yMIiRH5iAxKaWtL pj4d9Hmvt2GNGobkl84SAmOa30V7H26/gdzbmZvY0MFcnvHmRhswrTTnfvQ5EiJnLoiTFdthA9NA 01H7dfcip5sHbTI1tMnSdMwNjVIfBZBn8A2lPtAG3a9K6rH2170CLMfbLabcT/uYRg/PwinHWec3 j4wdyOiBk3MIkBidaY0YUaP9XunvuqCwZ4t+UVsUpPx86dkuyrYCCLnbUZUokfnof54Na6runN5M 0JrQegMFzjvpj9b8saJ1ohdmVoH+qDsppaUKDD4hA8NPzsAhA2IbUWwvD+CDT+vwzmJn+oEbNSzT 0nXBjz6v04MB0pogJ3sJqBKjG+7YhpV7jox2N1SK/MWF2a/vPrDnuxYFKS9/w3EQ2kd7Xr7nJxal PXkkwieaXrnyAmtMht9bTt4VqvXwD05iRzvUzz29DS44t42p1fr863p9Ayn9UXZS0jToa4QTzja3 veHa+NyMptAW4a7hc+oIqBKj2+7djk/D/dgQeHLxjJwrW2pZi2tIvQbedJEQGNLSjXSc1pNUrCnR vDMthn7KJuHh8Jt+jqboLjjXGu/cT76yU18r2rzFOTbcNCKiP8r33baP7prHbMA53bz6iOsPfVJR XRvAxjJntJ0s8mh9icJy0B6zvr3UrxfSKJy+4zSlSdF3OVlH4M8Xt1MSDqZVMWpqYoe1JVMfaam1 LY6QhuaXLpACEUX645FSS3jdcZz2FNHUDXleUJ30Hf3vVDsmDhG1lzaTjh1tvZcDGimR/z0aOTkp 0ZoC/YK2Kt37nx1YvIzjLVnBm8SIPHqYnSIUI71YKf09lhT23BiqDiFHSLm50ivaVP0XQEQT5zxS CoXWHcdGDc/EnZM6os/+6n8VP/x0BV6aVYXKKmdsmKTRwLgz22DaHftYuk/HeDIoSuzQkzJ0I4kd lQE4ZbT4/c+NmPVONVJThCUboGmt0uMR+KpkD4ssAxO/mkTACWJETdEgVqxZNW1lqGaFFKRex151 tJDy6lA3tHSMRaklMs49ftn4tpg4Qf16EVnO0eZWChXuhETesmmt7N93ddajtNpdpwN6pmBEbia6 dfVie7kfW7fbL9iNjdBHbr+W+nGyBRFsDx63vWsAACAASURBVOmfqntuIevE6hqewjP7mVQlRpOn lmP5iihH+EJsXVsy9a1QbQwpSH37T5oAgWGhbgh3jEUpHB3nnOvYXsOV57eDFRtdyWKSgrrV1tn/ R4aEiPZVPfqvziAXN05LfQ5IwWl5mejU0YMt2wIor7BfmNat9+mjWlpfG/SHiCZMYsZKJvW0zWB9 qR9ULidzCKgSI4qJ9cEnMRnoZK0tmfpYqNaFXEPKKyibC8hRoW6I5BivKUVCyZ5rjjksDffc2kl5 4bQ2MnNuFTY5wGiBhIj+0N9wuXXrImYApvW2eQur9T1NZuQXbx4nHJ2OO28yf/9hqHqRN/cXX68K dYqPRUHgTxeocYIcb4DGVCm6zi/M3tK8KSFHSL0GTnpYADGvfPFIqTlmZ3ymxcxbrwm5rczUCj79 WiWeea0SVQ6YeqHQ3/+7vwsorLrbElmbjh6RpW++JWG321np+tKm0VJ6qoaBikdLhw5MA0XI/ea7 BvicYYzotscHThUjAtkoRPG6kqk/Noe6lyANz9/USwp5W/MLo/3MohQtMbXXTxzfFpeOU2vSvWRZ LR5+Zifo1e405MQMkH8uFRZFVreN3PzQVGN6uoYNZfavsaxYWY9fNvpwyrFqrTIP6puqm+GvXN2I Tb+xKkXz3CkTo3+bE7peCPnz2pJpxc3btJcgHXDwDacC4tzmF8bymUUpFmrm3pOWJnD1Re2Vxy4i bwsPP70Tv2219w8HucK59rL2utFC1857Pd7mwrU4NxqV0BoLWb+tXe9DXb1963L03SaHrRlpAqr9 4tEot6IyALL+49Q6AVViNOVxMs+Pac0oRKVFw9qSqS81P7HXN7b3oJsmAji2+YWxfmZRipVc/PfR lM+F57bF6UPNd5wYXLsHn6jQg+YFH7P6PcVposXb8We1Qbcuez3WVldHaXmDDkpFwag2ujB9v6YR ZBFnRyKHrZ99XY/ynQEce4TaKdE/Hp6u7xejDbycWiYwcUJbFIw23+MGidGCYlNnPjpflP/gA8XF e8ZH2suoIa9g43JAmCZIBjo2dDBIWPOad2KG8vUicoY7Y16VrdMpZC1HfvdIkJI1vTK7Cq+9UWXr iClnXw/yz2ijIuz1Ht1Khh5Pv7rTEVabe1TMAR9IjGhfndlJgRjpVfT40X/hrJzVwfXd46ckOVSt FVX/BmD6LkkeKQVjV/ueHkrVgfTojyB5bq6qtmfaiJydXj6haR8VbTBN5kR7eMjlEfXEdz82gmIg WZ0qq6Ue/ZnqQF7RVSUa9Xfr6sGOioDt08Oq2hhLvm4TI72NQi5vvkF2D0HKOfjKgwBcHwuQSO5h UYqEUnzXkPHChflqjRfuf2yHvpM/vprGdnf/vim4dFw7XH1RO9CGUjsTTR9R8Luff/FhQ5kfASn1 zZ121YmE4PwxbdDogx5M0w5h+mZVA35c68OQE9QZPPTaLwWnDs7E9h0BikRqF27HlKtKjMizyjtL TJ2m24OZEFi7pmTaouCDe0zZ5Y0tPQ8SLwdfoOL9iNwM3HyV+ebHyR66QtUGOOMZoCm619+2xw9d v14p+nTQaUPUrocZbW3p1fDYTYEEySlp89Q2S9MNSM4bY/7USfOyWvtMDmxnvV1tS+j3lBShj2DH nBbz7pHWmqeff3lWFZ6fWRnRtYl4kSoxeuSZCt3PokpmUorFSwqzhwaXsccIqffASRcBOD74AhXv KaTx5q1+0EY7M1Oyegnvso8Hl09ohzNPVfflnzm3Gv99YaflfugO6OnVR3yTrmwPEiW7Erk9euqV nfr+KtqP01KimE7kk418wdEIhabT7EpHHpKGC85pq6+30FSelYnaTgYPZAlI9VCVaOqW9iuFDXmg qnCb8724oK0+VWt2NawQI6qzEGi/tmTqA8H133OEVFBKw6c9FCv4YrPf80gpfqL0B2/a5H3izyhM DnZEcqV1IfJArvoXdphm66dKvm/QfynG6o2a3DSRqfa4s+wfMVE/vrmgOuTIrjUO8Zy3IuAjjd4L 36pC6WZ7tx3Ewymae0mMzj/H/GfKKjEy2iok9i8qzPl112fjDb3mFWwsBUR28DHV71WJ0pz51Xjs +cQOl2yFJd3N/9ymx8pR/RwY+dOUF5luF4xWN9ozymrt9YH/7sDC98yZQ9+3iwdnjshCwSj72/XQ UxV4u6imteabep6CAF51oZrQB8EVjSYMQvB9bnqfKGKkM5cYubgw512D/64puxMn/NLRG/DcbZyw 6lXV9B1t1mvbRkOihkomx6g3XKHONxv5orvlX9vx68aWp6fMfkbox8kjd3dW7pamtXrTH2zyTk7P plmJPFiv+KYeRR/U6lN5FIPKrkRulAb8IVWfygs3/Whm/Wi9jb6LZHBxxMHqpvDyTsrAth0B/Jig xg6qxOh/L+3EnPnW/kjRny8hvlpbMnW58aztEqQDD/rrURC41Dhh5SuLUnS06aGk0BGqEnldePyF naD1EKsSmS3/5RJ1AhtJO2hK6//u367UcovMo8kwYulHTUYRqr0ctNTunH29GHx8Bsj4wMrNpt+u brJKpLJVJRJcenLJ4i+REhnKXKTAgpbEqPCtaptQiQ1rS6bONQrfJUi9Dp40EsAZxgmrX1mUIiNO bkFUrkfQw0lB9KxMNNq74jz1cZlaahO1+dZ7t+um0i1dY/ZxcpRKI4YPP6unxV0c2Nseg42DD0rV nbeSAYJViUZlbxXVIC1NA4WcUJHIBD4zQ8Pn31jXLhXtMPIkMbpkrPk/Qu0VI2qdrF9bMu0Zo527 BKn3oEkXmOkyyCggmlcWpfC0/nJJO6WL/HdOK8f8peasmYRvye6zfQ9IsSykwe5Sm96RR3ISom+/ t9YCLbgeFPPo4y/qdXGiUOoUE8nqRKO01FSBL1ZaN6Kg+FiffFkP8rWoKs4STYt2bO9ByQ+Nlo72 ze6/xBUjIiXarS2Zep/BbJcg9Rk46RoA/YwTdr2yKO1Nnhb6aUGYFsVVJLJQuu/RHVi52ro/SEY7 aPqxn8Wjg+dmkBCV61M6ofYSGXWz8pWixH74WR2+WtUACoZn9aZf8o9nbFy3st0kghSm5OjD1Kwr 0QisXRtND3hIG2ndllSJ0bPTKzFjrl3TdHv0QlrvQ/76xNpvH9Qrs0uQeg2c9A8Anfe41KYPLEq7 we/X3YsJZ6nzETb7nWrQ2okdsXbItFvFBund9PZ890JhJf7vvnJ9zcQOLwZ71ib0p81b/Hj/kzpQ yIWMdIH9e1jnFql9O49pVoWhWxf6KO2RIiexZDWqItEPnjOGZeplbCxzj1m4KjGiH2SvzrF2Wj5c v2oy8Maakmnr6RpdkJp82FVOBcQugQqXgRXnWJSAwwel4rF7OiubZ6fQ4k+/at8ud/rCDein3tqM /O7d/sB2fP51gy1eC2L5vlD8n/eW12HVj43IzBCwwl8f+Yhb9UOjLXt5SCjeXlyL9FSh7Hknwftt WwA/rbNvijbSZyH/jCxMnGD+uiqJEX0fHJWE9t6akqlfU510Aeox8KoDpMCNjqokoJvdqvDo4AaT 8NzjM3D3X9WFGievC9PftPfBJFdH7dtqyh676W9UYfK0cny8ot62EA3xNo42epJF3k/rfPo2hpxu akdM6ekCxcvNinkTXeuNdSUKRKgqIi15hSfvEbSu5NREYnTlBUkiRmTWAJSsLZm6lPpDf7r98PcF 9nDa4Ji+MmJwmD21Y0QSdeLmWfJQoNJb9z8p6qNNf3SMB8vrgbJf/TPnVYOmIrdud8/0jMGlpdeP Pq8D/acfKqOHZYJc5qhIJx6Trlv92bm29uTLO1G+w6/kjzIxu+L8droFnhN94CWbGFF/aEAf41nW BUlqYn9h3ZYTo+yIX5NJlFQ9kAbsa2/fhlU/WG+8YJRvvHbsYP7sMDkSJQ8dm7YkjhAZvIzX4o9q Qf8piuqo4Vkg7+dmp306emwXc9oXQ/14x40dzW6enh+53UlPE3oIFSUFxJCpqu8+OaB13DRdEB8p sb/xUf+r0HvQjWcC4hTjoBNfk2FNidZUVMwbU3+SJR25VdlQZp73gXieE/pjkD/KHF9cNBp64L8V IH9zZLGVDIlCXsxfUqOviZgdnHDRB7Uod4BFGnkJ+fDzemiK9mmRWTgZcnz6pf17lc4ckYmrLzZ/ Y7grvKELYG3J1Ifoe6uPkITUekp9b7Ozv8qJPFJS5RKEetSJfv0aTJjCp82VJLQ//2JCZs5+9Fus 3btLa7CguAan5WXihsvN/4PWYsEWnfh5XaPuZd7nB+iPttmJ8iTBe+H1Sj3on9n5R5IfRTy+5lLz +84VYtQEKCc3V3qLi4VPHyEdMOCma4RA70jg2X1NIo6UKKYJuc5RkWio/uQr9lnStdQm2ogZT7hl cm1EFkO0sZQTdN9t5GFjZ6XEMYfHt6eHhN4JIySjX/1+6KMYVcYOtFcpI03Tpwgrdlr7PJEYXXtZ UosRdbOW0rby6Z+/nbZTN3ESQvY0Ot8NrzRSomiGZicydCDLLyuT7groTDVi9NQrlfofbSvbw2XZ R4AMEd54txpTHt9hXyUUlkzGDuRdQ0UaNTwTz0ztAnKlZFVSJUZkPetEg41wXGUA+9F5w+a2R7iL nXhu3qIaUOwOs5OVokTid+4Zarwv/PupCpCTVE5MIJEIvPZGFR58wvzvvcHooTv3gdlrckbewa8q xcjOvYXBbYzmfUBqetgjbdSoUpqYNX9yNpraxHgthUhwqyjRnLFheh5j81u87Z8PlevOK1u8gE8w ARcToHUzitOlKt11c0cMO1mN1wiqM4VZUTFNRyMjN4oRMZEIdKNXrTrTY2lAPrMfIpWiRNNpZidy B3PdZe2VLNBSXW+6axve+9iejY1ms+L8mEBLBChkxgXX/qYbtbR0TTzHb/lzB9AoxuykKiCpm8WI GAsNXejVK4Xs7OQ9SJE8ECRKlMz+1WFMpz3xkjmRZ7MyBSaObwearzY7kbXZ7Hersd7CgHpmt4Hz YwLRECjb7Mdjz1cgINVY4NHfE/JcMdMkJ6QsRmF6V4ocOuvVAgFXj5CMJjpdlLp29mD8mWqcpJIY Pf3aTlBUUk5MIF4CNbXWWprFU18yB//PsxXw+SQorpbZieJ0kfd18vsYT1IlRq+/Ve3aabpmPHXH 3l4BdEmUP2NOFaVuXTwYO1qNGJFVFXnr5sQEzCIg3aNHu5r8+Is7dR91tLnc7ERRWj2aiNlyjdw9 me36jNpIYmTW7I3ZzKLPT+xL93ghRQe4fc4uqPVOE6Xe+6XgySlqonok1gMZ1In8lgnEQID2pdXU Slx+nvmRVcnVkNeLqEcjpxybjr9f1yGG1oS/JfG++1KHpEnR9CZ88911lkRJhfUdrSlFY+jQr5c6 MaJFzMT5deSu54tr61wCtNVh2pNqzMJpI3c0338So9tvMN8XX+KJkf486aA0IPEEiZpntyhRBM7H 71MzMiKXIG4173TunzKuWaIQeGdxDf4xpVxJcyL9UapKjMgNWIL+EG0aIQkI8yVcyaMQfaZ2iRIF 1vv3nftEX+EI7qBpCbftwo6gWXwJEzCVAIXquO4favYqkSiF8+hysqKRkRN9UprYaem5F69NJ08N 5jtSMrGW8WZltSgdc1gaptyuRoz+99JOR7uRj7ev+H4mYCaBku8bcOmNW5TsVaJN7aEcotL3/x8K pukSXIyaur06q4NXAubv/jTzqTIhLxIlSqr3KdHDeM+taqK8PvxMhZIvlgl4OQsm4FgCFMLi+cIm /3dm7/8zPIXTd5OSqu9/UogRgFQEMryQSHdosFhTH3LVovTFynplYnT/Yzuw6P1aU3lwZkwgWQiQ B+9HnlWzgdYQueUr6pR8/5NFjOhZDABtvBCIz1e9i55qlaJkeHUwG8fkqeVY9im7AjKbK+eXXATI EzptoA0EpOk+JEmUDGEyk+qbC2rw2PPJs8fQ70EmBehL6DWk5g+IKlFqXo4ZnynC66df2R/N0oy2 cB5MwAkE6A88xVdS9QPSrDaSGJGAJlMSgUAaCVLSjJCMznWDKN04eRu++a7BqDK/MgEmYBIBMpuu b5BQ4dXBjComoxgRN02KtmRll24GRLflQaLk1OHw9XewGLnteeL6uosAbZ+g/XxOS8kqRkY/0Agp aRMtGHo9wJUKwkzECvWKm7diza+Nsd7O9zEBJhAhAdrP1+iTuGSs+a6GIqzCHpeRk+Rkm6YLBiAR aJPUgkQwCt+q1pnYLUr0ML65sBrr1vuC+4jfMwEmoJDAK7Or0NAgbf9RSt9/w3xcYXMdnbUAPCRI zvh5YCMqu0WJHsbpc6uweYvfRgpcNBNITgL0/acwFuG8L6gkw2K0my4JEq0jJX2yS5ToYXx5dhW2 lbMYJf1DyABsI0DT936/NH3zfGsNYjHak1DST9kF47BalOhh5MB6wT3A75mAfQTI0Ims71TELgrV Khajvanw6KgZExIl8hmnOrEYqSbM+TOB6AksKK7FPx9S4yk8uDb0/f/fy+r/zgSX6Yb3JEgujA+p Fq1qUWIxUtt/nDsTiIfAex/XKRUl4/tfV58osbrjob3nvSRITZ4H9zye1J+yMgVy9lU3m0luRkYO yUxqxtx4JuBkAocMUOcvgL7/+WeYH2rdyTwjrZu6v7qR1sBh15EYTRzfTolvquCmUuRJEWR2Hnwu Gd5npFPrOTEB5xG47rL2yr//FBKdEsc2293/EvCzIO3mAavEyCiS9j4JAcyc17QXyjieDK8eXr1M hm52VRszMwQun6D+x6gBhUXJINH0KqBV0Z8FdiUNWC5GRldccX47x/rUMurIr0wg0Ql0bK9ZKkYG TxKliwuSfiuogQM0Qkp6d9JWj4x20f/9DbkuSfEKHr43BxPmcy0vCIehw6eiIdCtiwdjR7dRPk3X Up14pNREJiBkJQlScvk4b/ZU0DDdijWjZsXu9ZEeSq8XePpVtjHZC06IA7SJkRMTiJdA7/1S8OSU zvFmE/f99P0nv3rkyihZk9S0eg0yuUdIVs4Zt/agjTuzDSZO4OF7a5z4PBMwg0D/fs4QI6MtNFPi 1JAYRh1Vvnr8qNEgkncNyQprmmg7kESJLPA4MQG7CGRkJL4F5CEDUvGfu+0fGTXv42QWJQ2o0gSQ lNuFnShGxsNJ0SztcvRo1IFf3UugPs64jhr9VUjgdPxR6Zh2xz6ObWGyilIDtNqkXENyshgZ35Kz R2aB/jAkc3wUgwW/RkegsZHX11oiNuzkDNzy5w4tnXbMcRIlSkm1ppRVvUOTkOodNzmmmwFVYvT6 73GVzGzqmSMy9fqmpyX2L1YzmXFeTKAlAqOHZyoTI4pAa3YiUco/I8vsbJ2aX13x873qvIDY4dQa ml0vVWJ0273b8elXTdbzNN1mZiI3I5QK36pC6WYOUWEmW84reQgUjMoC7flTkcgZK/m/o2SMbMwq xwgcakQiMCtfB+aj65AmZHIIkhVi9MRLO6FipESi9OIjXXFQ3xQHPkdcJSbgbAIXntvGEjGi6bWX Z5lvtk2ilAQjJX2mToOQCT9CskKMjK8kiZIqV0CP/qszjj5MndNHow38ygQShQBto7gwX81Witsf 2D0yMniRbzoWJYNGNK9NAyNNAluiuc1t11opRgabJ1/eielvmv9LifK/99ZOGHJChlEUvzIBJtAC Ado+QdsoVKS/3bMdy1eE9rqmUpTMXhJQwSa2POVmuk8LaFpZbBk4/y47xMigQh4XVPxSovxvu7YD Rg3j8BUGa35lAs0JXHNpe6j64339Hdvw+dfhPa6pEiUSWTLOSMC0ldrkFVLobxKtgXaKkcGSHkpy B2L2Qiflf93E9qANjDPnJp+ncIMvvzKB5gQ6ddBwwTltlfmlu/KWrfh5XWPzYkN+pu8/JcNXXciL Yjh47WXt9bso5HrCJCFLqS3erBp/WU2CzQA5QYyMB4UWOn0+4PLzzJ/HvuK8dsjK0KDC5NSoP78y AbcQ+EOfFDx2jxrvCxTldfa71Vi/0RcVDhIl8lFp9tRhoomSDDQtHWnz5uWQzCaM1DpJjIwnd8bc KvzvJTUOMcj31VUXqjFnNerPr0zA6QTI+4JKMZo+typqMTKY0fS9ijVlEqVEmb4T0DYRLyNM2gYD nptfad7Y2LdjZjuC9xnFmi/tI3jkGTWO1c85PQvU9pQU3kAba//wfe4lcFpeJu66uaOSBtDI6OnX dmLzlvj2AKoUpTOGun9NSRMB3ZZBjxgrpVgvhDxQSY9alCn9QSbPBmanux4q37XpNd68ac63vkHi 5qvMd11CbScXZLPnV2N9aXTTCvG2i+9nAnYRoDhGKqbDqT1vLqjBo89VQJrkickILWP29N31l7fX 16oXFNfa1Q1xlys0/EqZ6CMkoQXWx52jjRmoEiPagf3+7zuwzWoePTQkcioSjQ6fe6gLDh/Ee5VU 8OU8nUWAjIVUidGc+dW6H0mzxMggp2qkRD9yR+S61hjA37ApRx8hNU3ZSbhWkFSKkeEOxHiYzHol kbv5n9vNym6vfKbc3glDT3Ltw7lXe/gAE2hO4OqL2imLHUTeVh57Xs2aL7WDREmFRxcXi1JpcbHQ p3V0QZJC6MOl5p3u9M9uFCOD6Zff1uNPt2zFO4vV2JP87S8dcM5p5vrVM+ruhFdaxD7qEB4JOqEv rKxD504e3eHwGEXP9qtzqkDeVlQnVW7GXClKQQMifQ0JkL8C7loQd7MYGQ/7T+sa8eLrVfD7ocQY 46qL2qFDew3PvGa+J2KjDXa9nnB0Ouh/8Ue1mLuoBt+sijMIkF0NMbFcjwf6jxBVTkRNrGpMWR06 IBVTFcYxou0TVoZ7MITP7A28JEqNPmDJMpesKQUNiHRB8gY8P/pFIKaHxI6bEkGMDG5bt/vx3xeb fpGpsBAcf1YbZKQLPPqc+l99RpusfM09PgP0f+F7tZi3sBrf/RTZpkUr66i6LCEAGjEksvk/rY/Q H1pVibZl2OFRW5Uo3XZNB/h9cpcXclXczMlXrjHy8dCbIwZOqawVlbcCQv9snHTiayKJkcE3EAA+ +bIebdto6N8v1Ths2utBfVPRsb0Hm7b4UbHTGT88AhIgsTQr9TkgBWT+26mjB1u2BVBe4Yx2mtW+ UPmQEJ11ahZ0p7uHmjd9+eaCauxwyHNC7R53Vhtcc0mTd4JQHOI99u+nKvDGu2qmziOp2+ff1CMz Q8OAA8397p9yXAZ+2eDT/0dSD7uuEZDPrymZ9iWVrwvQqlV3yt4DbzofgHPj+gL6XhsVpt3B8Uzs 6hQq97Ov1DyYlDftYj9zRBbW/OrDr1HuNlfBhKYUzj2jjel7pw7snaL7+WvXVmsS4MrEFKbTh2bi v/d2xjGHmydERj/TNHJdvUm2zkamMb6SN5ILzjXvh0vzakyeWo6iD+yf2iJRUvGD1BWipImpa7+d qtsx7BoR9Rk4aSSAfs07zCmfE3FkFIotPZgej8Ah/c39tWSURdNbldUSqx0wtdW/Xwp65vy+jGlU 0KRXGhWSAGdladj0mx87qxJDmIafkoHrr1DnXLeqOoDnZ6rxVB9N12ZmCPzpwvagTd+q0rW3b8OK b8I7SVVVdqh86QdpMoqSP+C9bd2qKfpDt0uQeg2adAyAY0OBsvtYsoiRwfmrkgb9F+qRiqzIjjks TRc9KsfOJCFw8rHpSqswoF+qPq2Vnq5hQ5kP1TXO+OUfbaPph8S1l7bX14q6dNr1tY02m1avX/5F vel771ottNkFBx+UiosK2mLkYPM3ulNR5H2BRkY0neW0pFKUflzr078DDmvzzqWF3f5u1GnXk917 0KT9AZxhnHDKa7KJkcG95IdG/LY1gOOPVvMHm0ZgNK21YmW9aTvRjbpH+kpThxeca77T2VDlD/xD qm6BlpoisHa9zzFTUqHqGnzsxGPS8eeLm+L67Ntl19c1+BJT39Pifumm+NzkxFMhcoMzeVJH9NpP TXRk8r7wyLMVqKl17g8TVaJEcdRoZmSjjf0b4tn4Zm3J1KeM47ue8D4DJmVA4FLjhBNek1WMDPZk Fk4PUJ6iTa40rUW/trduD2BbufVTWrQLnkYsVkbBHXRQKgpGNa1d/bCmEY0ONcr74xFpuPrC9nro gpx91UxrGs+Z8frdj436pk3js9Wv5HlBpck6bUb97wvusDZVJUr0t8RhorR4bcnUN4xnbZcg9Rt4 fVVAaLcYJ+x+/csl7fSpFrPr4RQDhkjbRb9mPvi0DgJCN0yI9L5Ir+vbKwW0QL55qx8/r7N+CoP+ CLZv58FBfdX8Im6JA00LkZUfrddRHWgvmBPSUYem4U8XtsPFBW3RI9saITLa/ez0Sqz5xfpngJwC k+cF+qGgKlGwzKddth+PREnFd8NJoiQhX1tbMu1Do993CdLPqx6q7T3wxisBYc0cilGDEK80RXH2 SPMXM+9/bAeWfBg67HCIajjm0I6KAFaubkCbTE2JKFFDaZMppa9t2GBK0Tc7tPMoa1u4jqSpSwrh QRM4q35oBJng25EOGZCKyye0xcQJ7ZQZeoRr18x51SicZ32wx4EHpuLisW11k/1w9Yvn3JMv78Qr c+w31IilDZ9+mdiipEnt32tWTf3RYLNLkOhArwE3jxQCvY2TdryqEqMpj+/QN0/a0SYzyiQzaZV7 laiOhw5M09eVSJSsHDHQ1N2n9GuwrfUjJaNvDhuYpk+PEWea0rBKmPr3TcGl49rpI4QDelo7SjTa To5En/h9c7ZxzIrXkUOawkb03l9du+/9zw68VWTfHiMzOKoUpa9WNcQdWiOeNgrg1jWrpu6Ky7OH IPUeNOkQAMfHU0A896oUIze7Zg9mSsN4r1fgYEVm4bSuRCOGb75r0PfxBJet+j198eoaJFRZF0ZS /yMObhKm2jqJ739uVGbw0Wf/FN2SjEIH0KZeuxK5ynnyFetdS106ri2uPF9tYMnrbt+m/4izi62Z 5aoSpRG5mfji2wb8ttWWOettiwtzCjVkfQAAIABJREFU/i+Y056CNHBSVwBjgi+w6j2LUeSkv/y2 AVWKjQGGn5Jpy36lku8b8frb1boQqBLdSEjTWs4F57RFTZ3U15giuSeSa2jf1QXntMHNV3cAbeK1 Ky1fUYfHX9iJtxU5922pXZ06aLj8vHYw239bcHlk1v23e7Y70cQ5uJpRv1clSqcOtkeUpBTL166a +mIwiD0Eqc+gSWRz9OfgC6x4z2IUPWVaiCevC7Q/RVWi/UoZGZrlmwdp2oxEd96iGgT8AFnG2ZVI mC48ty12VjWNmGKtR86+Hpx/Tlvcek0HJe6hIq0X+fp7bnqlbk1ntfkvmbBTmHHyGqIqGaEjGhqc a9YdT9tJlMgNmNkM7RAlTcjZa0qmLQrmsYcgHTngwfJaUXUTAHVPTHDppH6KDBhozShRpumaIdv1 kfbxqLTAo4Jo0blrZ3tMw8l9DU0nLHivVl/Tor1EdiVy0UPCtG1HAD+uidxWvMs+TUL09+s7mu6r LFoW0/5XgUeeqcDPNljSnX9OG9D0pMr0YmElnplu/fSjyjaFypvWkhNBlITEY2tWTVsZ3Ma9Yk7k FWxcDghLPDawGAV3RezvyckmsSRHmyoT/TGjMOx2pe7ZXowelqnUnUwkbaNRBnkWX/R+bYtrTB3b a/pGXHIManci56E0NWd29NNI2kWbry/KbwsVPiiDy3/46Qp9RB18LNHfX3dZeyVha668ZSt+Xhf5 j65YOQuBAUUzcr4Lvn9vQRpb+m9IXBd8kYr3LEbmU504vq3uGdn8nHfnSFMihsv83Uetfdeze5Mw qdgaEG1LSJRopEpGEB4N6NhBQ98DUkBTfXYnCjlCnrvtECJqO7mF+scNHZVj+L/7tieM8UK0sFSJ 0hU3b8WaX5WKUsVJA7I7TZ68Z9yjEIK0cRykeC1aMNFcz2IUDa3orqXF4j9doNZ6iUK70wjBbl94 ZKlGMaTI3Qyn3QTI/Y9hGLL7qLXvaGMvTdOpTGS88PLsKmwrt8VCTGXTosrbpaK0aPHMnOHNG7rH GhKd7HPQLTXQpLIREotR8y4w9zNt7ly3wQdyO68qHdDDC7LCa2gEvv3ePgetFPPo4y/q9bAdZApv p/m0KtbR5EuRgW+9dzu+/V7pL9uwVdqvu1f3MpE/Su30MW3kJTdANDJN9kTfQRWb5unH3rJP69XE FhPylbUl04qb991eIyS6YEhB6W8C6NL84ng/sxjFSzDy+7t38+Lc07OUzDEH14KcVU5/swpbttn/ K5W8HdAak0rLw+C2O+X9C4WVeO2Navh89v5xpj9g9GtddbIruqvqdsWTf1amwMTx7ZR835VM3wlt 1OIZ3d5q3ua9Rkh0Qe+BN51EMd2aXxzPZxajeOhFf29lVUCfV8/KND8SZXBtyAcdCd9v2wIgZ7B2 ps1b/Hj/kzqQp3QK206/1hM5kX82GhF9sbLBMs8SoXimpQndKSo5R1Wd/jGlPOGtZ2NhSE6CVY6U Fr5Xq+99jKVuoe5Jlbj+p1VT97KQakGQbuwJiGGhMorlGDlOHHOa+UP4ZDDtjoV38D3kJ67BB5AH ApXp+KPS9TDMX5XUg8KT25nKNvtRvLwOZA1Hgd5UBQG0q43kXeH2B7brU5VWungK1V4ajf7v/s6g uFMqE60X3fVQue7WSWU5bs5bpSileIW+FGBSPLHvFhbmTAnFOqQg9Rn0Vz8gJ4a6IdpjtMCuIuoj mbLOX2J/6OFoedhx/berG/S9J4MVbqKldg04MFXf/PnLRp8jgp9RXJ+lH9Xhp3U+PRJnTjd3j5im v1GFO6eVY/mKekeEzZg4oa3ug0/1M134VjUee36na4MrquYTnD+JEq0jZ2WY64iZNuKmmidKs9eW TN1ruo7aEVKQ9usyZZOWVTUJQFw/e0iMVLgIof0wtIufU+QE1pf68M6SWqSlqgljEVyTU47NAE3j kLcFJyRqe9EHtfi11I8O7TRYEejOzHbTAv49/9mBDz6pc0RgwWOPSMNVF7XHqbnqrRtpi8FLr7vT U7eZz0A0eTU0Sn00QwJipkcHs0RJSPlQ8w2xRvtCCtK6dXcG+gyYdAoE+hgXRvuqUozs3JwZLQcn XU9RMmmXN8WfoXhAKtOgP6Tqng0oTLRTQkWvW+/TvT5s2uJHp44ePTihSgbx5j3r7Wrc/98KFH9U 65jRAe11u3Zie0tiNd3yr+1Y/AHPgsTyHNHUGlnbOlGU/H55/brvpoWMlBhSkAjAAQMn7S8EhsQC g8UoFmrW3UMjF4oSe9xRasKjB7eEzM8zMzR8/Z29C+/BdSLXOfOX1FjGILjsSN6/8W41pjxeoY/q yDjFCYnWCGktmLxDq07U/kl3bQeNbDnFTkC1KJERRdQRlwXWLH29+90ttapFQeo98AYfhLispRtb Os5i1BIZZx3/cW2jbpGmKhJtcGv1taUxbWyLShtcl+D3xOClWbSxMoDjjlQvzsFlh3pPcXumPlGh W5FV7HSGENH+LgoaSBGcaSuB6vTcjErQfiq7jTVUt9Oq/FWKEu19ilqUBApbWj8iJi0K0lEDp5bV iqprAUT8TWUxsuoxM6ecHTubTMPT0zRY4biUotKSb7MNZT49tIU5rYg/F3KWSsK0s1KCnKhancik lox0SJBos69T0ojcDDx+b2dLng1qM5l0v2NxOAynsFZZDxKlX0t9GGNyFG5aU4pWlATEA2tKppa0 1N6QG2ONi/MKSmcDONv4HO6VxSgcHeefOy0vEzdeoX5To0Hi6dcqQVZjTkvkqJb2VV2p2P0StZvW huYuqsE3NoSND8edfAWeOTxTubNeow40RUcjI/rDyUkdgd77peDJKZ1NL4BM8p9+LSIryIBX+rss KOy5vaVKtDhCohv6DLipEwROb+lm4ziLkUHCva80fbXkwzp4NHMtc1oiQvuiaCqPDC2ctlZAZrOv zKlCQ4Oa/VsffV6nmzFPf7Pa1vDRofrmgnPb4I4bO4IiB1uR6IfJs9Mro1+LsKJyCVYGjb7JFRB5 1DAzGSOllasbQLHMwqTPFxX2eCTMeYQdIeXmr+/rEZ4fw2XAYhSOjjvP0Y57CmNuVZozv1p3Bkqe FpyWPB6A9m+dPjQzbstEmpKjX5M//2KvR4tQjIeckKH/oVJtfWmU7RQHvUZ9kulV5Ujp4WcqwqG8 Z/HMPUOWN784rCDRxXkFpd8DOLD5jfSZxSgUlcQ4RtE9J09SHzogmBYFWCPvzQHnLKMEVw8d2ms4 8uA0HH5wKg7okYIe2R60ydL2uMb4QKbltFb2/U+NesTdVT80wOc8vdVDZYwengmasrUq0b6qJ18O afVrVRWSvhw7RElInFRUmLMsHPxIBOlBALRJdo/EYrQHjoT8QPuVaDf+OQrcPoUDZncgwHB1a36O XBNlpGtITxfw+yRq6qTugbqx0dnrISSk487MwrgzrRsJE7up/yMPK7ypvflzZMfnfr1S8Ph9ataU QoyUtneS2V0LC0XYn2Vh15AIUu+BN9H8wkXBwFiMgmkk7nsaqZAvvPKdARx7RMTGlnED+eMR6Ti4 f6rulYCC3zk50Zw5hUCg/UJVNRL1DdKxIzziqGnAhLPb4P7/64RBijdHB/cbGS7c8eAOfPOdM7x3 BNctWd9v3xHAF9824NTB5o6OaU2JQqzTJvygNOutwnazgj6HfNuqIHU64cGN6XW7zb9ZjEJyTOiD P/zciIXv18LjESDv3lak7K5e5B6Xgb69UnQT8dLNYX9YWVElV5dB1oMU4v4/d3fG4YOsNW0n9z/P z6zi2EUOfIJ+2+q3RJSkEHevLZm6qjUErU7ZUQZDC0pnSiCfpm9UDPEp0Nbsd6pbqyufdwABKyLS hmqmU02kQ9XVScdIiGh96IbLrTPpN9pPBhxvLqwGuWzi5GwCNFr+9537mF5Jipf2n2d3NAqZ0aWo sFNYiwcqPCJByhu7cdzE8e1eUyFGHGzL9GdAeYb79/DirBHqg/+FasiyT+t0x7orvtljOiDUpUl9 jIRoxCmZuOkq64WIwD/1SiVmzHXePrOkfihaabwqUVr6Ue36ISdk7tdK8frpiASp9LfGU7O7eOdH kmE017AYRUPLeddSWJGrLmxnS8U+/6ZeN6H+8LM6W8p3aqFkpj5qWJbu6seOOvKoyA7q5pV5+KBU TLnd/JESgH5CiJ9aq2lEgiSlfA3AuNYyi+Y8i1E0tJx7bbcuHj3ECK1P2JHW/NqoC9O7xbVwumWb Sj5tszR9H9Gl49RHbW2pHbRW9PpbPPXeEh+3HD/msDTcc2sns6vbTQixubVMIxWkCwG80FpmkZ5n MYqUlHuus9r1UCgy09+swluLakB7gJIl0X4S2nk/api5llLR8CMLOgqi58SNzdG0g6/dTcBkUSoW QgzenXvL7yIVpA4AylvOJvIzLEaRs3LblakpAhcXtEXBaHtGSwavT76ox4L3an6PrOrs/UBGnaN5 peCHucel47QhmZY5Pm2pfryvqCUy7j9uoihdI4R4NBIiEQkSZSSlfBPA6EgybekaFqOWyCTW8aMO SdN/tZN3b7sTWfkUfVCD1T81QrpYm8hI4ZD+qcg7McNSrwot9R+NRskHnVO9arRUbz4eHQGTRClb CLEpkpKjEaSxAKZHkmmoa1iMQlFJ7GMFo7Jwxfn2GD2EIkvrGxQGfNWPDa4QJxKhAf1SccIx6SCW TkhLljV5KP92NW9wdUJ/WFEH+mF5500xuxFbJIQYHmk9oxEkmqSOacWSxSjS7ki869q20XDe2W10 wwcnta7og1p8+mU9Vqysh1OC4RGfrEyBwwam4Y9HpOlTck5i9uhzO0HrRZySj8Apx6bj9htiEqVL hBDPR0osYkGiDKWULwK4INLM6ToWo2hoJe61hw5IxejhWTjlOPun8ZpTXrfBh5XfNej/v1/TiNJN PstGUB3bayBXKxQgkTwoWOUJozmDcJ9fnVOFFwo5ims4RslwLkZRai+EiNiTbrSCdCqAiPcjsRgl w2MaXRuHnZyBW/5MNjLOTuRzjTwMbNzkQ+kmP0o3+0ARdmMZTZGT2k4dNHTu6EGPHA96ZnuxXw+v 7mm7a+dWvXfZBmreoqZwGWRaz4kJEIEoRalQCFEQDbloBYm+PRH5AWExiqYbkutaWhspGNUGl59n 356ZWIn7/UBFZUDf80SOVBt9TZYSXo8Atcv4nJmu6Y5MMzMFaI+QmxJZKc5dVA165cQEmhOIQpRO F0K80/z+cJ+jEiTKSEp5L4C/hcuUxSgcHT5nEKCRw4Sz2oCilHJyBoEHn6jAu0s5PIQzesO5tYhw psMrRPhwE81bGIsg/QHA6uYZGZ/Zh5VBgl8jJdC+rYb8M7Iw7iwWpkiZmX0dGSy8uaDasrUzs+vP +VlPYERuBm6+qsXp9/uEELdGW6uoBYkKkFJ+AODE5oU9N6MSr8xmh4rNufDnyAjQesroYZksTJHh MuUqms14/W0WIlNgJmEmYUTpICEERRuPKsUqSJcCeCa4JBajYBr8Ph4CZHlGUWp5xBQPxfD3khDN nl8NWhPjxATiIRBClJYJIU6KJc9YBYl26e0aCrEYxYKe72mNAIXZPuvUTN0dUWvX8vnICDz8dAXe XlzDHhYiw8VXRUigmSidJ4R4NcJb97gsJkGiHKSU1wJ4+LHnd2LOfN4stwdV/mAqAa9X4PS8DFxz qT2xfUxtjA2Zfb2qAXMXVuP9j+t4jcgG/slSZJssbdurj+57aVaWmBtrm2MWJCrw7oe3n7j0wzpa T+LEBJQTILPqIw9Jw+jhmTj+KOdtsFUOIMoCFhTXYt6iat2PX5S38uVMIGoCAuL+opnZYS2wW8s0 LkGizPPyS5dCILe1gvg8EzCTABlAnDE0ExPOZsu85lzJ6ek7S2qwoyLQ/BR/ZgKqCASE39OvaNa+ a+IpwARBKjsXQhbGUwm+lwnESsDrAY45PB3DT8nAicck76jpvY/r9FhQX5XU87RcrA8T3xczAQEx t2hm9pkxZ/D7jXELUm6u9Hq6lv0MIKKY6fFWmO9nAi0RoP1Mg0/I0GMFDTootaXLEub4N6sasPjD Wn1tqLKKR0MJ07EubIgATi2ambMg3qrHLUhUgSEFZTcJyCnxVobvZwJmEdinowcn/TEdJx+brscR Mitfu/MhH3sffV6H95bXYcs2ttm2uz+4fJ3AqsUzswcBIu6IY6YIUu5Zazt4UtPWA+AJfX5CHUeA zMePPCQVfzw8HUcflgba5+SmRNNxH6+ow8df1INHQm7queSoqwCuKJqZ85QZrTVFkKgiQwrKHhGQ 15hRKc6DCagk0LO7F4f2T8XB/VP10VOXfZzjcZsct67+sRG0FvTFtw1Y84u7I92q7EfO2xEEttZI /37LC3vWmlEb0wRpeP6mXn4R+AGA14yKcR5MwCoC7dtpOLB3CvockIJ+vVLQM8eD7vt6kZZm2tcj ZFPKNvvx60Yfftnow/c/NYBiMW36jafhQsLig84kIMUdiwuz7zKrcqZ+4/IKSl8GcJ5ZleN8mICd BMhIonMnD9q1FaBpP4p+2yZT6EKV4hWgDbspv//8CgRoszjg+X2wVVsndW8IdQ0S9fUSOysD+nTb zqqAbo69dbsfPtYeO7uXy46fQJVX+vdfUNhze/xZNeVg7mjG438Afs8EAKYKnVmN5XyYQDQEaPqM /nNiAkxgbwISeMpMMaISTF3dXfxaz29EFBFl924iH2ECTIAJMAEXEGjwavIhs+tpqiBR5aSG+8yu JOfHBJgAE2ACziEgIF5ZOL07WVabmkwXpMXTcz4A5BJTa8mZMQEmwASYgFMI+ODX7lZRGdMFiSop Ie5UUVnOkwkwASbABGwmIOTL8fqsa6kFSgRpycyc9wEsbalQPs4EmAATYAKuJODzBwL/UlVzJYJE lZXAZFWV5nyZABNgAkzAFgKvFBf2/ElVycoE6fdR0iJVFed8mQATYAJMwFICDR6pKV2OUSZIhEnT cFvTYMlSaFwYE2ACTIAJmE5APrWwsNta07MNylCpIC2anvM5gFlB5fFbJsAEmAATcB+BGq8vVdna kYFDqSBRIR4/bgfgMwrkVybABJgAE3AXASnwyILZXcpU11q5IC2clbMaAi+obgjnzwSYABNgAkoI bGvwND6gJOdmmSoXJCrP25hCo6TqZmXzRybABJgAE3A4ASlw97JX9y+3opqWCBIN9SQER5S1oke5 DCbABJiAWQQEfqxon/2YWdm1lo8lgkSVyKqlEOeitLUK8XkmwASYABNwBgEZwC0rnhSNVtXGMkGa Ny+nRorA361qGJfDBJgAE2ACsROQwHtLCnPmxJ5D9HdaJkhUtZP757wACTIF58QEmAATYALOJeD3 yMD1VlfP8kB6eWPLjoaUH5sdi8lqcFweE2ACTCBRCUghH18yo/vVVrfP0hESNW7xjOzPADxrdUO5 PCbABJgAE2idgAS2BOobyMuO5clyQaIWpkpBjd1heWu5QCbABJgAEwhPQOC24jd62fL32RZBml+Y vQVC/F94KnyWCTABJsAErCQgpPzk5P7Zts1g2SJIBPik/t2eAPS1JCt5c1lMgAkwASYQmoDPr8kr J08WgdCn1R+13KghuEmDx244VJMaWd15g4/zeybABJgAE7CYgJBTFs/o/leLS92jONtGSFSLpTN6 fA2IaXvUiD8wASbABJiA1QTWeVI9SmMdRdIgWwWJKphZKwmC0hgbkYDga5gAE2ACyUpACnH1wpe6 2e5v1HZBIg8Omha4nAP5JetXgdvNBJiAzQReXjIje77NddCLt12QqBaLpvdYDOBJJwDhOjABJsAE kojAJq/0X+eU9jpCkAhGXV3KzQB+dQoYrgcTYAJMINEJSImrFxT23O6UdjpGkD6c26VSCslTd055 MrgeTIAJJDgBMd1q56mtAXWMIFFFl8zovhCQj7dWaT7PBJgAE2AC8RAQpV7p+3M8Oai411GCRA30 pHnIDv4HFY3lPJkAE2ACTABSQF7qpKk6o08cJ0hkehjQtAsB+IxK8isTYAJMgAmYRUD+t2hmzgKz cjMzH8cJEjVu6fRun0DIe8xsKOfFBJgAE0h2AlJgdWatsNUbQ7g+cKQgUYX9m3P+CYmPwlWezzEB JsAEmEDEBBoAnEd7PyO+w+ILHStIxcXCJ/2YIIByi5lwcUyACTCBxCMg8dclM3K+cHLDHCtIBG3J 7JxfpBRXOBkg140JMAEm4AICby8uzH7E6fV0tCARvMWF2a8L4Amng+T6MQEmwAScSUCU+j24GBDS mfXbXSvHCxJVtVr6b4TEl7urze+YABNgAkwgAgI+aHJc8Ws5WyO41vZLXCFIywt71krNkw+gwnZi XAEmwASYgEsISIhbF0/P+cAl1YUrBIlgLpmx789CyIvYK7hbHi2uJxNgAjYTmLNkZrepNtchquJd I0jUqqIZ3d+EkA9G1UK+mAkwASaQbAQEfhQy/RI3rBsFd42rBIkq3imQc6uUgsJVcGICTIAJMIG9 CVSLgHZOUWEn1y1xuE6QCguFPw0YD4Ff9u4HPsIEmAATSGoCUgAXFRV2W+lGCq4TJII8vzB7SwCB MwE4dsexGx8GrjMTYALuJiCBfxXNzJnl1la4UpAI9tIZPb6WQlzMRg5uffS43kyACZhKQMq3Th6Q fYepeVqcmbC4PNOLyysoux2Qd5meMWfIBJgAE3APgZVCpp/kxnWjYMSuFyRAiryCspfIaWBww/g9 E2ACTCAZCEhgS8AXOKJ4do8Nbm+va6fsdoMXspMsvxSQH+8+xu+YABNgAklBoEZq2qhEECPqrQQY ITU9dCPzy7o0CnwkIfsmxWPIjWQCTCDZCUgJed6Smd1fSxQQCTBCauoKsrzzSd9IGr4mSudwO5gA E2ACLREQErclkhhROxNmhGR02uBxm/6oBQJLAGQax/iVCTABJpBIBKSQjy+Z0f3qRGoTtSVhRkhG x1D484CU4ynorHGMX5kAE2ACCUTglX0COdckUHt2NSXhBIlatrSw+1wJ/IX3KO3qZ37DBJhAQhCQ S8iIizzWJERzmjUi4absgts3NL/0b1Lg3uBj/J4JMAEm4FICnwqZPtzte43CsU/IEZLR4KLCnPsE xP3GZ35lAkyACbiUwKpUKc5IZDGifknoEVLTg6dvnH0cwJUufRC52kyACSQzAYFf/I2BExNlr1G4 rkzoEVJTw2njbPafAbwSDgSfYwJMgAk4kMAGf8A/NBnEiNgnwQip6RHLz5eebaL0VQFR4MCHjqvE BJgAE2hGQJZByNzFM3r80OxEwn5MGkGiHjzyCpnSvrxsuhAYk7A9yg1jAkzA9QRog78mcErRjJzv XN+YKBqQBFN2u2mseFI07oPy8QJi7u6j/I4JMAEm4BwCuhhJLS/ZxIh6IKkEiRpcWDiwoaPcni8l ZjvnEeSaMAEmwAR0ApulRwx2a8TXePswqabsgmHl5kqvZ99NL0PKscHH+T0TYAJMwB4CotTjl3kL Z+Wstqd8+0tNuhGSgby4WPg6BbpRDKWXjWP8ygSYABOwicB6v/SdksxiRNyTVpCo8eR+o5PMpjDo /7PpIeRimQATSHICAuInvyZOLi7s+VOSo0ges+/wHS3FkLFl9wiJv4W/js8yASbABEwkIMU3fnhG FBd23WRirq7NKqlHSLt7TcglM3JuFVL8lR2y7qbC75gAE1BIQOKj+pSGXBaj3YyT1qhhN4I93w0Z WzpRSDwBwLPnGf7EBJgAEzCNwLuZtThn3rycGtNyTICMWJBCdOKQgo2jBASFBc4KcZoPMQEmwARi JiAgnivv0O1K2hcZcyYJeiMLUgsdS5FnRSAwTwBdWriEDzMBJsAEoiQg/7l4Zs4dgJBR3pgUl7Mg henm3Pz1fb3CO19C9g1zGZ9iAkyACbRGwC+Aq4pm5jzV2oXJfJ4FqZXezx1f2tnjxxwAJ7ZyKZ9m AkyACYQiUCGAsUUzcxaEOsnHdhNgK7vdLEK+K34tZ2tqZfVQ3kAbEg8fZAJMIDyBtQGPOIHFKDwk 4yyPkAwSEbzmFZTeCuBfyRS2IwIsfAkTYAIhCYjlnkZx1sI53X4LeZoP7kWABWkvJOEPDMkvPVsI vAigTfgr+SwTYAJJS0DI51N31vxp/vx+9UnLIIaGsyDFAG342E2DAlLOYWOHGODxLUwgsQn4hJA3 Fs3o/p/Ebqaa1rEgxcj1xAm/dEzzpbwK4NQYs+DbmAATSCACehwjTRQUTc8uTqBmWdoUNmqIEfey V/cv7ySzzwBwL7sbihEi38YEEoWAxOfw4WgWo/g6lEdI8fHT7x6aX3a6FPIFAPuYkB1nwQSYgIsI CCEeTdlZdROvF8XfaSxI8TPUcxiev6mXH4GZEDjKpCw5GybABJxNoAoCf1o8I+cVZ1fTPbXjKTuT +mphYbe1qVXVJ9KvJZ7CMwkqZ8MEnEtgpcePo1mMzO0gHiGZy1PPbejYjWdKKZ7hKTwFcDlLJmAz ASnk44GMhhuLn+9VZ3NVEq54FiRFXZo7ZkMPzau9LIBTFBXB2TIBJmAtge1SYuKSwhxyJcZJAQGe slMAlbIsnt1jwz4yOw8Q/wAku5lXxJmzZQIWEVgqJA5nMVJLm0dIavnquQ8bV3pUIACywhtgQXFc BBNgAuYRqBPAbScOyH548mQRMC9bzikUARakUFQUHDsuf31Gpua5FxLXsi88BYA5SyZgNgGJLwFx /uLC7FVmZ835hSbAghSai7KjeQVlgwXw5P+3d26xVVRRGP7/PbW2oqYgSIGC8a6QoPLgJYKppYL1 Fi+p0AcUrw/6oDHxHhU1XuOLiYmJqVGjRvAIGjERQ9GCGNEgmGgUlYhSORRRqQhKy5lZZho1EUs9 9zMz5386ObPXWnutb03yZ86Zvbe2HSoZYgUWgUIJDBj48K8NjQ/rVNdCUebmL0HKjVdRrAeflliz ALBbANQUJaiCiIAIFEzAgA/DPS8CAAAGbElEQVRovF5PRQWjzCuABCkvbMVxap275RQL2AlgWnEi KooIiECeBHYCvGvG5Man9V9RngSL4CZBKgLEQkI0N1uNO7z3ZsIeAFBfSCz5ioAI5EOAS/2Mf0P4 Zmw+3vIpHgEJUvFYFhSp9bJtRwUueIa0mQUFkrMIiEC2BLbB7KYVqQmLsnWQXWkJSJBKyzfH6MaW Oen5ND4OYHSOzjIXARHIjoABfK7GMre+k5r4S3YusioHAQlSOSjnOMfs9p5RGboHAV6vlx5yhCdz ERiewDrS3di1qHHN8GYarQQBCVIlqGc55+BLDz6fBDEjSxeZiYAIDE3gJxrunj5lXKdeWhgaUBSu SpCi0IVhczC2XJ6eS/BRAJOGNdWgCIjAvgQGQDzl9/c/2P3GkX37Dup7tAhIkKLVj/1mE65dGgHv JiNuB9CwX0MNiIAI/E3gdd/827pTEzf+fUGf0SYgQYp2f/6TXXNHerTn2/0ArgN4wH8MdEEEqpwA zT6C5+7QceLxuxEkSPHr2WDGze09x3j0FgDoAKBd22PaR6VdVAJfkLina9G41wFaUSMrWFkISJDK grl0k8zs6JmKjHsI5AWlm0WRRSDCBIjvYbxvlDW+lErRj3CmSu1/CEiQ/gdQXIZb5/SebkFwL4i2 uOSsPEWgQAKbaXx0JH55NpWaMlBgLLlHgIAEKQJNKGYKLe1bTyXtXgDnFzOuYolAhAh8R8MjI7Hj eQlRhLpShFQkSEWAGMUQ4aGAvo87SVys/5ii2CHllAeBrwh7fEfD+Bd1LEQe9GLgIkGKQZMKSfHs 9i3HO8dbYZgHoLaQWPIVgQoR+JjEY9NPHPeGFrVWqANlmlaCVCbQlZ7mnLmbxwfm3QzjdVrHVOlu aP4sCAQElhn4xIpXx72Xhb1MEkBAgpSAJuZSwqx5vSP8Absy3OUYwHG5+MpWBMpAYDdgL4D25IpF TV+XYT5NESECEqQINaOcqSxYYG7lF73nerAbDZgNwCvn/JpLBP5FgPjGjM8EA3s6tcXPv8hU1RcJ UlW1e+hiW9vTkwLyKsKu1n55QzPS1ZIQ6AfwGh07uxY2rtRi1pIwjlVQCVKs2lXaZMOnplVf9s5G YNeSdqG2Jiot7yqO/hkMz9bAf1HnEVXxXTBE6RKkIaDoEjCzY9tY8zPzSV4Dw7FiIgIFEtgF8FWY 37ki1fRhgbHknlACEqSENrZ4ZYXHX2ydQeIKGi41YGTxYitSwgkEAFbC7OXa2rrU2y8ftjPh9aq8 AglIkAoEWE3ubW3fHDhwyIg2g3UQvBBAfTXVr1qzJGBYC+AV52UWLl84KZ2ll8xEABIk3QR5ETjz ou2H1B04cB7Jyww4D8CIvALJKQkEDLCPaG4JAre4a/HYb5NQlGooPwEJUvmZJ27G8PDAg+iFr45f YkAbgTGJK1IF7UsgA9gqI5cGe4PXupc0/bCvgb6LQK4EJEi5EpP9sATa28372UufxoAXwHg+aFOH ddBgnAj8BGAZzN4i6pd1pUb9GqfklWv0CUiQot+jWGfYfOkPTTU1nGVw5xhspp6eYtXO8EiHNQSW +84tH+2PXavzhmLVv9glK0GKXcvim3C4zmn1hvRJ8F2rITgL5HTtqxepfvowrIfDKgZ8NwNvZXfq 8F2RylDJJJqABCnR7Y12ceHPezu89FTzeRaIGQBOBzAh2lknKrvdMKwDsdrI9/v/qFn9wZtjfktU hSomVgQkSLFqV/KTbWnvmeCcdxrMzjDwVAAnAzg0+ZWXvMKMARsIfGLAGs+CNXu3T/i8u5uZks+s CUQgSwISpCxByaxSBIwtc348CgimATaNNihQk7Xn3rD92EmzL438FMT6gO7Tg3cHny1dOv73Yb00 KAIVJiBBqnADNH1+BMJ1ULUH+ZO9wJ9ixAk0d7yZHQ3iaAB1+UWNlZcBCF+13mjARho2mLPPXcAN Xanxm2NViZIVgb8ISJB0KySMgHHW3HSTH7hjBp+iDEcYrYmGpsHvxMSY/AQY7oS9zYAewLYQDD83 G7DJkRsz9f2bup8/ck/CmqdyqpyABKnKb4BqLL95/qa62j21Y3xzjQDHGvwxNDfKYA2ObDCzBpg1 GFy9ozWY8QA4Oxhm9QT/efraz75+vxMIxQTh9gUg+mDYS2CXGcKxPwJaH8k+GnfArC8g+hyw3Rx+ 9PZie+DqtmqNTzXemar5T7boKrYfCqI6AAAAAElFTkSuQmCC"/></symbol><symbol viewBox="0 0 24 24" id="webpack" xmlns="http://www.w3.org/2000/svg"><path d="M19.376 15.988l-7.709 4.45-7.708-4.45V7.087l7.708-4.45 7.709 4.45z" fill="#fff" fill-opacity=".785" stroke-width="0"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18.21 0 .41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.939v2.104h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07l7-3.94zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#1c78c0"/></symbol><symbol viewBox="0 0 24 24" id="wolframlanguage" xmlns="http://www.w3.org/2000/svg"><title>wolframLanguage</title><g transform="scale(.12121)" fill="none" fill-rule="evenodd"><circle cx="99.197" cy="98.946" r="83.28" fill="#212121" stroke-width=".841"/><path d="M182.529 98.828a83.406 83.406 0 0 1-39.14 70.721.064.064 0 0 1-.038.019l-28.62-35.665 23.71 2.612s11.385 1.177 13.978 0c2.373-.938 15.175-18.963 15.175-18.963s-36.75-23.23-49.312-36.032c1.434-21.575-1.656-50.269-1.656-50.03-9.251 9.234-10.429 10.669-19.68 19.203-4.028-13.04-5.923-17.547-9.95-30.588-12.104 9.95-21.337 26.799-27.977 46.48a78.68 78.68 0 0 0-4.23 5.094 109.774 109.774 0 0 0-2.667 3.66 114.558 114.558 0 0 0-5.132 8.002 172.555 172.555 0 0 0-3.403 6.051c-7.706 14.475-14.034 31.066-19.515 46.001a.858.858 0 0 1-.092-.184c-14.988-30.912-9.502-67.85 13.822-93.072 23.325-25.223 59.723-33.575 91.71-21.045 31.988 12.53 53.029 43.382 53.017 77.736z" fill="#e53935"/><path d="M101.452 69.178s-1.416-8.295-2.373-11.367c6.401-6.18 7.357-7.118 13.52-13.04.477 11.845.238 18.006-.479 32.481-3.55-3.568-10.668-8.074-10.668-8.074zm-27.737 40.778s-6.64-4.029-11.624-4.728c1.435-3.329 5.223-7.596 6.18-8.773-1.913.699-15.653 6.86-17.087 12.084a74.804 74.804 0 0 1 11.385 3.79 35.993 35.993 0 0 0-8.774 20.158s21.815-3.33 38.185-1.196c.283.168.609.251.938.24l8.534.239 27.111 45.136.221.35c-.037.018-.055.037-.073.037-51.133 18.485-88.085-15.543-95.976-27.443.034-.102.058-.206.074-.313 7.1-30.017 15.855-65.939 30-76.552 7.356-12.82 9.49-31.783 22.751-41.734 3.33 9.951 8.553 30.588 12.103 40.539 15.653 15.652 39.361 35.094 55.234 43.15 1.656.956 3.79 7.596 3.79 7.596l-6.401 8.056-68.276-6.879a54.462 54.462 0 0 0-4.58-.183 86.848 86.848 0 0 0-14.144 1.36c3.311-8.295 10.43-14.935 10.43-14.935zm22.054-8.774c3.789-.46 7.817.956 12.323 3.568 4.267-1.195 4.745-1.434 9.013-2.612-5.463-4.028-11.386-8.295-19.442-7.118a47.249 47.249 0 0 0-1.894 6.162z" fill="#fff" stroke-width=".936"/></g></symbol><symbol viewBox="0 0 24 24" id="word" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M7 13l1.5 7h2l1.5-3 1.5 3h2l1.5-7h1v-2h-4v2h1l-.9 4.2L13 15h-2l-1.1 2.2L9 13h1v-2H6v2h1z" fill="#01579b"/></symbol><symbol viewBox="0 0 24 24" id="xaml" xmlns="http://www.w3.org/2000/svg"><path d="M18.93 12l-3.47 6H8.54l-3.47-6 3.47-6h6.92l3.47 6m4.84 0l-4.04 7L18 18l3.46-6L18 6l1.73-1 4.04 7M.23 12l4.04-7L6 6l-3.46 6L6 18l-1.73 1-4.04-7z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="xml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="yaml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="yang" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8 8 8 0 0 0 8 8 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 2.5A1.5 1.5 0 0 1 13.5 8 1.5 1.5 0 0 1 12 9.5 1.5 1.5 0 0 1 10.5 8 1.5 1.5 0 0 1 12 6.5m0 8a1.5 1.5 0 0 0-1.5 1.5 1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0-1.5-1.5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 289.99999 290.00001" id="yarn" xmlns="http://www.w3.org/2000/svg"><path d="M250.733 218.418c-12.39 2.943-18.661 5.653-33.993 15.641-24.004 15.487-50.176 22.688-50.176 22.688s-2.168 3.252-8.44 4.723c-10.84 2.633-51.647 4.878-55.364 4.956-9.988.077-16.105-2.555-17.809-6.66-5.188-12.388 7.434-17.809 7.434-17.809s-2.788-1.703-4.414-3.252c-1.471-1.47-3.02-4.413-3.484-3.33-1.936 4.724-2.943 16.261-8.13 21.45-7.125 7.2-20.598 4.8-28.573.619-8.75-4.646.62-15.564.62-15.564s-4.724 2.788-8.518-2.942c-3.407-5.266-6.582-14.248-5.73-25.32 1.084-12.777 15.176-25.011 15.176-25.011s-2.477-18.661 5.653-37.787c7.356-17.422 27.179-31.437 27.179-31.437s-16.648-18.352-10.454-35c4.027-10.84 5.653-10.763 6.97-11.227 4.645-1.781 9.136-3.717 12.466-7.356 16.648-17.964 37.864-14.557 37.864-14.557s9.911-30.431 19.203-24.469c2.865 1.859 13.163 24.778 13.163 24.778s10.996-6.426 12.235-4.026c6.659 12.931 7.433 37.632 4.49 52.654-4.955 24.778-17.344 38.096-22.3 46.459-1.161 1.936 13.319 8.053 22.456 33.373 8.44 23.152.929 42.587 2.245 44.756.232.387.31.542.31.542s9.679.774 29.114-11.228c10.376-6.427 22.688-13.628 36.703-13.783 13.55-.232 14.247 15.719 4.104 18.12z" fill="#2c8ebb" stroke-width=".774"/></symbol><symbol viewBox="0 0 24 24" id="zip" xmlns="http://www.w3.org/2000/svg"><path d="M14 17h-2v-2h-2v-2h2v2h2m0-6h-2v2h2v2h-2v-2h-2V9h2V7h-2V5h2v2h2m5-4H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#afb42b"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/icon_image_comment.svg b/app/assets/images/icon_image_comment.svg
deleted file mode 100644
index cf6cb972940..00000000000
--- a/app/assets/images/icon_image_comment.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
diff --git a/app/assets/images/icon_image_comment@2x.svg b/app/assets/images/icon_image_comment@2x.svg
deleted file mode 100644
index 83be91d3705..00000000000
--- a/app/assets/images/icon_image_comment@2x.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index 4e967936ce0..296cb856734 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":180,"spriteSize":82176,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":189,"spriteSize":85766,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o-open","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index 77ce6b2d89f..8d5426da19c 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="evfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="evsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="evthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="bookmark" xmlns="http://www.w3.org/2000/svg"><path d="M6.746 10.505a2 2 0 0 1 2.508 0L11 11.911V3H5v8.91l1.746-1.405zM5 1h6a2 2 0 0 1 2 2v10.999a1 1 0 0 1-1.627.779L8 12.064l-3.373 2.714A1 1 0 0 1 3 13.998V3a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-o" xmlns="http://www.w3.org/2000/svg"><path d="M13 5l-4.365-.005a2 2 0 0 1-1.882-1.33A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0-2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-o-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.59 5.464a2.998 2.998 0 0 1 1.096 3.845l-1.666 3.436A4 4 0 0 1 10.46 15H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.558a2 2 0 0 1 1.898 1.368l.21.632h4.973a2 2 0 0 1 2 2 2 2 0 0 1-.027.329l-.023.135zM5.285 7a1 1 0 0 0-.9.564l-1.939 4a1 1 0 0 0 .9 1.436h7.074a2 2 0 0 0 1.8-1.128l1.665-3.436a1 1 0 0 0-.9-1.436h-7.7z"/></symbol><symbol viewBox="0 0 16 16" id="folder-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 6H5.333a2 2 0 0 0-1.664.89l-3.333 5a2 2 0 0 0-.285.662A3.017 3.017 0 0 1 0 12V3a2 2 0 0 1 2-2h2.838a3 3 0 0 1 2.828 2H11a3 3 0 0 1 3 3zM5.333 7h9.53a1 1 0 0 1 .875 1.486l-2.492 4.485A2 2 0 0 1 11.498 14H2a1 1 0 0 1-.832-1.555l3.333-5A1 1 0 0 1 5.333 7z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M9 13h3v-3H4v3h3v-1a1 1 0 0 1 2 0v1zm5-3v3.659c0 .729-.657 1.341-1.5 1.341h-9c-.843 0-1.5-.612-1.5-1.341V10h-.88C.502 10 0 9.486 0 8.853c0-.307.12-.601.333-.816l6.405-6.463a1.56 1.56 0 0 1 2.374-.052L15.66 8.03c.444.441.455 1.167.024 1.622a1.108 1.108 0 0 1-.804.348H14zM7.95 3.273l-4.595 4.64h9.264l-4.67-4.64z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 38 38" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#1F78D1"/><path fill="#FFF" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 38 38" id="image-comment-light" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-external" xmlns="http://www.w3.org/2000/svg"><path d="M11 4a5.99 5.99 0 0 0-2 .341V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2.528a6.003 6.003 0 0 0 2.705 1.736A2.99 2.99 0 0 1 8 16H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3v1zM8.212 8.97l-.568-.876A.25.25 0 0 1 7.66 7.8l.404-.5a.25.25 0 0 1 .284-.076l.938.36c.256-.182.543-.325.85-.42l.323-.988a.25.25 0 0 1 .237-.173h.643a.25.25 0 0 1 .238.173l.321.989c.308.094.595.237.852.418l.937-.359a.25.25 0 0 1 .284.076l.404.5a.25.25 0 0 1 .016.293l-.568.875c.113.297.18.616.192.95l.9.54a.25.25 0 0 1 .114.27l-.145.627a.25.25 0 0 1-.221.192l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.282a.25.25 0 0 1-.29-.051l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.905a.25.25 0 0 1-.29.05l-.577-.281a.25.25 0 0 1-.138-.26L9 12.254a3.015 3.015 0 0 1-.512-.607l-1.114-.098a.25.25 0 0 1-.222-.192l-.145-.627a.25.25 0 0 1 .115-.27l.899-.54c.012-.334.08-.653.192-.95zm2.806 2.034a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7H2a1 1 0 1 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2H9V2a1 1 0 1 0-2 0v5z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="podcast" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862a1 1 0 0 1-.785 1.177A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-1-1 1 1 0 0 1 .02-.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 7.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4.464 2.464A1 1 0 0 1 5.88 3.88a3 3 0 0 0 0 4.242 1 1 0 0 1-1.415 1.415 5 5 0 0 1 0-7.072zm7.072 7.072A1 1 0 0 1 10.12 8.12a3 3 0 0 0 0-4.242 1 1 0 0 1 1.415-1.415 5 5 0 0 1 0 7.072zM2.343.343a1 1 0 1 1 1.414 1.414 6 6 0 0 0 0 8.486 1 1 0 1 1-1.414 1.414 8 8 0 0 1 0-11.314zm11.314 11.314a1 1 0 1 1-1.414-1.414 6 6 0 0 0 0-8.486A1 1 0 0 1 13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.625 4.423A4.897 4.897 0 0 1 8.079 3c2.73 0 4.944 2.239 4.944 5s-2.214 5-4.944 5c-1.41 0-2.723-.6-3.655-1.633a.98.98 0 0 0-1.397-.066 1.008 1.008 0 0 0-.064 1.413A6.87 6.87 0 0 0 8.079 15C11.9 15 15 11.866 15 8s-3.099-7-6.921-7A6.866 6.866 0 0 0 3.08 3.158L1.833 2.137a.49.49 0 0 0-.695.074.504.504 0 0 0-.11.311L1 7.26a.497.497 0 0 0 .6.492l4.576-1.013a.5.5 0 0 0 .206-.877L4.625 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.375 4.423A4.897 4.897 0 0 0 7.921 3c-2.73 0-4.944 2.239-4.944 5s2.214 5 4.944 5c1.41 0 2.723-.6 3.655-1.633a.98.98 0 0 1 1.397-.066c.403.373.432 1.005.064 1.413A6.87 6.87 0 0 1 7.921 15C4.1 15 1 11.866 1 8s3.099-7 6.921-7c1.915 0 3.706.792 4.999 2.158l1.247-1.021a.49.49 0 0 1 .695.074c.07.088.11.198.11.311L15 7.26a.497.497 0 0 1-.6.492L9.824 6.739a.5.5 0 0 1-.206-.877l1.757-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="fcfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="fcsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="fcthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8c1.657 0 3 1.373 3 3.067v7.346c0 1.065-.54 2.053-1.426 2.611l-4 2.52a2.944 2.944 0 0 1-3.148 0l-4-2.52A3.083 3.083 0 0 1 1 10.414V3.066C1 1.373 2.343 0 4 0zm0 2.045c-.552 0-1 .457-1 1.022v7.346c0 .355.18.685.475.87l4 2.52a.981.981 0 0 0 1.05 0l4-2.52c.295-.185.475-.515.475-.87V3.067c0-.565-.448-1.022-1-1.022H4zm0 1.533c0-.282.224-.511.5-.511h4V12.1a.52.52 0 0 1-.069.258.494.494 0 0 1-.684.183l-3.5-2.098a.513.513 0 0 1-.247-.44V3.577z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="staged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.828 3.536l1.415-1.415a1 1 0 1 1 1.414 1.415l-2.121 2.12a.997.997 0 0 1-1.415 0L9.707 4.244a1 1 0 0 1 1.414-1.415l.707.708zM2 3h5a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="unstaged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.572 10.506c.867 1.42.375 3.247-1.098 4.082a3.184 3.184 0 0 1-1.57.412h-9.81C1.387 15 0 13.665 0 12.018a2.9 2.9 0 0 1 .427-1.512L5.332 2.47C6.2 1.05 8.096.577 9.57 1.412c.453.257.831.622 1.098 1.059l4.905 8.035zM8.89 3.479a1.014 1.014 0 0 0-.366-.353 1.053 1.053 0 0 0-1.412.353l-4.905 8.035a.967.967 0 0 0-.143.504c0 .549.462.994 1.032.994h9.81c.184 0 .364-.048.523-.137a.974.974 0 0 0 .366-1.361L8.889 3.479zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/image_comment_light_cursor.svg b/app/assets/images/illustrations/image_comment_light_cursor.svg
new file mode 100644
index 00000000000..ac712ea0c96
--- /dev/null
+++ b/app/assets/images/illustrations/image_comment_light_cursor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/image_comment_light_cursor@2x.svg b/app/assets/images/illustrations/image_comment_light_cursor@2x.svg
new file mode 100644
index 00000000000..02943acd9d7
--- /dev/null
+++ b/app/assets/images/illustrations/image_comment_light_cursor@2x.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/job_not_triggered.svg b/app/assets/images/illustrations/job_not_triggered.svg
new file mode 100644
index 00000000000..e13c1cb0a7d
--- /dev/null
+++ b/app/assets/images/illustrations/job_not_triggered.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 310 141" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><g fill-rule="nonzero"><path fill="#e5e5e5" d="M48 69c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 48 69m14 0c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 62 69"/><g fill="#31af64"><path d="M19 88C8.507 88 0 79.493 0 69s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="M17.07 71.02l-2.829-2.828a1.995 1.995 0 0 0-2.828 0 1.997 1.997 0 0 0 0 2.83l4.243 4.243a1.993 1.993 0 0 0 2.823.005l7.79-7.79a1.998 1.998 0 0 0-.007-2.822 1.99 1.99 0 0 0-2.822-.006l-6.37 6.37v-.001"/></g></g><g transform="translate(187)"><rect width="116" height="134" y="7" fill="#f9f9f9" rx="10"/><rect width="116" height="134" x="5" y="2" fill="#fff" rx="10"/><path fill="#eee" fill-rule="nonzero" d="M15 4a8 8 0 0 0-8 8v114a8 8 0 0 0 8 8h96a8 8 0 0 0 8-8V12a8 8 0 0 0-8-8H15m0-4h96c6.627 0 12 5.373 12 12v114c0 6.627-5.373 12-12 12H15c-6.627 0-12-5.373-12-12V12C3 5.373 8.373 0 15 0"/><g transform="translate(23 25)"><g fill="#e1dbf1"><rect width="16" height="4" rx="2"/><rect width="16" height="4" x="32" y="12" rx="2"/></g><rect width="16" height="4" x="44" fill="#eee" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#e1dbf1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="20" fill="#fee1d3" rx="2" id="a"/><rect width="8" height="4" x="32" y="36" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="64" fill="#fef0e8" rx="2" id="b"/><rect width="12" height="4" x="16" y="48" fill="#e1dbf1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#fc6d26" rx="2"/><g fill="#e1dbf1"><rect width="4" height="4" x="56" y="36" rx="2"/><rect width="4" height="4" x="64" y="60" rx="2"/></g><rect width="4" height="4" x="72" y="60" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="32" fill="#fc6d26" rx="2" id="c"/><g fill="#eee"><rect width="28" height="4" y="36" rx="2"/><rect width="28" height="4" x="44" y="48" rx="2"/></g><rect width="28" height="4" x="32" y="60" fill="#efedf8" rx="2"/><rect width="28" height="4" y="12" fill="#6b4fbb" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#c3b8e3" rx="2"/><rect width="8" height="4" y="24" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6b4fbb" rx="2"/><rect width="12" height="4" y="48" fill="#fc6d26" rx="2"/><g fill="#fef0e8"><rect width="12" height="4" y="60" rx="2"/><rect width="12" height="4" x="16" y="60" rx="2"/></g></g><g transform="translate(23 97)"><rect width="16" height="4" fill="#efedf8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#fc6d26" rx="2"/><rect width="16" height="4" x="44" fill="#6b4fbb" rx="2"/><use xlink:href="#a"/><rect width="8" height="4" x="38" y="12" fill="#fef0e8" rx="2"/><use xlink:href="#b"/><use xlink:href="#c"/><rect width="14" height="4" y="12" fill="#eee" rx="2"/></g></g><g fill-rule="nonzero"><path fill="#eee" d="M109 101a2 2 0 1 1 0-4c2.524 0 5-.346 7.379-1.02a2 2 0 0 1 1.091 3.849 31.007 31.007 0 0 1-8.47 1.172m18.09-5.825a31.174 31.174 0 0 0 6.187-5.899 2 2 0 1 0-3.131-2.489 27.133 27.133 0 0 1-5.393 5.142 2.001 2.001 0 0 0 2.337 3.247m11.297-15.288a30.923 30.923 0 0 0 1.576-8.407 2 2 0 1 0-3.996-.188 26.875 26.875 0 0 1-1.372 7.32 2 2 0 1 0 3.791 1.275m.283-18.89a30.855 30.855 0 0 0-3.593-7.763 2 2 0 1 0-3.362 2.166 26.905 26.905 0 0 1 3.128 6.757 2 2 0 0 0 3.828-1.16M127.875 45.41a30.973 30.973 0 0 0-7.435-4.228 2 2 0 0 0-1.477 3.717 26.936 26.936 0 0 1 6.474 3.682 2 2 0 0 0 2.438-3.172m-17.834-6.391a31.09 31.09 0 0 0-8.5.886 2 2 0 0 0 .959 3.883 27.06 27.06 0 0 1 7.408-.771 2 2 0 1 0 .132-3.998m-18.272 5.207a31.139 31.139 0 0 0-6.383 5.688 2 2 0 1 0 3.045 2.593 27.152 27.152 0 0 1 5.564-4.957 2 2 0 1 0-2.226-3.324M79.96 59.121a30.864 30.864 0 0 0-1.862 8.349 2 2 0 1 0 3.987.323c.203-2.506.75-4.946 1.62-7.268a2 2 0 1 0-3.746-1.404m-.923 18.873a30.827 30.827 0 0 0 3.327 7.881 2.001 2.001 0 0 0 3.435-2.051 26.785 26.785 0 0 1-2.895-6.859 2 2 0 0 0-3.865 1.029M89.301 93.94a31.008 31.008 0 0 0 7.286 4.476 2 2 0 1 0 1.603-3.665 26.983 26.983 0 0 1-6.346-3.899 2 2 0 0 0-2.543 3.087m17.61 6.991a2 2 0 0 1 .265-3.991c.601.04 1.205.06 1.812.06a1.999 1.999 0 1 1-.001 3.999c-.695 0-1.387-.023-2.076-.069"/><path fill="#fc0" d="M117.78 63.798c.241.268.288.563.14.884l-10.848 23.24c-.174.334-.455.502-.843.502-.054 0-.148-.014-.282-.04a.855.855 0 0 1-.512-.382.761.761 0 0 1-.09-.603l3.957-16.232-8.156 2.03a1.08 1.08 0 0 1-.241.02.93.93 0 0 1-.623-.222c-.24-.2-.328-.462-.26-.783l4.04-16.574a.858.858 0 0 1 .321-.462.917.917 0 0 1 .563-.18h6.59c.254 0 .468.083.642.25a.797.797 0 0 1 .261.593.818.818 0 0 1-.1.362l-3.435 9.301 7.955-1.969c.107-.027.187-.04.241-.04.254 0 .482.1.683.301"/><path fill="#e5e5e5" d="M148 69c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 148 69m14 0c0-1.105.887-2 1.998-2h4c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.992 1.992 0 0 1 162 69"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/manual_action.svg b/app/assets/images/illustrations/manual_action.svg
new file mode 100644
index 00000000000..85735855b46
--- /dev/null
+++ b/app/assets/images/illustrations/manual_action.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 398 151" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><path fill="#fef0e8" stroke="#fc6d26" stroke-width="4" d="M57.7 106.5h21.6a4.2 4.2 0 0 1 4.2 4.2v5.6a4.2 4.2 0 0 1-4.2 4.2H57.7a4.2 4.2 0 0 1-4.2-4.2v-5.6a4.2 4.2 0 0 1 4.2-4.2"/><g transform="translate(42 117)"><rect width="52" height="23" x=".5" y=".5" fill="#fff" stroke="#eee" stroke-width="4" rx="4.2"/><g fill="#fdc4a8"><rect width="11" height="2" x="8" y="8" rx="1"/><rect width="11" height="2" x="8" y="14" rx="1"/></g></g><g fill-rule="nonzero"><path fill="#e1dbf1" d="M96.31 132.32c1.048 0 1.648.007 4.319.042 11.523.153 18.377-.12 26.32-1.533 24.23-4.309 38.521-18.02 38.521-45.03 0-31.02 21.885-44.487 66.903-40.522l.351-3.985c-47.09-4.147-71.25 10.727-71.25 44.507 0 24.868-12.746 37.1-35.22 41.09-7.623 1.356-14.284 1.621-25.567 1.471a287.717 287.717 0 0 0-4.372-.042v4"/><path fill="#eee" d="M242 57.678c-6.29-1.373-11-6.976-11-13.678 0-6.702 4.71-12.304 11-13.678v4.136c-4.057 1.274-7 5.065-7 9.542 0 4.478 2.943 8.268 7 9.542v4.136"/></g><g transform="translate(242)"><rect width="116" height="134" y="7" fill="#f9f9f9" rx="10"/><rect width="116" height="134" x="5" y="2" fill="#fff" rx="10"/><path fill="#eee" fill-rule="nonzero" d="M15 4a8 8 0 0 0-8 8v114a8 8 0 0 0 8 8h96a8 8 0 0 0 8-8V12a8 8 0 0 0-8-8H15m0-4h96c6.627 0 12 5.373 12 12v114c0 6.627-5.373 12-12 12H15c-6.627 0-12-5.373-12-12V12C3 5.373 8.373 0 15 0"/><g transform="translate(23 25)"><g fill="#e1dbf1"><rect width="16" height="4" rx="2"/><rect width="16" height="4" x="32" y="12" rx="2"/></g><rect width="16" height="4" x="44" fill="#eee" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#e1dbf1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="20" fill="#fee1d3" rx="2" id="a"/><rect width="8" height="4" x="32" y="36" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="64" fill="#fef0e8" rx="2" id="b"/><rect width="12" height="4" x="16" y="48" fill="#e1dbf1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#fc6d26" rx="2"/><g fill="#e1dbf1"><rect width="4" height="4" x="56" y="36" rx="2"/><rect width="4" height="4" x="64" y="60" rx="2"/></g><rect width="4" height="4" x="72" y="60" fill="#fc6d26" rx="2"/><rect width="8" height="4" x="32" fill="#fc6d26" rx="2" id="c"/><g fill="#eee"><rect width="28" height="4" y="36" rx="2"/><rect width="28" height="4" x="44" y="48" rx="2"/></g><rect width="28" height="4" x="32" y="60" fill="#efedf8" rx="2"/><rect width="28" height="4" y="12" fill="#6b4fbb" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#c3b8e3" rx="2"/><rect width="8" height="4" y="24" fill="#fef0e8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6b4fbb" rx="2"/><rect width="12" height="4" y="48" fill="#fc6d26" rx="2"/><g fill="#fef0e8"><rect width="12" height="4" y="60" rx="2"/><rect width="12" height="4" x="16" y="60" rx="2"/></g><g transform="translate(0 72)"><rect width="16" height="4" fill="#efedf8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#fc6d26" rx="2"/><rect width="16" height="4" x="44" fill="#6b4fbb" rx="2"/><use xlink:href="#a"/><rect width="8" height="4" x="38" y="12" fill="#fef0e8" rx="2"/><use xlink:href="#b"/><use xlink:href="#c"/><rect width="14" height="4" y="12" fill="#eee" rx="2"/></g></g></g><g transform="translate(330 83)"><circle cx="33" cy="33" r="33" fill="#fff"/><g fill-rule="nonzero"><path fill="#eee" d="M33 68C13.67 68-2 52.33-2 33S13.67-2 33-2s35 15.67 35 35-15.67 35-35 35m0-4c17.12 0 31-13.879 31-31C64 15.88 50.121 2 33 2 15.88 2 2 15.879 2 33c0 17.12 13.879 31 31 31"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width=".968" d="M42.383 34.655v-3.308l-2.112-.343c-.116-.456-.351-.913-.703-1.598l1.29-1.711-2.463-2.398-1.76 1.256a6.347 6.347 0 0 0-1.642-.684l-.233-2.055h-3.401l-.352 2.055c-.586.114-1.055.342-1.642.684l-1.76-1.255-2.463 2.397 1.173 1.711c-.352.57-.469 1.027-.704 1.598l-1.995.228v3.31l2.112.342c.116.57.351 1.027.703 1.598l-1.172 1.712 2.463 2.397 1.759-1.141c.469.227 1.056.456 1.642.684l.352 2.055h3.518l.352-2.055c.586-.114 1.055-.342 1.642-.684l1.76 1.255 2.463-2.397-1.29-1.712a6.03 6.03 0 0 0 .703-1.598l1.76-.344M33 36.367c-1.994 0-3.519-1.484-3.519-3.424 0-1.941 1.525-3.424 3.519-3.424 1.994 0 3.519 1.483 3.519 3.424 0 1.94-1.525 3.424-3.519 3.424" stroke-linecap="round" stroke-linejoin="bevel"/><path fill="#e1dbf1" d="M33 53.563c-11.598 0-21-9.206-21-20.563s9.402-20.563 21-20.563S54 21.643 54 33s-9.402 20.563-21 20.563m0-4.375c9.13 0 16.532-7.248 16.532-16.188 0-8.94-7.402-16.188-16.532-16.188-9.13 0-16.532 7.248-16.532 16.188 0 8.94 7.402 16.188 16.532 16.188"/></g></g><path fill="#fff" d="M164 114c14.912 0 27-12.09 27-27 0-14.912-12.09-27-27-27-14.912 0-27 12.09-27 27 0 14.912 12.09 27 27 27"/><g fill-rule="nonzero"><path fill="#eee" d="M164 118c-17.12 0-31-13.879-31-31 0-17.12 13.879-31 31-31 17.12 0 31 13.879 31 31 0 17.12-13.879 31-31 31m0-4c14.912 0 27-12.09 27-27 0-14.912-12.09-27-27-27-14.912 0-27 12.09-27 27 0 14.912 12.09 27 27 27"/><path fill="#fc0" d="M172.78 80.798c.241.268.288.563.14.884l-10.848 23.24c-.174.334-.455.502-.843.502-.054 0-.148-.014-.282-.04a.855.855 0 0 1-.512-.382.761.761 0 0 1-.09-.603l3.957-16.232-8.156 2.03a1.08 1.08 0 0 1-.241.02.93.93 0 0 1-.623-.222c-.24-.2-.328-.462-.26-.783l4.04-16.574a.858.858 0 0 1 .321-.462.917.917 0 0 1 .563-.18h6.59c.254 0 .468.083.642.25a.797.797 0 0 1 .261.593.818.818 0 0 1-.1.362l-3.435 9.301 7.955-1.969c.107-.027.187-.04.241-.04.254 0 .482.1.683.301"/></g><g><path fill="#eee" fill-rule="nonzero" d="M37.801 99.01l5.355 2.648c2.271 1.122 4.643-.252 4.809-2.778l.487-7.546a27.675 27.675 0 0 0 2.87-4.076c7.594-13.152 3.088-29.972-10.07-37.565-13.153-7.594-29.971-3.087-37.566 10.07-7.594 13.154-3.087 29.973 10.07 37.565a27.46 27.46 0 0 0 24.05 1.687m.952-3.992a2.002 2.002 0 0 0-1.698-.035 23.454 23.454 0 0 1-21.299-1.124c-11.24-6.488-15.09-20.86-8.602-32.1 6.49-11.239 20.862-15.09 32.1-8.601 11.239 6.489 15.09 20.862 8.6 32.1a23.519 23.519 0 0 1-2.849 3.939 1.995 1.995 0 0 0-.504 1.204l-.466 7.229-5.285-2.613"/><path fill="#fdc4a8" d="M21.137 70.471A7.495 7.495 0 0 0 27.5 74c2.684 0 5.04-1.41 6.363-3.529C36.377 71.869 38 74.267 38 77.674c0 5.799-2.739 9.587-10.5 9.587S17 83.473 17 77.674c0-3.407 1.622-5.804 4.137-7.203M27.5 72a5.5 5.5 0 1 1 0-11 5.5 5.5 0 1 1 0 11"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/merge_request_changes_empty.svg b/app/assets/images/illustrations/merge_request_changes_empty.svg
index 707efa736e4..40efeb2de57 100644
--- a/app/assets/images/illustrations/merge_request_changes_empty.svg
+++ b/app/assets/images/illustrations/merge_request_changes_empty.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 374 268" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="d" width="230" height="176" rx="10" fill="#fff"/><rect id="e" width="14" rx="2" height="4"/><rect id="f" width="14" x="40" rx="2" height="4"/><rect id="g" width="14" x="40" y="24" rx="2" height="4"/><rect id="h" width="7" x="20" y="12" rx="2" height="4"/><rect id="i" width="7" y="24" rx="2" height="4"/><rect id="j" width="7" x="33" y="12" rx="2" height="4"/><circle id="l" cx="31" cy="31" r="31"/><circle id="c" cx="35" cy="35" r="35"/><circle id="a" cx="44" cy="44" r="44"/><circle id="b" cx="31" cy="31" r="31"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 94)"><circle cx="57" cy="57" r="44" fill="#f9f9f9"/><g transform="rotate(-7.999 120.507 -22.508)"><use fill="#fff" xlink:href="#a"/><circle cx="44" cy="44" r="42" stroke="#eee" stroke-width="4"/><path fill="#fee1d3" fill-rule="nonzero" d="M34.394 55.736A4 4 0 0 1 36.706 55H56a6 6 0 0 0 6-6V35a6 6 0 0 0-6-6H34a6 6 0 0 0-6 6v25.26l6.394-4.529m2.312 3.264l-7.972 5.647A3.001 3.001 0 0 1 24 62.194v-27.2c0-5.523 4.477-10 10-10h22c5.523 0 10 4.477 10 10v14c0 5.523-4.477 10-10 10H36.706"/><path fill="#fc6d26" d="M38 40a2 2 0 1 1 .001 3.999A2 2 0 0 1 38 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 45 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 52 40"/></g></g><g transform="translate(48)"><circle cx="41" cy="41" r="31" fill="#f9f9f9"/><g transform="rotate(-7.999 84.554 -15.551)"><use fill="#fff" xlink:href="#b"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><rect width="20" height="4" x="21" y="29" fill="#6b4fbb" rx="2"/></g></g><path fill="#f9f9f9" d="M235.58 229H102c-6.627 0-12-5.373-12-12V65c0-6.627 5.373-12 12-12h206c6.627 0 12 5.373 12 12v18.399A34.834 34.834 0 0 1 337 79c19.33 0 35 15.67 35 35s-15.67 35-35 35a34.831 34.831 0 0 1-17-4.399v72.4c0 6.627-5.373 12-12 12h-11.58c.381 1.941.58 3.947.58 6 0 17.12-13.879 31-31 31-17.12 0-31-13.879-31-31 0-2.053.2-4.059.58-6"/><g transform="translate(87 50)"><g transform="rotate(7.999 -44.933 1563.894)"><use fill="#fff" xlink:href="#c"/><circle cx="35" cy="35" r="33" stroke="#eee" stroke-width="4"/><g transform="translate(20 19)"><circle cx="15" cy="16" r="15" fill="#f4f1fa" stroke="#6b4fbb" stroke-width="3"/><g fill="#6b4fbb"><path d="M19.419 6.996h-.007L16.959 4l-2.454 2.997H14.5L12.046 4 9.591 6.998h-.003L7.133 4 4.677 6.999H2.001c2.605-4.204 7.231-7 12.502-7 5.269 0 9.892 2.793 12.498 6.994h-2.676l-2.452-2.994-2.453 2.996"/><circle cx="9.5" cy="17.5" r="1.5"/><circle cx="20.5" cy="17.5" r="1.5"/></g></g></g><use xlink:href="#d"/><rect width="226" height="172" x="2" y="2" stroke="#eee" stroke-width="4" rx="10"/><rect width="4" height="122" x="33" y="42" fill="#eee" rx="2"/><g transform="translate(13 59)"><rect width="10" height="4" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="12" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="24" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="36" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="10" height="4" y="60" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="72" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="84" fill="#fee1d3" rx="2"/></g><g transform="translate(55 59)"><use fill="#6b4fbb" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fef0e9" rx="2"/><use fill="#fee1d3" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#e1dbf1" rx="2"/><rect width="21" height="4" y="36" fill="#eee" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/><g transform="translate(98)"><use fill="#fee1d3" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fc6d26" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#fef0e9" rx="2" id="k"/><use fill="#e1dbf1" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#f0edf8" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fc6d26" rx="2"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fee1d3" rx="2"/><rect width="21" height="4" y="36" fill="#fef0e9" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/></g><g transform="translate(0 60)"><use fill="#f0edf8" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#6b4fbb" rx="2"/><use fill="#e1dbf1" xlink:href="#f"/><use xlink:href="#k"/><use fill="#fee1d3" xlink:href="#g"/><use fill="#eee" xlink:href="#h"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fef0e9" rx="2"/><use fill="#fc6d26" xlink:href="#j"/></g><rect width="4" height="63" x="74" y="13" fill="#eee" rx="2"/></g><rect width="230" height="4" y="27" fill="#eee" rx="2"/></g><g transform="rotate(7.999 -1289.786 1797.583)"><use fill="#fff" xlink:href="#l"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><path fill="#fc6d26" d="M29 29h-6a2 2 0 1 0 0 4h6v6a2 2 0 1 0 4 0v-6h6a2 2 0 1 0 0-4h-6v-6a2 2 0 1 0-4 0v6"/></g></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="374" height="268" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="44" cy="44" r="44"/><circle id="b" cx="31" cy="31" r="31"/><circle id="c" cx="35" cy="35" r="35"/><rect id="d" width="230" height="176" rx="10"/><circle id="e" cx="31" cy="31" r="31"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(4 98)"><circle cx="53" cy="53" r="44" fill="#F9F9F9"/><g transform="translate(6 6)"><use fill="#FFF" xlink:href="#a"/><circle cx="44" cy="44" r="42" stroke="#EEE" stroke-width="4"/><path fill="#FEE1D3" fill-rule="nonzero" d="M34.394 55.736A4 4 0 0 1 36.706 55H56a6 6 0 0 0 6-6V35a6 6 0 0 0-6-6H34a6 6 0 0 0-6 6v25.265l6.394-4.53zM36.706 59l-7.972 5.647A3 3 0 0 1 24 62.199V35c0-5.523 4.477-10 10-10h22c5.523 0 10 4.477 10 10v14c0 5.523-4.477 10-10 10H36.706z"/><path fill="#FC6D26" d="M38 40a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm7 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm7 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></g></g><g transform="translate(50 2)"><circle cx="39" cy="39" r="31" fill="#F9F9F9"/><g transform="translate(5 5)"><use fill="#FFF" xlink:href="#b"/><circle cx="31" cy="31" r="29" stroke="#EEE" stroke-width="4"/><rect width="20" height="4" x="21" y="29" fill="#6B4FBB" rx="2"/></g></g><path fill="#F9F9F9" d="M235.58 229H102c-6.627 0-12-5.373-12-12V65c0-6.627 5.373-12 12-12h206c6.627 0 12 5.373 12 12v18.399A34.842 34.842 0 0 1 337 79c19.33 0 35 15.67 35 35s-15.67 35-35 35a34.842 34.842 0 0 1-17-4.399V217c0 6.627-5.373 12-12 12h-11.58c.38 1.941.58 3.947.58 6 0 17.12-13.88 31-31 31s-31-13.88-31-31c0-2.053.2-4.059.58-6z"/><g transform="translate(87 50)"><g transform="translate(212 26)"><use fill="#FFF" xlink:href="#c"/><circle cx="35" cy="35" r="33" stroke="#EEE" stroke-width="4"/><g transform="translate(20 19)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M19.419 6.996h-.007L16.959 4l-2.454 2.997h-.006L12.045 4 9.59 6.998h-.003L7.132 4 4.676 7H2c2.605-4.204 7.23-7 12.502-7C19.771 0 24.394 2.793 27 6.994h-2.676L21.872 4l-2.453 2.996z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g></g><use fill="#FFF" xlink:href="#d"/><rect width="226" height="172" x="2" y="2" stroke="#EEE" stroke-width="4" rx="10"/><rect width="4" height="122" x="33" y="42" fill="#EEE" rx="2"/><g transform="translate(13 59)"><rect width="10" height="4" fill="#FEE1D3" rx="2"/><rect width="10" height="4" y="12" fill="#F0EDF8" rx="2"/><rect width="10" height="4" y="24" fill="#FEF0E9" rx="2"/><rect width="10" height="4" y="36" fill="#FEE1D3" rx="2"/><rect width="10" height="4" y="48" fill="#E1DBF1" rx="2"/><rect width="10" height="4" y="60" fill="#F0EDF8" rx="2"/><rect width="10" height="4" y="72" fill="#FEF0E9" rx="2"/><rect width="10" height="4" y="84" fill="#FEE1D3" rx="2"/></g><g transform="translate(55 59)"><rect width="14" height="4" fill="#6B4FBB" rx="2"/><rect width="14" height="4" x="20" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" fill="#FEF0E9" rx="2"/><rect width="14" height="4" y="12" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" y="24" fill="#FEF0E9" rx="2"/><rect width="14" height="4" y="48" fill="#E1DBF1" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#FEF0E9" rx="2"/><rect width="7" height="4" x="20" y="12" fill="#FEE1D3" rx="2"/><rect width="7" height="4" x="27" y="36" fill="#6B4FBB" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#FEE1D3" rx="2"/><rect width="7" height="4" y="24" fill="#FC6D26" rx="2"/><rect width="21" height="4" x="13" y="24" fill="#E1DBF1" rx="2"/><rect width="21" height="4" y="36" fill="#EEE" rx="2"/><rect width="7" height="4" x="33" y="12" fill="#6B4FBB" rx="2"/><g transform="translate(98)"><rect width="14" height="4" fill="#FEE1D3" rx="2"/><rect width="14" height="4" x="20" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" fill="#FC6D26" rx="2"/><rect width="14" height="4" y="12" fill="#FEF0E9" rx="2"/><rect width="14" height="4" x="40" y="24" fill="#E1DBF1" rx="2"/><rect width="14" height="4" y="48" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#FEE1D3" rx="2"/><rect width="7" height="4" x="20" y="12" fill="#FC6D26" rx="2"/><rect width="7" height="4" x="27" y="36" fill="#6B4FBB" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#FC6D26" rx="2"/><rect width="7" height="4" y="24" fill="#6B4FBB" rx="2"/><rect width="21" height="4" x="13" y="24" fill="#FEE1D3" rx="2"/><rect width="21" height="4" y="36" fill="#FEF0E9" rx="2"/><rect width="7" height="4" x="33" y="12" fill="#6B4FBB" rx="2"/></g><g transform="translate(0 60)"><rect width="14" height="4" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="20" fill="#6B4FBB" rx="2"/><rect width="14" height="4" x="40" fill="#E1DBF1" rx="2"/><rect width="14" height="4" y="12" fill="#FEF0E9" rx="2"/><rect width="14" height="4" x="40" y="24" fill="#FEE1D3" rx="2"/><rect width="7" height="4" x="20" y="12" fill="#EEE" rx="2"/><rect width="7" height="4" y="24" fill="#6B4FBB" rx="2"/><rect width="21" height="4" x="13" y="24" fill="#FEF0E9" rx="2"/><rect width="7" height="4" x="33" y="12" fill="#FC6D26" rx="2"/></g><rect width="4" height="63" x="74" y="13" fill="#EEE" rx="2"/></g><rect width="230" height="4" y="27" fill="#EEE" rx="2"/></g><g transform="translate(233 201)"><use fill="#FFF" xlink:href="#e"/><circle cx="31" cy="31" r="29" stroke="#EEE" stroke-width="4"/><path fill="#FC6D26" d="M29 29v-6a2 2 0 1 1 4 0v6h6a2 2 0 1 1 0 4h-6v6a2 2 0 1 1-4 0v-6h-6a2 2 0 1 1 0-4h6z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/multi_file_editor_empty.svg b/app/assets/images/illustrations/multi_file_editor_empty.svg
new file mode 100644
index 00000000000..bd376f0a050
--- /dev/null
+++ b/app/assets/images/illustrations/multi_file_editor_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300"><g fill="none" fill-rule="evenodd" transform="translate(35 29)"><path fill="#EEE" fill-rule="nonzero" d="M90 23a2 2 0 1 1 0-4h10a2 2 0 0 1 0 4H90zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h1a11.98 11.98 0 0 1 9.457 4.612 2 2 0 0 1-3.151 2.464A7.981 7.981 0 0 0 331 23h-1zm9 11.39a2 2 0 0 1 4 0v10a2 2 0 0 1-4 0v-10zm0 180a2 2 0 1 1 4 0V223c0 .56-.038 1.114-.114 1.662a2 2 0 0 1-3.962-.55A8.21 8.21 0 0 0 339 223v-8.61zm-4.769 15.931a2 2 0 0 1 1.618 3.658A11.967 11.967 0 0 1 331 235h-5.782a2 2 0 0 1 0-4H331c1.13 0 2.224-.233 3.231-.679zm-19.013.679a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zM115 231a2 2 0 0 1 0 4h-10a2 2 0 0 1 0-4h10zm-26.2 4c.131-.646.2-1.315.2-2v-2h4a2 2 0 0 1 0 4h-4.2z"/><path fill="#EEE" fill-rule="nonzero" d="M103 211h258a6 6 0 0 0 6-6V63a6 6 0 0 0-6-6H166a5 5 0 0 1-5-5v-8.5a5.5 5.5 0 0 0-5.5-5.5H109a6 6 0 0 0-6 6v167zm62-167.5V52a1 1 0 0 0 1 1h195c5.523 0 10 4.477 10 10v142c0 5.523-4.477 10-10 10H99V44c0-5.523 4.477-10 10-10h46.5a9.5 9.5 0 0 1 9.5 9.5z"/><rect width="40" height="4" x="118" y="78" fill="#6B4FBB" rx="2"/><rect width="30" height="4" x="118" y="90" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="153" y="90" fill="#E1DBF1" rx="2"/><rect width="150" height="4" x="118" y="102" fill="#EFEDF8" rx="2"/><rect width="90" height="4" x="118" y="114" fill="#E1DBF1" rx="2"/><rect width="60" height="4" x="118" y="138" fill="#EFEDF8" rx="2"/><rect width="20" height="4" x="118" y="150" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="144" y="150" fill="#C3B8E3" rx="2"/><rect width="20" height="4" x="170" y="150" fill="#E1DBF1" rx="2"/><rect width="130" height="4" x="118" y="162" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="118" y="174" fill="#C3B8E3" rx="2"/><rect width="30" height="4" x="154" y="174" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="190" y="174" fill="#EFEDF8" rx="2"/><rect width="40" height="4" x="118" y="186" fill="#E1DBF1" rx="2"/><path fill="#F9F9F9" d="M89 24.292l11.434 19.326v170.326L89 226.336V24.292z"/><path fill="#EEE" fill-rule="nonzero" d="M89 229.286v-5.9l9.434-10.223V44.165L89 28.22v-7.856l13.434 22.707v171.655L89 229.286zM10 4a6 6 0 0 0-6 6v223a6 6 0 0 0 6 6h69a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h69c5.523 0 10 4.477 10 10v223c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><circle cx="25" cy="23" r="11" fill="#FEF0E8"/><path fill="#FEE1D3" d="M46 17h16a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4zm0 8h27a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4z"/><path fill="#EEE" d="M16 50h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-4 12h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4zM26 78h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4z"/><g transform="translate(14 110)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><path fill="#EEE" d="M16 140h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4z"/><g transform="translate(24 124)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><g fill="#FC6D26" transform="translate(24 92)"><rect width="8" height="8" rx="2"/><rect width="28" height="4" x="14" y="2" rx="2"/></g><path fill="#FDC4A8" fill-rule="nonzero" d="M152 50.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zm0-3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/service_desk_callout.svg b/app/assets/images/illustrations/service_desk_callout.svg
new file mode 100644
index 00000000000..2886388279e
--- /dev/null
+++ b/app/assets/images/illustrations/service_desk_callout.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><rect width="7" height="1" x="59" y="38" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M60.5 42a3.5 3.5 0 0 0 0-7v7z"/><rect width="7" height="1" x="12" y="38" fill="#E1DBF2" transform="matrix(-1 0 0 1 31 0)" rx=".5"/><path fill="#6B4FBB" d="M17.5 42a3.5 3.5 0 0 1 0-7v7z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M39 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M35 56a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M26.5 40c0 4.143 3.355 7.5 7.494 7.5h10.012A7.497 7.497 0 0 0 51.5 40c0-4.143-3.355-7.5-7.494-7.5H33.994A7.497 7.497 0 0 0 26.5 40zm-3 0c0-5.799 4.698-10.5 10.494-10.5h10.012C49.802 29.5 54.5 34.2 54.5 40c0 5.799-4.698 10.5-10.494 10.5H33.994C28.198 50.5 23.5 45.8 23.5 40z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M35.255 42.406a1 1 0 1 1 1.872-.703 2.001 2.001 0 0 0 3.76-.038 1 1 0 1 1 1.886.665 4 4 0 0 1-7.518.076zM31.5 40a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm15 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/><path fill="#6B4FBB" d="M38 22h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zm0 3h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z" style="mix-blend-mode:multiply"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/service_desk_empty.svg b/app/assets/images/illustrations/service_desk_empty.svg
new file mode 100644
index 00000000000..daaaeae6a17
--- /dev/null
+++ b/app/assets/images/illustrations/service_desk_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="226" height="178" viewBox="0 0 226 178"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M109.496 165.895a78.17 78.17 0 0 0 6.158.08 2 2 0 0 0-.11-4c-1.94.053-3.886.028-5.84-.074a2 2 0 0 0-2.1 1.893 1.996 1.996 0 0 0 1.89 2.102zm18.408-1.245a76 76 0 0 0 6-1.4 2 2 0 1 0-1.064-3.856c-1.875.52-3.772.96-5.686 1.327a2.001 2.001 0 0 0 .75 3.93zm17.572-5.636a76.28 76.28 0 0 0 5.486-2.803 2 2 0 1 0-1.962-3.485 72.42 72.42 0 0 1-5.2 2.656 2.003 2.003 0 0 0 1.676 3.635zm44.342-74.897a75.786 75.786 0 0 0-.674-6.127 2.002 2.002 0 0 0-3.956.598c.29 1.92.505 3.857.64 5.805a1.998 1.998 0 0 0 2.133 1.857 2 2 0 0 0 1.858-2.133zm-3.505-18.144a76.141 76.141 0 0 0-2.13-5.78 2.001 2.001 0 0 0-3.695 1.534 72.381 72.381 0 0 1 2.02 5.476 1.999 1.999 0 1 0 3.805-1.229zm-7.754-16.73a77.053 77.053 0 0 0-3.454-5.1 1.998 1.998 0 0 0-2.797-.423 1.998 1.998 0 0 0-.424 2.796 73.06 73.06 0 0 1 3.273 4.835c.58.94 1.814 1.23 2.753.647a2.001 2.001 0 0 0 .646-2.754zm-11.582-14.446a76.37 76.37 0 0 0-4.572-4.128 1.999 1.999 0 1 0-2.559 3.073 72.633 72.633 0 0 1 4.334 3.913 2.001 2.001 0 1 0 2.798-2.86zm-101.422-4.91a77.634 77.634 0 0 0-4.64 4.05 2.001 2.001 0 0 0 2.749 2.906 72.611 72.611 0 0 1 4.4-3.84 2 2 0 1 0-2.509-3.115zM52.7 43.062a75.962 75.962 0 0 0-3.546 5.04 2 2 0 1 0 3.363 2.168 72.314 72.314 0 0 1 3.36-4.777 2 2 0 0 0-3.177-2.432zm-9.373 15.924c-.82 1.882-1.56 3.8-2.226 5.745a2 2 0 1 0 3.787 1.294 72.253 72.253 0 0 1 2.108-5.443 1.998 1.998 0 0 0-1.036-2.63 2.001 2.001 0 0 0-2.633 1.036zm-5.26 17.74a76.33 76.33 0 0 0-.777 6.11 2 2 0 0 0 3.985.347c.17-1.947.415-3.88.737-5.793a2 2 0 0 0-3.945-.664zM74.87 155.55a76.028 76.028 0 0 0 5.437 2.897 2 2 0 1 0 1.737-3.603 71.34 71.34 0 0 1-5.152-2.745 1.998 1.998 0 0 0-2.737.714 2.002 2.002 0 0 0 .715 2.738zm16.97 7.34a76.606 76.606 0 0 0 5.975 1.498 2 2 0 1 0 .816-3.916 72.52 72.52 0 0 1-5.662-1.42 1.999 1.999 0 1 0-1.129 3.837z"/><path fill="#F9F9F9" d="M2.12 130c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M39 166c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 92 39 92 4 107.67 4 127s15.67 35 35 35z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M53.925 116.226A1.995 1.995 0 0 0 53 116H25a1.99 1.99 0 0 0-.898.212l14.663 13.406c.39.357.99.348 1.37-.02l13.79-13.372zm1.075 4.53L42.92 132.47a5 5 0 0 1-6.854.1L23 120.624V138a2 2 0 0 0 2 2h28a2 2 0 0 0 2-2v-17.244zM25 112h28a6 6 0 0 1 6 6v20a6 6 0 0 1-6 6H25a6 6 0 0 1-6-6v-20a6 6 0 0 1 6-6z"/><path fill="#F9F9F9" d="M150.12 131c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M187 167c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M180.51 137H199a2 2 0 0 0 2-2v-16a2 2 0 0 0-2-2h-24a2 2 0 0 0-2 2v22.743l7.51-4.743zm1.157 4l-9.6 6.062a2 2 0 0 1-3.067-1.69V119a6 6 0 0 1 6-6h24a6 6 0 0 1 6 6v16a6 6 0 0 1-6 6h-17.333z"/><path fill="#6B4FBB" d="M180 129a2 2 0 1 1-.001-3.999A2 2 0 0 1 180 129zm7 0a2 2 0 1 1-.001-3.999A2 2 0 0 1 187 129zm7 0a2 2 0 1 1-.001-3.999A2 2 0 0 1 194 129z"/><g><path fill="#F9F9F9" d="M76.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M113 78c-21.54 0-39-17.46-39-39S91.46 0 113 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S132.33 4 113 4 78 19.67 78 39s15.67 35 35 35z"/><g transform="translate(133 35)"><rect width="7" height="1" y="3" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M1.5 7a3.5 3.5 0 1 0 0-7v7z"/></g><g transform="matrix(-1 0 0 1 93 35)"><rect width="7" height="1" y="3" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M1.5 7a3.5 3.5 0 1 0 0-7v7z"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M113 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M109 56a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M97.5 40c0-5.8 4.698-10.5 10.494-10.5h10.012c5.796 0 10.494 4.7 10.494 10.5s-4.698 10.5-10.494 10.5h-10.012C102.198 50.5 97.5 45.8 97.5 40zm3 0c0 4.143 3.355 7.5 7.494 7.5h10.012A7.496 7.496 0 0 0 125.5 40c0-4.143-3.355-7.5-7.494-7.5h-10.012A7.496 7.496 0 0 0 100.5 40z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M109.255 42.406a.998.998 0 0 1 .584-1.287.997.997 0 0 1 1.287.583 2 2 0 0 0 3.76-.038 1 1 0 0 1 1.886.665 4.001 4.001 0 0 1-7.518.076zM105.5 40a1.5 1.5 0 1 1 .001-3.001A1.5 1.5 0 0 1 105.5 40zm15 0a1.5 1.5 0 1 1 .001-3.001A1.5 1.5 0 0 1 120.5 40z"/><path fill="#6B4FBB" d="M112 22h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zm0 3h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z" style="mix-blend-mode:multiply"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/wiki_login_empty.svg b/app/assets/images/illustrations/wiki_login_empty.svg
new file mode 100644
index 00000000000..1cfa47220a5
--- /dev/null
+++ b/app/assets/images/illustrations/wiki_login_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="386" height="298" viewBox="0 0 386 298" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M4 51h16v15.997A5.003 5.003 0 0 1 15.003 72H8.997A5.005 5.005 0 0 1 4 66.997V51z"/><rect id="b" width="24" height="10" y="44" rx="3"/></defs><g fill="none" fill-rule="evenodd" transform="translate(0 3)"><g transform="rotate(15 23.151 968.24)"><rect width="53" height="44" fill="#FFF" stroke="#FDE5D8" stroke-width="3" stroke-linecap="round" rx="5"/><path fill="#FDE5D8" d="M29.5 28.3l2.758-3.861c.962-1.347 2.527-1.34 3.484 0l6.516 9.122c.962 1.347.399 2.439-1.252 2.439H17.994c-1.653 0-2.21-1.099-1.252-2.439l6.516-9.122c.962-1.347 2.527-1.34 3.484 0L29.5 28.3z" opacity=".6"/><circle cx="16" cy="16" r="6" fill="#FDB997"/></g><g transform="scale(-1 1) rotate(25 -75.08 -334.15)"><rect width="3" height="11" x="12.45" y="23.45" fill="#6B4FBB" transform="rotate(45 13.95 28.95)" rx="1.5"/><rect width="3" height="14" x="9.45" y="15.45" fill="#6B4FBB" transform="rotate(45 10.95 22.45)" rx="1.5"/><path fill="#FFF" stroke="#E1DCF1" stroke-width="3" d="M16 39.6C6.871 37.747 0 29.676 0 20 0 8.954 8.954 0 20 0s20 8.954 20 20c0 8.955-5.886 16.536-14 19.084v15.91A5.007 5.007 0 0 1 21 60c-2.761 0-5-2.244-5-5.006V39.6zm4-7.6c6.627 0 12-5.373 12-12S26.627 8 20 8 8 13.373 8 20s5.373 12 12 12z"/></g><g transform="scale(1 -1) rotate(-15 -383.616 -172.407)"><path stroke="#FDE5D8" stroke-width="3" d="M1.5 38.5h9V4c0-1.378-1.12-2.5-2.496-2.5H3.996A2.503 2.503 0 0 0 1.5 4v34.5z"/><rect width="2" height="27" x="5" y="7" fill="#FDA77D" opacity=".8" rx="1"/><path stroke="#FDE5D8" stroke-width="3" d="M2.427 41.553h7.146L6 48.699l-3.573-7.146z"/></g><g transform="rotate(-30 420.145 -545.422)"><path fill="#FFF" stroke="#FDE5D8" stroke-width="3" d="M9 3c0-1.657 1.347-3 3-3 1.657 0 3 1.352 3 3v43H9V3z"/><use fill="#FFF" xlink:href="#a"/><path stroke="#FDE5D8" stroke-width="3" d="M5.5 52.5v14.497A3.505 3.505 0 0 0 8.997 70.5h6.006a3.503 3.503 0 0 0 3.497-3.503V52.5h-13z"/><rect width="2" height="14" x="9" y="51" fill="#FDA77D" rx="1"/><rect width="2" height="14" x="13" y="51" fill="#FDA77D" rx="1"/><use fill="#FFF" xlink:href="#b"/><rect width="21" height="7" x="1.5" y="45.5" stroke="#FDE5D8" stroke-width="3" rx="3"/></g><g transform="translate(72 97.488)"><rect width="125" height="160" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><rect width="125" height="160" x="125" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><path fill="#FFF" stroke="#E5E5E5" stroke-width="4" d="M7 12.008C7 8.69 9.686 6 12.993 6H125v148H12.993C9.683 154 7 151.305 7 147.992V12.008zm236 0C243 8.69 240.314 6 237.007 6H125v148h112.007c3.31 0 5.993-2.695 5.993-6.008V12.008z" stroke-linecap="round"/><rect width="84" height="42" x="142" y="29" stroke="#EEE" stroke-width="4" rx="3"/><rect width="88" height="4" x="141" y="93" fill="#E5E5E5" rx="2"/><rect width="88" height="4" x="141" y="107" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="141" y="121" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="93" fill="#E5E5E5" rx="2"/><rect width="26" height="4" x="22" y="27" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="41" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="55" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="69" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="107" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="121" fill="#BFBFBF" rx="2"/></g><path stroke="#B5A7DD" stroke-width="2.5" d="M23.139 182.922l-1.347-.6a2.004 2.004 0 0 1-1.02-2.64l.815-1.831a1.995 1.995 0 0 1 2.645-1.01l1.308.583a9.959 9.959 0 0 1 2.177-1.876l-.376-1.402a2.004 2.004 0 0 1 1.41-2.455l1.937-.519a1.995 1.995 0 0 1 2.449 1.421l.375 1.402a9.959 9.959 0 0 1 2.824.536l.84-1.158a2.004 2.004 0 0 1 2.796-.448l1.622 1.178a1.995 1.995 0 0 1 .437 2.797l-.867 1.193a9.946 9.946 0 0 1 1.341 2.541l1.461-.05a2.004 2.004 0 0 1 2.075 1.926l.07 2.003a1.995 1.995 0 0 1-1.935 2.067l-1.445.05c-.256.93-.644 1.817-1.15 2.632l.944 1.087a2.004 2.004 0 0 1-.191 2.825l-1.513 1.315a1.995 1.995 0 0 1-2.824-.204l-.963-1.108a10.084 10.084 0 0 1-2.776.744l-.28 1.441a2.004 2.004 0 0 1-2.344 1.588l-1.967-.382a1.995 1.995 0 0 1-1.579-2.35l.275-1.414a10.044 10.044 0 0 1-2.312-1.704l-1.277.678a2.004 2.004 0 0 1-2.709-.822l-.94-1.77a1.995 1.995 0 0 1 .833-2.705l1.29-.687a9.946 9.946 0 0 1-.11-2.872zm10.98 4.93a4 4 0 1 0-2.07-7.727 4 4 0 0 0 2.07 7.728z"/><ellipse cx="197" cy="289.988" fill="#F9F9F9" rx="125" ry="4.5"/><path fill="#6B4FBB" d="M164 100.492a3.002 3.002 0 0 1 3.001-3.004H183a3.006 3.006 0 0 1 3.001 3.004v34.988c0 2.213-1.45 2.954-3.24 1.651l-7.76-5.643-7.76 5.643c-1.789 1.302-3.24.566-3.24-1.651v-34.988z"/><g opacity=".2"><path fill="#FC8A51" d="M5.747 234.768l-2.688 1.114c-1.017.422-1.803-.134-1.754-1.228l.128-2.907-1.115-2.688c-.422-1.017.135-1.803 1.229-1.754l2.907.128 2.687-1.115c1.018-.422 1.803.135 1.755 1.229l-.128 2.907 1.114 2.687c.422 1.018-.134 1.803-1.228 1.755l-2.907-.128zM191.564 37.953l-3.72.164c-1.326.059-1.992-.88-1.48-2.115l1.426-3.438-.164-3.72c-.059-1.326.88-1.992 2.115-1.48l3.438 1.426 3.72-.164c1.326-.059 1.992.88 1.48 2.114l-1.426 3.44.164 3.719c.059 1.326-.88 1.992-2.114 1.48l-3.44-1.426z"/><path fill="#6B4FBB" d="M348.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775zm9.261 164.735l-2.907-.125c-1.1-.048-1.577-.884-1.07-1.855l1.344-2.58.126-2.908c.047-1.1.883-1.577 1.855-1.07l2.58 1.344 2.907.126c1.1.047 1.577.883 1.07 1.855l-1.344 2.58-.125 2.907c-.048 1.1-.884 1.577-1.856 1.07l-2.58-1.344zM88.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/wiki_logout_empty.svg b/app/assets/images/illustrations/wiki_logout_empty.svg
new file mode 100644
index 00000000000..c71841f72e5
--- /dev/null
+++ b/app/assets/images/illustrations/wiki_logout_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/multi-editor-off.png b/app/assets/images/multi-editor-off.png
new file mode 100644
index 00000000000..82a6127f853
--- /dev/null
+++ b/app/assets/images/multi-editor-off.png
Binary files differ
diff --git a/app/assets/images/multi-editor-on.png b/app/assets/images/multi-editor-on.png
new file mode 100644
index 00000000000..2bcd29abf13
--- /dev/null
+++ b/app/assets/images/multi-editor-on.png
Binary files differ
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 5d060165f4b..6a0662ba903 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,9 +1,10 @@
/* eslint-disable no-param-reassign, class-methods-use-this */
-/* global Pager */
import Cookies from 'js-cookie';
+import Pager from './pager';
+import { localTimeAgo } from './lib/utils/datetime_utility';
-class Activities {
+export default class Activities {
constructor() {
Pager.init(20, true, false, data => data, this.updateTooltips);
@@ -15,7 +16,7 @@ class Activities {
}
updateTooltips() {
- gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
+ localTimeAgo($('.js-timeago', '.content_list'));
}
reloadActivities() {
@@ -33,6 +34,3 @@ class Activities {
$sender.closest('li').toggleClass('active');
}
}
-
-window.gl = window.gl || {};
-window.gl.Activities = Activities;
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 34669dd13d6..c1f7fa2aced 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,62 +1,59 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
-
-window.Admin = (function() {
- function Admin() {
- var modal, showBlacklistType;
- $('input#user_force_random_password').on('change', function(elem) {
- var elems;
- elems = $('#user_password, #user_password_confirmation');
- if ($(this).attr('checked')) {
- return elems.val('').attr('disabled', true);
- } else {
- return elems.removeAttr('disabled');
- }
- });
- $('body').on('click', '.js-toggle-colors-link', function(e) {
- e.preventDefault();
- return $('.js-toggle-colors-container').toggle();
- });
- $('.log-tabs a').click(function(e) {
- e.preventDefault();
- return $(this).tab('show');
- });
- $('.log-bottom').click(function(e) {
- var visible_log;
- e.preventDefault();
- visible_log = $(".file-content:visible");
- return visible_log.animate({
- scrollTop: visible_log.find('ol').height()
- }, "fast");
- });
- modal = $('.change-owner-holder');
- $('.change-owner-link').bind("click", function(e) {
- e.preventDefault();
- $(this).hide();
- return modal.show();
- });
- $('.change-owner-cancel-link').bind("click", function(e) {
- e.preventDefault();
- modal.hide();
- return $('.change-owner-link').show();
- });
- $('li.project_member').bind('ajax:success', function() {
- return gl.utils.refreshCurrentPage();
- });
- $('li.group_member').bind('ajax:success', function() {
- return gl.utils.refreshCurrentPage();
- });
- showBlacklistType = function() {
- if ($("input[name='blacklist_type']:checked").val() === 'file') {
- $('.blacklist-file').show();
- return $('.blacklist-raw').hide();
- } else {
- $('.blacklist-file').hide();
- return $('.blacklist-raw').show();
- }
- };
- $("input[name='blacklist_type']").click(showBlacklistType);
- showBlacklistType();
+import { refreshCurrentPage } from './lib/utils/url_utility';
+
+function showBlacklistType() {
+ if ($('input[name="blacklist_type"]:checked').val() === 'file') {
+ $('.blacklist-file').show();
+ $('.blacklist-raw').hide();
+ } else {
+ $('.blacklist-file').hide();
+ $('.blacklist-raw').show();
}
+}
+
+export default function adminInit() {
+ const modal = $('.change-owner-holder');
+
+ $('input#user_force_random_password').on('change', function randomPasswordClick() {
+ const $elems = $('#user_password, #user_password_confirmation');
+ if ($(this).attr('checked')) {
+ $elems.val('').attr('disabled', true);
+ } else {
+ $elems.removeAttr('disabled');
+ }
+ });
+
+ $('body').on('click', '.js-toggle-colors-link', (e) => {
+ e.preventDefault();
+ $('.js-toggle-colors-container').toggle();
+ });
+
+ $('.log-tabs a').on('click', function logTabsClick(e) {
+ e.preventDefault();
+ $(this).tab('show');
+ });
+
+ $('.log-bottom').on('click', (e) => {
+ e.preventDefault();
+ const $visibleLog = $('.file-content:visible');
+ $visibleLog.animate({
+ scrollTop: $visibleLog.find('ol').height(),
+ }, 'fast');
+ });
+
+ $('.change-owner-link').on('click', function changeOwnerLinkClick(e) {
+ e.preventDefault();
+ $(this).hide();
+ modal.show();
+ });
+
+ $('.change-owner-cancel-link').on('click', (e) => {
+ e.preventDefault();
+ modal.hide();
+ $('.change-owner-link').show();
+ });
+
+ $('li.project_member, li.group_member').on('ajax:success', refreshCurrentPage);
- return Admin;
-})();
+ $("input[name='blacklist_type']").on('click', showBlacklistType);
+ showBlacklistType();
+}
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d963101028a..21d8c790e90 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
@@ -6,6 +7,7 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
+ projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key',
@@ -76,6 +78,14 @@ const Api = {
.done(projects => callback(projects));
},
+ // Return single project
+ project(projectPath) {
+ const url = Api.buildUrl(Api.projectPath)
+ .replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url);
+ },
+
newLabel(namespacePath, projectPath, data, callback) {
let url;
@@ -115,7 +125,7 @@ const Api = {
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
- .replace(':id', id);
+ .replace(':id', encodeURIComponent(id));
return this.wrapAjaxCall({
url,
type: 'POST',
@@ -127,7 +137,7 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
- .replace(':id', id)
+ .replace(':id', encodeURIComponent(id))
.replace(':branch', branch);
return this.wrapAjaxCall({
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
deleted file mode 100644
index 88756884d16..00000000000
--- a/app/assets/javascripts/aside.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
-
-window.Aside = (function() {
- function Aside() {
- $(document).off("click", "a.show-aside");
- $(document).on("click", 'a.show-aside', function(e) {
- var btn, icon;
- e.preventDefault();
- btn = $(e.currentTarget);
- icon = btn.find('i');
- if (icon.hasClass('fa-angle-left')) {
- btn.parent().find('section').hide();
- btn.parent().find('aside').fadeIn();
- return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
- } else {
- btn.parent().find('aside').hide();
- btn.parent().find('section').fadeIn();
- return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
- }
- });
- }
-
- return Aside;
-})();
diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js
index e7dc4ef8304..c6eca72c51b 100644
--- a/app/assets/javascripts/behaviors/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/copy_as_gfm.js
@@ -74,6 +74,18 @@ const gfmRules = {
return `![${el.dataset.title}](${el.getAttribute('src')})`;
},
},
+ MermaidFilter: {
+ 'svg.mermaid'(el, text) {
+ const sourceEl = el.querySelector('text.source');
+ if (!sourceEl) return false;
+
+ return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``;
+ },
+ 'svg.mermaid style, svg.mermaid g'(el, text) {
+ // We don't want to include the content of these elements in the copied text.
+ return '';
+ },
+ },
MathFilter: {
'pre.code.math[data-math-style=display]'(el, text) {
return `\`\`\`math\n${text.trim()}\n\`\`\``;
diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js
new file mode 100644
index 00000000000..1cf0b960eb0
--- /dev/null
+++ b/app/assets/javascripts/behaviors/secret_values.js
@@ -0,0 +1,42 @@
+import { n__ } from '../locale';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+
+export default class SecretValues {
+ constructor(container) {
+ this.container = container;
+ }
+
+ init() {
+ this.values = this.container.querySelectorAll('.js-secret-value');
+ this.placeholders = this.container.querySelectorAll('.js-secret-value-placeholder');
+ this.revealButton = this.container.querySelector('.js-secret-value-reveal-button');
+
+ this.revealText = n__('Reveal value', 'Reveal values', this.values.length);
+ this.hideText = n__('Hide value', 'Hide values', this.values.length);
+
+ const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus);
+ this.updateDom(isRevealed);
+
+ this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this));
+ }
+
+ onRevealButtonClicked() {
+ const previousIsRevealed = convertPermissionToBoolean(
+ this.revealButton.dataset.secretRevealStatus,
+ );
+ this.updateDom(!previousIsRevealed);
+ }
+
+ updateDom(isRevealed) {
+ this.values.forEach((value) => {
+ value.classList.toggle('hide', !isRevealed);
+ });
+
+ this.placeholders.forEach((placeholder) => {
+ placeholder.classList.toggle('hide', isRevealed);
+ });
+
+ this.revealButton.textContent = isRevealed ? this.hideText : this.revealText;
+ this.revealButton.dataset.secretRevealStatus = isRevealed;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index b70b0a9bbf8..417ac31fc86 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -5,6 +5,7 @@
// %button.js-toggle-button
// %div.js-toggle-content
//
+import { getLocationHash } from '../lib/utils/url_utility';
$(() => {
function toggleContainer(container, toggleState) {
@@ -32,7 +33,7 @@ $(() => {
// If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container!
- const hash = window.gl.utils.getLocationHash();
+ const hash = getLocationHash();
const anchor = hash && document.getElementById(hash);
const container = anchor && $(anchor).closest('.js-toggle-container');
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 0d590a9dbc4..f7ae6f1cd12 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
import Dropzone from 'dropzone';
-import '../lib/utils/url_utility';
+import { visitUrl } from '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
@@ -49,7 +49,7 @@ export default class BlobFileDropzone {
});
this.on('success', function (header, response) {
$('#modal-upload-blob').modal('hide');
- window.gl.utils.visitUrl(response.filePath);
+ visitUrl(response.filePath);
});
this.on('maxfilesexceeded', function (file) {
dropzoneMessage.addClass(HIDDEN_CLASS);
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index c8f68860fbd..d36d9f0de2d 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -1,7 +1,9 @@
+import { getLocationHash } from '../lib/utils/url_utility';
+
const lineNumberRe = /^L[0-9]+/;
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
- const hash = gl.utils.getLocationHash();
+ const hash = getLocationHash();
if (hash && lineNumberRe.test(hash)) {
const hashUrlString = `#${hash}`;
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
index c858a6bb7b4..57b031956e8 100644
--- a/app/assets/javascripts/blob/notebook/index.js
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -1,10 +1,8 @@
/* eslint-disable no-new */
import Vue from 'vue';
-import VueResource from 'vue-resource';
+import axios from '../../lib/utils/axios_utils';
import notebookLab from '../../notebook/index.vue';
-Vue.use(VueResource);
-
export default () => {
const el = document.getElementById('js-notebook-viewer');
@@ -50,14 +48,14 @@ export default () => {
`,
methods: {
loadFile() {
- this.$http.get(el.dataset.endpoint)
- .then(response => response.json())
- .then((res) => {
- this.json = res;
+ axios.get(el.dataset.endpoint)
+ .then(res => res.data)
+ .then((data) => {
+ this.json = data;
this.loading = false;
})
.catch((e) => {
- if (e.status) {
+ if (e.status !== 200) {
this.loadError = true;
}
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 20d23162940..679c883cdcf 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -2,7 +2,6 @@
import _ from 'underscore';
import Vue from 'vue';
-import VueResource from 'vue-resource';
import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
@@ -25,8 +24,6 @@ import './components/new_list_dropdown';
import './components/modal/index';
import '../vue_shared/vue_resource_interceptor';
-Vue.use(VueResource);
-
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
@@ -95,14 +92,13 @@ $(() => {
Store.disabled = this.disabled;
gl.boardService.all()
- .then(response => response.json())
- .then((resp) => {
- resp.forEach((board) => {
+ .then(res => res.data)
+ .then((data) => {
+ data.forEach((board) => {
const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') {
list.position = Infinity;
- list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
}
@@ -113,7 +109,9 @@ $(() => {
Store.addBlankState();
this.loading = false;
})
- .catch(() => new Flash('An error occurred. Please try again.'));
+ .catch(() => {
+ Flash('An error occurred while fetching the board lists. Please try again.');
+ });
},
methods: {
updateTokens() {
@@ -124,7 +122,7 @@ $(() => {
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
- .then(res => res.json())
+ .then(res => res.data)
.then((data) => {
newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js
index edfe7c326db..72db626d3c7 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
@@ -65,7 +65,7 @@ export default {
// Save the labels
gl.boardService.generateDefaultLists()
- .then(resp => resp.json())
+ .then(res => res.data)
.then((data) => {
data.forEach((listObj) => {
const list = Store.findList('title', listObj.title);
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 29aeb8e84aa..84b76a6f1b1 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -115,7 +115,7 @@ export default {
},
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
- scroll: document.querySelectorAll('.boards-list')[0],
+ scroll: true,
group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index faa76da964f..983429550f0 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,9 +1,8 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
-/* global MilestoneSelect */
-/* global Sidebar */
import Vue from 'vue';
import Flash from '../../flash';
+import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import assignees from '../../sidebar/components/assignees/assignees';
@@ -12,6 +11,7 @@ import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
+import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index d2044f20ebe..d825ff38587 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -89,7 +89,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
page: this.page,
per: this.perPage,
}))
- .then(resp => resp.json())
+ .then(res => res.data)
.then((data) => {
if (clearIssues) {
this.issues = [];
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index df2809e1805..e210d69895e 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -40,7 +40,7 @@ class List {
save () {
return gl.boardService.createList(this.label.id)
- .then(resp => resp.json())
+ .then(res => res.data)
.then((data) => {
this.id = data.id;
this.type = data.list_type;
@@ -90,7 +90,7 @@ class List {
}
return gl.boardService.getIssuesForList(this.id, data)
- .then(resp => resp.json())
+ .then(res => res.data)
.then((data) => {
this.loading = false;
this.issuesSize = data.size;
@@ -108,7 +108,7 @@ class List {
this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
- .then(resp => resp.json())
+ .then(res => res.data)
.then((data) => {
issue.id = data.id;
issue.iid = data.iid;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index fa7ddd25e1f..d78d4701974 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -1,82 +1,79 @@
-/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
-
-import Vue from 'vue';
+import axios from '../../lib/utils/axios_utils';
+import { mergeUrlParams } from '../../lib/utils/url_utility';
export default class BoardService {
- constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
- this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
- issues: {
- method: 'GET',
- url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
- }
- });
- this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
- generate: {
- method: 'POST',
- url: `${listsEndpoint}/generate.json`
- }
- });
- this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
- this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
- bulkUpdate: {
- method: 'POST',
- url: bulkUpdatePath,
- },
- });
+ constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
+ this.boardsEndpoint = boardsEndpoint;
+ this.boardId = boardId;
+ this.listsEndpoint = listsEndpoint;
+ this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
+ this.bulkUpdatePath = bulkUpdatePath;
+ }
+
+ generateBoardsPath(id) {
+ return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`;
}
- all () {
- return this.lists.get();
+ generateIssuesPath(id) {
+ return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`;
}
- generateDefaultLists () {
- return this.lists.generate({});
+ static generateIssuePath(boardId, id) {
+ return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
}
- createList (label_id) {
- return this.lists.save({}, {
+ all() {
+ return axios.get(this.listsEndpoint);
+ }
+
+ generateDefaultLists() {
+ return axios.post(this.listsEndpointGenerate, {});
+ }
+
+ createList(labelId) {
+ return axios.post(this.listsEndpoint, {
list: {
- label_id
- }
+ label_id: labelId,
+ },
});
}
- updateList (id, position) {
- return this.lists.update({ id }, {
+ updateList(id, position) {
+ return axios.put(`${this.listsEndpoint}/${id}`, {
list: {
- position
- }
+ position,
+ },
});
}
- destroyList (id) {
- return this.lists.delete({ id });
+ destroyList(id) {
+ return axios.delete(`${this.listsEndpoint}/${id}`);
}
- getIssuesForList (id, filter = {}) {
+ getIssuesForList(id, filter = {}) {
const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
- return this.issues.get(data);
+ return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
}
- moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) {
- return this.issue.update({ id }, {
- from_list_id,
- to_list_id,
- move_before_id,
- move_after_id,
+ moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
+ return axios.put(BoardService.generateIssuePath(this.boardId, id), {
+ from_list_id: fromListId,
+ to_list_id: toListId,
+ move_before_id: moveBeforeId,
+ move_after_id: moveAfterId,
});
}
- newIssue (id, issue) {
- return this.issues.save({ id }, {
- issue
+ newIssue(id, issue) {
+ return axios.post(this.generateIssuesPath(id), {
+ issue,
});
}
getBacklog(data) {
- return this.boards.issues(data);
+ return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`));
}
bulkUpdate(issueIds, extraData = {}) {
@@ -86,15 +83,15 @@ export default class BoardService {
}),
};
- return this.issues.bulkUpdate(data);
+ return axios.post(this.bulkUpdatePath, data);
}
static getIssueInfo(endpoint) {
- return Vue.http.get(endpoint);
+ return axios.get(endpoint);
}
static toggleIssueSubscription(endpoint) {
- return Vue.http.post(endpoint);
+ return axios.post(endpoint);
}
}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 2cfd6179a25..637d0dbde23 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -30,6 +30,7 @@ export default class Clusters {
installHelmPath,
installIngressPath,
installRunnerPath,
+ installPrometheusPath,
clusterStatus,
clusterStatusReason,
helpPath,
@@ -44,6 +45,7 @@ export default class Clusters {
installHelmEndpoint: installHelmPath,
installIngressEndpoint: installIngressPath,
installRunnerEndpoint: installRunnerPath,
+ installPrometheusEndpoint: installPrometheusPath,
});
this.toggle = this.toggle.bind(this);
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index e5ae439d26e..cd58b88db69 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -67,6 +67,16 @@ export default {
and send the results back to GitLab.`,
));
},
+ prometheusDescription() {
+ return sprintf(
+ _.escape(s__('ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.')), {
+ gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html", target="_blank" rel="noopener noreferrer">
+ ${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
+ </a>`,
+ },
+ false,
+ );
+ },
},
};
</script>
@@ -106,6 +116,16 @@ export default {
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
/>
+ <application-row
+ id="prometheus"
+ :title="applications.prometheus.title"
+ title-link="https://prometheus.io/docs/introduction/overview/"
+ :description="prometheusDescription"
+ :status="applications.prometheus.status"
+ :status-reason="applications.prometheus.statusReason"
+ :request-status="applications.prometheus.requestStatus"
+ :request-reason="applications.prometheus.requestReason"
+ />
<!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
<!-- Add GitLab Runner row, all other plumbing is complete -->
</div>
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index 755c2981c2e..13468578f4f 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -7,6 +7,7 @@ export default class ClusterService {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
+ prometheus: this.options.installPrometheusEndpoint,
};
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index e731cdc3042..bd4a1fb37f9 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -28,6 +28,13 @@ export default class ClusterStore {
requestStatus: null,
requestReason: null,
},
+ prometheus: {
+ title: s__('ClusterIntegration|Prometheus'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
},
};
}
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 50d0cb5c86d..b6a0ece7907 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -121,7 +121,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 = ref[0], maxHeight = ref[1];
$swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
@@ -158,7 +158,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 = ref[0], maxHeight = ref[1];
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
@@ -176,6 +176,7 @@ export default class ImageFile {
left: dragTrackWidth
});
+ $frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
_this.initDraggable($dragger, framePadding, function(e, left) {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 9b952ea7b60..3a03cbf6b90 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,9 +1,10 @@
/* eslint-disable func-names, wrap-iife, consistent-return,
no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
prefer-template, object-shorthand, prefer-arrow-callback */
-/* global Pager */
import { pluralize } from './lib/utils/text_utility';
+import { localTimeAgo } from './lib/utils/datetime_utility';
+import Pager from './pager';
export default (function () {
const CommitsList = {};
@@ -91,7 +92,7 @@ export default (function () {
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
- gl.utils.localTimeAgo($processedData.find('.js-timeago'));
+ localTimeAgo($processedData.find('.js-timeago'));
return processedData;
};
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 9e5dbd64a7e..144caf1d278 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,7 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
+import { localTimeAgo } from './lib/utils/datetime_utility';
-window.Compare = (function() {
- function Compare(opts) {
+export default class Compare {
+ constructor(opts) {
this.opts = opts;
this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading");
@@ -34,12 +35,12 @@ window.Compare = (function() {
this.initialState();
}
- Compare.prototype.initialState = function() {
+ initialState() {
this.getSourceHtml();
- return this.getTargetHtml();
- };
+ this.getTargetHtml();
+ }
- Compare.prototype.getTargetProject = function() {
+ getTargetProject() {
return $.ajax({
url: this.opts.targetProjectUrl,
data: {
@@ -52,22 +53,22 @@ window.Compare = (function() {
return $('.js-target-branch-dropdown .dropdown-content').html(html);
}
});
- };
+ }
- Compare.prototype.getSourceHtml = function() {
- return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
+ getSourceHtml() {
+ return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val()
});
- };
+ }
- Compare.prototype.getTargetHtml = function() {
- return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
+ getTargetHtml() {
+ return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val()
});
- };
+ }
- Compare.prototype.sendAjax = function(url, loading, target, data) {
+ static sendAjax(url, loading, target, data) {
var $target;
$target = $(target);
return $.ajax({
@@ -81,10 +82,8 @@ window.Compare = (function() {
loading.hide();
$target.html(html);
var className = '.' + $target[0].className.replace(' ', '.');
- gl.utils.localTimeAgo($('.js-timeago', className));
+ localTimeAgo($('.js-timeago', className));
}
});
- };
-
- return Compare;
-})();
+ }
+}
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 72c0d98d47c..e633ef8a29e 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,68 +1,60 @@
/* 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 */
-window.CompareAutocomplete = (function() {
- function CompareAutocomplete() {
- this.initDropdown();
- }
-
- CompareAutocomplete.prototype.initDropdown = function() {
- return $('.js-compare-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
- const $dropdownContainer = $dropdown.closest('.dropdown');
- const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
- const $filterInput = $('input[type="search"]', $dropdownContainer);
- $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
- ref: $dropdown.data('ref'),
- search: term,
- }
- }).done(function(refs) {
- return callback(refs);
- });
- },
- selectable: true,
- filterable: true,
- filterRemote: true,
- fieldName: $dropdown.data('field-name'),
- filterInput: 'input[type="search"]',
- renderRow: function(ref) {
- var link;
- if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
- } else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
- return $('<li />').append(link);
+export default function initCompareAutocomplete() {
+ $('.js-compare-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ const $dropdownContainer = $dropdown.closest('.dropdown');
+ const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
+ const $filterInput = $('input[type="search"]', $dropdownContainer);
+ $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref'),
+ search: term,
}
- },
- id: function(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- }
- });
- $filterInput.on('keyup', (e) => {
- const keyCode = e.keyCode || e.which;
- if (keyCode !== 13) return;
- const text = $filterInput.val();
- $fieldInput.val(text);
- $('.dropdown-toggle-text', $dropdown).text(text);
- $dropdownContainer.removeClass('open');
- });
-
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
- $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
- if ($dropdown.hasClass('has-tooltip')) {
- $dropdown.tooltip('fixTitle');
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ fieldName: $dropdown.data('field-name'),
+ filterInput: 'input[type="search"]',
+ renderRow: function(ref) {
+ var link;
+ if (ref.header != null) {
+ return $('<li />').addClass('dropdown-header').text(ref.header);
+ } else {
+ link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ return $('<li />').append(link);
}
- });
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ }
+ });
+ $filterInput.on('keyup', (e) => {
+ const keyCode = e.keyCode || e.which;
+ if (keyCode !== 13) return;
+ const text = $filterInput.val();
+ $fieldInput.val(text);
+ $('.dropdown-toggle-text', $dropdown).text(text);
+ $dropdownContainer.removeClass('open');
});
- };
- return CompareAutocomplete;
-})();
+ $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
+ if ($dropdown.hasClass('has-tooltip')) {
+ $dropdown.tooltip('fixTitle');
+ }
+ });
+ });
+}
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 46b68ebe158..74520675a7c 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -9,7 +9,7 @@ export default class ContextualSidebar {
}
initDomElements() {
- this.$page = $('.page-with-sidebar');
+ this.$page = $('.layout-page');
this.$sidebar = $('.nav-sidebar');
this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
this.$overlay = $('.mobile-overlay');
@@ -28,7 +28,7 @@ export default class ContextualSidebar {
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
this.$sidebarToggle.on('click', () => {
- const value = !this.$sidebar.hasClass('sidebar-icons-only');
+ const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
this.toggleCollapsedSidebar(value);
});
@@ -43,16 +43,16 @@ export default class ContextualSidebar {
}
toggleSidebarNav(show) {
- this.$sidebar.toggleClass('nav-sidebar-expanded', show);
+ this.$sidebar.toggleClass('sidebar-expanded-mobile', show);
this.$overlay.toggleClass('mobile-nav-open', show);
- this.$sidebar.removeClass('sidebar-icons-only');
+ this.$sidebar.removeClass('sidebar-collapsed-desktop');
}
toggleCollapsedSidebar(collapsed) {
const breakpoint = bp.getBreakpointSize();
if (this.$sidebar.length) {
- this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
+ this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
ContextualSidebar.setCollapsedCookie(collapsed);
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 23425672b16..eedbd3feeb5 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -276,13 +276,13 @@ export default class CreateMergeRequestDropdown {
let target;
let value;
- if (event.srcElement === this.branchInput) {
+ if (event.target === this.branchInput) {
target = 'branch';
value = this.branchInput.value;
- } else if (event.srcElement === this.refInput) {
+ } else if (event.target === this.refInput) {
target = 'ref';
- value = event.srcElement.value.slice(0, event.srcElement.selectionStart) +
- event.srcElement.value.slice(event.srcElement.selectionEnd);
+ value = event.target.value.slice(0, event.target.selectionStart) +
+ event.target.value.slice(event.target.selectionEnd);
} else {
return false;
}
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 f54ea7df522..cbce9205e75 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -2,6 +2,7 @@
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
+ import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@@ -12,6 +13,7 @@
userAvatarImage,
totalTime,
limitWarning,
+ icon,
},
};
</script>
@@ -52,7 +54,10 @@
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
- <i class= "fa fa-code-fork"></i>
+ <icon
+ name="fork"
+ :size="16">
+ </icon>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
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 5d95ddcd90e..508a411e599 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
@@ -3,6 +3,7 @@
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
+ import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@@ -13,6 +14,7 @@
userAvatarImage,
totalTime,
limitWarning,
+ icon,
},
computed: {
iconBranch() {
@@ -37,7 +39,10 @@
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
+ <icon
+ name="fork"
+ :size="16">
+ </icon>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
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 04d5440b77b..88fa6b073ca 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
@@ -3,6 +3,7 @@
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
+ import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@@ -12,6 +13,7 @@
components: {
totalTime,
limitWarning,
+ icon,
},
computed: {
iconBuildStatus() {
@@ -40,7 +42,10 @@
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
+ <icon
+ name="fork"
+ :size="16">
+ </icon>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index b41d464475f..2a05c6f001e 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,5 +1,6 @@
<script>
import actionBtn from './action_btn.vue';
+ import { getTimeago } from '../../lib/utils/datetime_utility';
export default {
props: {
@@ -21,7 +22,7 @@
},
computed: {
timeagoDate() {
- return gl.utils.getTimeago().format(this.deployKey.created_at);
+ return getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index c8874e48c09..a162424b3cf 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,4 +1,4 @@
-import './lib/utils/url_utility';
+import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
import imageDiffHelper from './image_diff/helpers/index';
@@ -31,7 +31,7 @@ export default class Diff {
isBound = true;
}
- if (gl.utils.getLocationHash()) {
+ if (getLocationHash()) {
this.highlightSelectedLine();
}
@@ -73,7 +73,7 @@ export default class Diff {
}
openAnchoredDiff(cb) {
- const locationHash = gl.utils.getLocationHash();
+ const locationHash = getLocationHash();
const anchoredDiff = locationHash && locationHash.split('_')[0];
if (!anchoredDiff) return;
@@ -128,7 +128,7 @@ export default class Diff {
}
// eslint-disable-next-line class-methods-use-this
highlightSelectedLine() {
- const hash = gl.utils.getLocationHash();
+ const hash = getLocationHash();
const $diffFiles = $('.diff-file');
$diffFiles.find('.hll').removeClass('hll');
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 06ce84d7599..300b02da663 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -1,8 +1,8 @@
/* global CommentsStore */
-/* global notes */
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
+import Notes from '../../notes';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({
@@ -129,7 +129,7 @@ const DiffNoteAvatars = Vue.extend({
},
methods: {
clickedAvatar(e) {
- notes.onAddDiffNote(e);
+ Notes.instance.onAddDiffNote(e);
// Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState();
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
index dc43e4b2cc7..1b8a9af9390 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -2,6 +2,7 @@
/* global NoteModel */
import Vue from 'vue';
+import { localTimeAgo } from '../../lib/utils/datetime_utility';
class DiscussionModel {
constructor (discussionId) {
@@ -71,7 +72,7 @@ class DiscussionModel {
$(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
}
- gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
+ localTimeAgo($('.js-timeago', `${discussionSelector}`));
} else {
$discussionHeadline.remove();
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 678af8f7b7a..42f61d33f6e 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -5,36 +5,37 @@ import IssuableIndex from './issuable_index';
import Milestone from './milestone';
import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
-/* global MilestoneSelect */
+import MilestoneSelect from './milestone_select';
import NewBranchForm from './new_branch_form';
-/* global NotificationsForm */
-/* global NotificationsDropdown */
+import NotificationsForm from './notifications_form';
+import notificationsDropdown from './notifications_dropdown';
import groupAvatar from './group_avatar';
import GroupLabelSubscription from './group_label_subscription';
-/* global LineHighlighter */
+import LineHighlighter from './line_highlighter';
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select';
-/* global Search */
-/* global Admin */
+import Search from './search';
+import initAdmin from './admin';
import NamespaceSelect from './namespace_select';
import NewCommitForm from './new_commit_form';
import Project from './project';
import projectAvatar from './project_avatar';
-/* global MergeRequest */
-/* global Compare */
-/* global CompareAutocomplete */
-/* global ProjectFindFile */
+import MergeRequest from './merge_request';
+import Compare from './compare';
+import initCompareAutocomplete from './compare_autocomplete';
+import ProjectFindFile from './project_find_file';
import ProjectNew from './project_new';
import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
-/* global Sidebar */
+import Sidebar from './right_sidebar';
import IssuableTemplateSelectors from './templates/issuable_template_selectors';
import Flash from './flash';
import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
+import SecretValues from './behaviors/secret_values';
import DeleteModal from './branches/branches_delete_modal';
import Group from './group';
import GroupsList from './groups_list';
@@ -72,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
-import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports';
@@ -90,7 +90,8 @@ import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
import Diff from './diff';
import ProjectLabelSubscription from './project_label_subscription';
-import ProjectVariables from './project_variables';
+import SearchAutocomplete from './search_autocomplete';
+import Activities from './activities';
(function() {
var Dispatcher;
@@ -109,6 +110,8 @@ import ProjectVariables from './project_variables';
return false;
}
+ const fail = () => Flash('Error loading dynamic module');
+
path = page.split(':');
shortcut_handler = null;
@@ -333,7 +336,7 @@ import ProjectVariables from './project_variables';
shortcut_handler = new ShortcutsIssuable(true);
break;
case 'dashboard:activity':
- new gl.Activities();
+ new Activities();
break;
case 'projects:commit:show':
new Diff();
@@ -354,7 +357,7 @@ import ProjectVariables from './project_variables';
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break;
case 'projects:activity':
- new gl.Activities();
+ new Activities();
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:commits:show':
@@ -372,7 +375,7 @@ import ProjectVariables from './project_variables';
if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer();
- if ($('.project-show-activity').length) new gl.Activities();
+ if ($('.project-show-activity').length) new Activities();
$('#tree-slider').waitForImages(function() {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
@@ -406,13 +409,13 @@ import ProjectVariables from './project_variables';
});
break;
case 'groups:activity':
- new gl.Activities();
+ new Activities();
break;
case 'groups:show':
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
- new NotificationsDropdown();
+ notificationsDropdown();
new ProjectsList();
if (newGroupChildWrapper) {
@@ -445,9 +448,6 @@ import ProjectVariables from './project_variables';
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
-
- if (UserFeatureHelper.isNewRepoEnabled()) break;
-
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
@@ -466,7 +466,6 @@ import ProjectVariables from './project_variables';
shortcut_handler = true;
break;
case 'projects:blob:show':
- if (UserFeatureHelper.isNewRepoEnabled()) break;
new BlobViewer();
initBlob();
break;
@@ -525,15 +524,25 @@ import ProjectVariables from './project_variables';
case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels
initSettingsPanels();
+
+ const runnerToken = document.querySelector('.js-secret-runner-token');
+ if (runnerToken) {
+ const runnerTokenSecretValue = new SecretValues(runnerToken);
+ runnerTokenSecretValue.init();
+ }
case 'groups:settings:ci_cd:show':
- new ProjectVariables();
+ const secretVariableTable = document.querySelector('.js-secret-variable-table');
+ if (secretVariableTable) {
+ const secretVariableTableValues = new SecretValues(secretVariableTable);
+ secretVariableTableValues.init();
+ }
break;
case 'ci:lints:create':
case 'ci:lints:show':
new CILintEditor();
break;
case 'users:show':
- new UserCallout();
+ import('./pages/users/show').then(m => m.default()).catch(fail);
break;
case 'admin:conversational_development_index:show':
new UserCallout();
@@ -583,7 +592,7 @@ import ProjectVariables from './project_variables';
// needed in rspec
gl.u2fAuthenticate = u2fAuthenticate;
case 'admin':
- new Admin();
+ initAdmin();
switch (path[1]) {
case 'broadcast_messages':
initBroadcastMessagesForm();
@@ -615,14 +624,14 @@ import ProjectVariables from './project_variables';
break;
case 'profiles':
new NotificationsForm();
- new NotificationsDropdown();
+ notificationsDropdown();
break;
case 'projects':
new Project();
projectAvatar();
switch (path[1]) {
case 'compare':
- new CompareAutocomplete();
+ initCompareAutocomplete();
break;
case 'edit':
shortcut_handler = new ShortcutsNavigation();
@@ -638,7 +647,7 @@ import ProjectVariables from './project_variables';
case 'show':
new Star();
new ProjectNew();
- new NotificationsDropdown();
+ notificationsDropdown();
break;
case 'wikis':
new Wikis();
@@ -683,7 +692,7 @@ import ProjectVariables from './project_variables';
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
- return new gl.SearchAutocomplete();
+ return new SearchAutocomplete();
}
};
diff --git a/app/assets/javascripts/docs/docs_bundle.js b/app/assets/javascripts/docs/docs_bundle.js
new file mode 100644
index 00000000000..a32bd6d0fc7
--- /dev/null
+++ b/app/assets/javascripts/docs/docs_bundle.js
@@ -0,0 +1,13 @@
+import Mousetrap from 'mousetrap';
+
+function addMousetrapClick(el, key) {
+ el.addEventListener('click', () => Mousetrap.trigger(key));
+}
+
+function domContentLoaded() {
+ addMousetrapClick(document.querySelector('.js-trigger-shortcut'), '?');
+ addMousetrapClick(document.querySelector('.js-trigger-search-bar'), 's');
+}
+
+document.addEventListener('DOMContentLoaded', domContentLoaded);
+
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 69c57f923b6..c05a83176f2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,3 +1,4 @@
+import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
@@ -126,7 +127,7 @@ class FilteredSearchManager {
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this);
- this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.checkForBackspaceWrapper = this.checkForBackspace.call(this);
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
@@ -179,22 +180,34 @@ class FilteredSearchManager {
this.unbindStateEvents();
}
- checkForBackspace(e) {
- // 8 = Backspace Key
- // 46 = Delete Key
- if (e.keyCode === 8 || e.keyCode === 46) {
- const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ checkForBackspace() {
+ let backspaceCount = 0;
+
+ // closure for keeping track of the number of backspace keystrokes
+ return (e) => {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
+ const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
+
+ if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
+ backspaceCount += 1;
+
+ if (backspaceCount === 2) {
+ backspaceCount = 0;
+ this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ }
+ }
- const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
- const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
- if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
- this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ // Reposition dropdown so that it is aligned with cursor
+ this.dropdownManager.updateCurrentDropdownOffset();
+ } else {
+ backspaceCount = 0;
}
-
- // Reposition dropdown so that it is aligned with cursor
- this.dropdownManager.updateCurrentDropdownOffset();
- }
+ };
}
checkForEnter(e) {
@@ -566,7 +579,7 @@ class FilteredSearchManager {
if (this.updateObject) {
this.updateObject(parameterizedUrl);
} else {
- gl.utils.visitUrl(parameterizedUrl);
+ visitUrl(parameterizedUrl);
}
}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 98837c3b2a0..abb04d77f8f 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -21,7 +21,7 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
-export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only');
+export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
export const canShowActiveSubItems = (el) => {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
@@ -161,13 +161,16 @@ export default () => {
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
- sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
- clearTimeout(timeoutId);
+ const topItems = sidebar.querySelector('.sidebar-top-level-items');
+ if (topItems) {
+ sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
+ clearTimeout(timeoutId);
- timeoutId = setTimeout(() => {
- if (currentOpenMenu) hideMenu(currentOpenMenu);
- }, getHideSubItemsInterval());
- });
+ timeoutId = setTimeout(() => {
+ if (currentOpenMenu) hideMenu(currentOpenMenu);
+ }, getHideSubItemsInterval());
+ });
+ }
headerHeight = document.querySelector('.nav-sidebar').offsetTop;
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index d918d80df8d..df20e1e9c88 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -57,12 +57,12 @@ class GfmAutoComplete {
displayTpl(value) {
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
// eslint-disable-next-line no-template-curly-in-string
- let tpl = '<li>/${name}';
+ let tpl = '<li><span class="name">/${name}</span>';
if (value.aliases.length > 0) {
- tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
+ tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>';
}
if (value.params.length > 0) {
- tpl += ' <small><%- params.join(" ") %></small>';
+ tpl += ' <small class="params"><%- params.join(" ") %></small>';
}
if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>';
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 7ca783d3af6..64f258aed64 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -2,6 +2,7 @@
/* global fuzzaldrinPlus */
import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
@@ -299,7 +300,7 @@ GitLabDropdown = (function() {
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
- _this.focusTextInput(true);
+ _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
@@ -789,24 +790,16 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking];
};
- GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
+ GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) {
- this.dropdown.one('transitionend', () => {
- const initialScrollTop = $(window).scrollTop();
+ const initialScrollTop = $(window).scrollTop();
- if (this.dropdown.is('.open')) {
- this.filterInput.focus();
- }
-
- if ($(window).scrollTop() < initialScrollTop) {
- $(window).scrollTop(initialScrollTop);
- }
- });
+ if (this.dropdown.is('.open')) {
+ this.filterInput.focus();
+ }
- if (triggerFocus) {
- // This triggers after a ajax request
- // in case of slow requests, the dropdown transition could already be finished
- this.dropdown.trigger('transitionend');
+ if ($(window).scrollTop() < initialScrollTop) {
+ $(window).scrollTop(initialScrollTop);
}
}
};
@@ -852,7 +845,7 @@ GitLabDropdown = (function() {
if ($el.length) {
var href = $el.attr('href');
if (href && href !== '#') {
- gl.utils.visitUrl(href);
+ visitUrl(href);
} else {
$el.trigger('click');
}
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index e7232ca3712..151a4ce012c 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,13 +1,14 @@
/* 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 */
import _ from 'underscore';
-import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
-import { n__ } from '../locale';
+import { n__, s__, createDateTimeFormat, sprintf } from '../locale';
export default (function() {
- function ContributorsStatGraph() {}
+ function ContributorsStatGraph() {
+ this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
+ }
ContributorsStatGraph.prototype.init = function(log) {
var author_commits, total_commits;
@@ -83,9 +84,12 @@ export default (function() {
return _.each(author_commits, (function(_this) {
return function(d) {
_this.redraw_author_commit_info(d);
- $(_this.authors[d.author_name].list_item).appendTo("ol");
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
+ if (_this.authors[d.author_name] != null) {
+ $(_this.authors[d.author_name].list_item).appendTo("ol");
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ }
+ return '';
};
})(this));
};
@@ -95,18 +99,26 @@ export default (function() {
};
ContributorsStatGraph.prototype.change_date_header = function() {
- var print, print_date_format, x_domain;
- x_domain = ContributorsGraph.prototype.x_domain;
- print_date_format = d3.time.format("%B %e %Y");
- print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
- return $("#date_header").text(print);
+ const x_domain = ContributorsGraph.prototype.x_domain;
+ const formattedDateRange = sprintf(
+ s__('ContributorsPage|%{startDate} – %{endDate}'),
+ {
+ startDate: this.dateFormat.format(new Date(x_domain[0])),
+ endDate: this.dateFormat.format(new Date(x_domain[1])),
+ },
+ );
+ return $('#date_header').text(formattedDateRange);
};
ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
- var author_commit_info, author_list_item;
- author_list_item = $(this.authors[author.author_name].list_item);
- author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find("span").html(author_commit_info);
+ var author_commit_info, author_list_item, $author;
+ $author = this.authors[author.author_name];
+ if ($author != null) {
+ author_list_item = $(this.authors[author.author_name].list_item);
+ author_commit_info = this.format_author_commit_info(author);
+ return author_list_item.find("span").html(author_commit_info);
+ }
+ return '';
};
return ContributorsStatGraph;
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index f64b4638485..9a4012232a0 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,6 +1,15 @@
/* eslint-disable func-names, space-before-function-paren, no-var, 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 */
import _ from 'underscore';
-import d3 from 'd3';
+import { extent, max } from 'd3-array';
+import { select, event as d3Event } from 'd3-selection';
+import { scaleTime, scaleLinear } from 'd3-scale';
+import { axisLeft, axisBottom } from 'd3-axis';
+import { area } from 'd3-shape';
+import { brushX } from 'd3-brush';
+import { timeParse } from 'd3-time-format';
+import { dateTickFormat } from '../lib/utils/tick_formats';
+
+const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty;
@@ -70,8 +79,8 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3.time.scale().range([0, width]).clamp(true);
- return this.y = d3.scale.linear().range([height, 0]).nice();
+ this.x = d3.scaleTime().range([0, width]).clamp(true);
+ return this.y = d3.scaleLinear().range([height, 0]).nice();
};
ContributorsGraph.prototype.draw_x_axis = function() {
@@ -93,9 +102,12 @@ export const ContributorsMasterGraph = (function(superClass) {
extend(ContributorsMasterGraph, superClass);
function ContributorsMasterGraph(data1) {
+ const $parentElement = $('#contributors-master');
+ const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
+
this.data = data1;
this.update_content = this.update_content.bind(this);
- this.width = $('.content').width() - 70;
+ this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right);
this.height = 200;
this.x = null;
this.y = null;
@@ -120,7 +132,7 @@ export const ContributorsMasterGraph = (function(superClass) {
ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
+ parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
@@ -131,8 +143,10 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ this.x_axis = d3.axisBottom()
+ .scale(this.x)
+ .tickFormat(dateTickFormat);
+ return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
};
ContributorsMasterGraph.prototype.create_svg = function() {
@@ -140,16 +154,16 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
+ return this.area = d3.area().x(function(d) {
return x(d.date);
}).y0(this.height).y1(function(d) {
d.commits = d.commits || d.additions || d.deletions;
return y(d.commits);
- }).interpolate("basis");
+ });
};
ContributorsMasterGraph.prototype.create_brush = function() {
- return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
+ return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content);
};
ContributorsMasterGraph.prototype.draw_path = function(data) {
@@ -161,7 +175,12 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.update_content = function() {
- ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
+ // d3Event.selection replaces the function brush.empty() calls
+ if (d3Event.selection != null) {
+ ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert));
+ } else {
+ ContributorsGraph.set_x_domain(this.x_max_domain);
+ }
return $("#brush_change").trigger('change');
};
@@ -219,14 +238,17 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ this.x_axis = d3.axisBottom()
+ .scale(this.x)
+ .ticks(8)
+ .tickFormat(dateTickFormat);
+ return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
};
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
+ return this.area = d3.area().x(function(d) {
var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
+ parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
@@ -236,11 +258,12 @@ export const ContributorsAuthorGraph = (function(superClass) {
return y(0);
}
};
- })(this)).interpolate("basis");
+ })(this));
};
ContributorsAuthorGraph.prototype.create_svg = function() {
- this.list_item = d3.selectAll(".person")[0].pop();
+ var persons = document.querySelectorAll('.person');
+ this.list_item = persons[persons.length - 1];
return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
};
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 2c0b6ab4ea8..241e026b84c 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -5,7 +5,7 @@ import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { COMMON_STR } from '../constants';
-
+import { mergeUrlParams } from '../../lib/utils/url_utility';
import groupsComponent from './groups.vue';
export default {
@@ -93,7 +93,7 @@ export default {
this.isLoading = false;
$.scrollTo(0);
- const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
+ const currentPath = mergeUrlParams({ page }, window.location.href);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index c76ce762b54..42e79a9e17a 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -1,4 +1,5 @@
<script>
+import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
@@ -60,7 +61,7 @@ export default {
if (this.hasChildren) {
eventHub.$emit('toggleChildren', this.group);
} else {
- gl.utils.visitUrl(this.group.relativePath);
+ visitUrl(this.group.relativePath);
}
}
},
@@ -76,7 +77,8 @@ export default {
class="group-row"
>
<div
- class="group-row-contents">
+ class="group-row-contents"
+ :class="{ 'project-row-contents': !isGroup }">
<item-actions
v-if="isGroup"
:group="group"
@@ -96,7 +98,7 @@ export default {
/>
</div>
<div
- class="avatar-container s40 hidden-xs"
+ class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs"
:class="{ 'content-loading': group.isChildrenLoading }"
>
<a
@@ -105,11 +107,12 @@ export default {
>
<img
v-if="hasAvatar"
- class="avatar s40"
+ class="avatar s24"
:src="group.avatarUrl"
/>
<identicon
v-else
+ size-class="s24"
:entity-id=group.id
:entity-name="group.name"
/>
@@ -122,7 +125,7 @@ export default {
:href="group.relativePath"
:title="group.fullName"
class="no-expand"
- data-placement="top"
+ data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
@@ -138,7 +141,8 @@ export default {
<div
v-if="group.description"
class="description">
- {{group.description}}
+ <span v-html="group.description">
+ </span>
</div>
</div>
<group-folder
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 09cb79c1afd..0dd0783ce06 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -1,15 +1,15 @@
<script>
-import { s__ } from '../../locale';
-import tooltip from '../../vue_shared/directives/tooltip';
-import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import { s__ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import icon from '~/vue_shared/components/icon.vue';
+import modal from '~/vue_shared/components/modal.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
-import Icon from '../../vue_shared/components/icon.vue';
export default {
components: {
- Icon,
- PopupDialog,
+ icon,
+ modal,
},
directives: {
tooltip,
@@ -27,7 +27,7 @@ export default {
},
data() {
return {
- dialogStatus: false,
+ modalStatus: false,
};
},
computed: {
@@ -43,13 +43,11 @@ export default {
},
methods: {
onLeaveGroup() {
- this.dialogStatus = true;
+ this.modalStatus = true;
},
- leaveGroup(leaveConfirmed) {
- this.dialogStatus = false;
- if (leaveConfirmed) {
- eventHub.$emit('leaveGroup', this.group, this.parentGroup);
- }
+ leaveGroup() {
+ this.modalStatus = false;
+ eventHub.$emit('leaveGroup', this.group, this.parentGroup);
},
},
};
@@ -64,10 +62,9 @@ export default {
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-container="body"
+ data-placement="bottom"
class="edit-group btn no-expand">
- <icon
- name="settings">
- </icon>
+ <icon name="settings"/>
</a>
<a
v-tooltip
@@ -77,13 +74,12 @@ export default {
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
+ data-placement="bottom"
class="leave-group btn no-expand">
- <i
- class="fa fa-sign-out"
- aria-hidden="true"/>
+ <icon name="leave"/>
</a>
- <popup-dialog
- v-show="dialogStatus"
+ <modal
+ v-show="modalStatus"
:primary-button-label="__('Leave')"
kind="warning"
:title="__('Are you sure?')"
diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue
index 959b984816f..9e90fe2b701 100644
--- a/app/assets/javascripts/groups/components/item_caret.vue
+++ b/app/assets/javascripts/groups/components/item_caret.vue
@@ -1,4 +1,6 @@
<script>
+import icon from '~/vue_shared/components/icon.vue';
+
export default {
props: {
isGroupOpen: {
@@ -7,9 +9,12 @@ export default {
default: false,
},
},
+ components: {
+ icon,
+ },
computed: {
iconClass() {
- return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
+ return this.isGroupOpen ? 'angle-down' : 'angle-right';
},
},
};
@@ -17,9 +22,9 @@ export default {
<template>
<span class="folder-caret">
- <i
- :class="iconClass"
- class="fa"
- aria-hidden="true"/>
+ <icon
+ :size="12"
+ :name="iconClass"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 9f8ac138fc3..2e42fb6c9a6 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -1,10 +1,14 @@
<script>
-import tooltip from '../../vue_shared/directives/tooltip';
+import icon from '~/vue_shared/components/icon.vue';
+import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
+import itemStatsValue from './item_stats_value.vue';
export default {
- directives: {
- tooltip,
+ components: {
+ icon,
+ timeAgoTooltip,
+ itemStatsValue,
},
props: {
item: {
@@ -34,65 +38,47 @@ export default {
<template>
<div class="stats">
- <span
- v-tooltip
+ <item-stats-value
v-if="isGroup"
- :title="s__('Subgroups')"
- class="number-subgroups"
- data-placement="top"
- data-container="body">
- <i
- class="fa fa-folder"
- aria-hidden="true"
- />
- {{item.subgroupCount}}
- </span>
- <span
- v-tooltip
+ css-class="number-subgroups"
+ icon-name="folder"
+ :title="__('Subgroups')"
+ :value="item.subgroupCount"
+ />
+ <item-stats-value
v-if="isGroup"
- :title="s__('Projects')"
- class="number-projects"
- data-placement="top"
- data-container="body">
- <i
- class="fa fa-bookmark"
- aria-hidden="true"
- />
- {{item.projectCount}}
- </span>
- <span
- v-tooltip
+ css-class="number-projects"
+ icon-name="bookmark"
+ :title="__('Projects')"
+ :value="item.projectCount"
+ />
+ <item-stats-value
v-if="isGroup"
- :title="s__('Members')"
- class="number-users"
- data-placement="top"
- data-container="body">
- <i
- class="fa fa-users"
- aria-hidden="true"
- />
- {{item.memberCount}}
- </span>
- <span
+ css-class="number-users"
+ icon-name="users"
+ :title="__('Members')"
+ :value="item.memberCount"
+ />
+ <item-stats-value
v-if="isProject"
- class="project-stars">
- <i
- class="fa fa-star"
- aria-hidden="true"
- />
- {{item.starCount}}
- </span>
- <span
- v-tooltip
+ 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"
- data-placement="left"
- data-container="body"
- class="item-visibility">
- <i
- :class="visibilityIcon"
- class="fa"
- aria-hidden="true"
+ />
+ <div
+ class="last-updated"
+ v-if="isProject"
+ >
+ <time-ago-tooltip
+ tooltip-placement="bottom"
+ :time="item.updatedAt"
/>
- </span>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue
new file mode 100644
index 00000000000..f441cabf6d2
--- /dev/null
+++ b/app/assets/javascripts/groups/components/item_stats_value.vue
@@ -0,0 +1,68 @@
+<script>
+import tooltip from '~/vue_shared/directives/tooltip';
+import icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ iconName: {
+ type: String,
+ required: true,
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'bottom',
+ },
+ /**
+ * value could either be number or string
+ * as `memberCount` is always passed as string
+ * while `subgroupCount` & `projectCount`
+ * are always number
+ */
+ value: {
+ type: [Number, String],
+ required: false,
+ default: '',
+ },
+ },
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ },
+ computed: {
+ isValuePresent() {
+ return this.value !== '';
+ },
+ },
+};
+</script>
+
+<template>
+ <span
+ v-tooltip
+ data-container="body"
+ :data-placement="tooltipPlacement"
+ :class="cssClass"
+ :title="title"
+ >
+ <icon :name="iconName"/>
+ <span
+ v-if="isValuePresent"
+ class="stat-value"
+ >
+ {{value}}
+ </span>
+ </span>
+</template>
diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue
index c02a8ad6d8c..118d94d4937 100644
--- a/app/assets/javascripts/groups/components/item_type_icon.vue
+++ b/app/assets/javascripts/groups/components/item_type_icon.vue
@@ -1,7 +1,11 @@
<script>
+import icon from '~/vue_shared/components/icon.vue';
import { ITEM_TYPE } from '../constants';
export default {
+ components: {
+ icon,
+ },
props: {
itemType: {
type: String,
@@ -16,9 +20,9 @@ export default {
computed: {
iconClass() {
if (this.itemType === ITEM_TYPE.GROUP) {
- return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder';
+ return this.isGroupOpen ? 'folder-open' : 'folder';
}
- return 'fa-bookmark';
+ return 'bookmark';
},
},
};
@@ -26,9 +30,6 @@ export default {
<template>
<span class="item-type-icon">
- <i
- :class="iconClass"
- class="fa"
- aria-hidden="true"/>
+ <icon :name="iconClass"/>
</span>
</template>
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index 6fde41414b3..b8baed682f5 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -29,7 +29,7 @@ export const PROJECT_VISIBILITY_TYPE = {
};
export const VISIBILITY_TYPE_ICON = {
- public: 'fa-globe',
- internal: 'fa-shield',
- private: 'fa-lock',
+ public: 'earth',
+ internal: 'shield',
+ private: 'lock',
};
diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js
index 8e273579aae..a120d501e35 100644
--- a/app/assets/javascripts/groups/new_group_child.js
+++ b/app/assets/javascripts/groups/new_group_child.js
@@ -1,3 +1,4 @@
+import { visitUrl } from '../lib/utils/url_utility';
import DropLab from '../droplab/drop_lab';
import ISetter from '../droplab/plugins/input_setter';
@@ -54,9 +55,9 @@ export default class NewGroupChild {
onClickNewGroupChildButton(e) {
if (e.target.dataset.action === NEW_PROJECT) {
- gl.utils.visitUrl(this.newGroupPath);
+ visitUrl(this.newGroupPath);
} else if (e.target.dataset.action === NEW_SUBGROUP) {
- gl.utils.visitUrl(this.subgroupPath);
+ visitUrl(this.subgroupPath);
}
}
}
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index a1689f4c5cc..4a7569078a1 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -71,7 +71,7 @@ export default class GroupsStore {
id: rawGroupItem.id,
name: rawGroupItem.name,
fullName: rawGroupItem.full_name,
- description: rawGroupItem.description,
+ description: rawGroupItem.markdown_description,
visibility: rawGroupItem.visibility,
avatarUrl: rawGroupItem.avatar_url,
relativePath: rawGroupItem.relative_path,
@@ -91,6 +91,7 @@ export default class GroupsStore {
subgroupCount: rawGroupItem.subgroup_count,
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
+ updatedAt: rawGroupItem.updated_at,
};
}
diff --git a/app/assets/javascripts/helpers/user_feature_helper.js b/app/assets/javascripts/helpers/user_feature_helper.js
deleted file mode 100644
index 638118a5204..00000000000
--- a/app/assets/javascripts/helpers/user_feature_helper.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Cookies from 'js-cookie';
-
-export default {
- isNewRepoEnabled() {
- return Cookies.get('new_repo') === 'true';
- },
-};
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
new file mode 100644
index 00000000000..704dff981df
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -0,0 +1,66 @@
+<script>
+ import { mapState } from 'vuex';
+ import icon from '../../../vue_shared/components/icon.vue';
+ import listItem from './list_item.vue';
+ import listCollapsed from './list_collapsed.vue';
+
+ export default {
+ components: {
+ icon,
+ listItem,
+ listCollapsed,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ fileList: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
+ ]),
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed');
+ },
+ },
+
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-list">
+ <list-collapsed
+ v-if="rightPanelCollapsed"
+ />
+ <template v-else>
+ <ul
+ v-if="fileList.length"
+ class="list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ />
+ </li>
+ </ul>
+ <div
+ v-else
+ class="help-block prepend-top-0"
+ >
+ No changes
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 6a0262f271b..6a0262f271b 100644
--- a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 742f746e02f..742f746e02f 100644
--- a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
new file mode 100644
index 00000000000..26a70f6e748
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -0,0 +1,95 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import repoFileButtons from './repo_file_buttons.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoPreview from './repo_preview.vue';
+import repoEditor from './repo_editor.vue';
+
+export default {
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'currentBlobView',
+ 'selectedFile',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ 'activeFile',
+ ]),
+ },
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ repoFileButtons,
+ ideStatusBar,
+ repoEditor,
+ repoPreview,
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = (e) => {
+ if (!this.changedFiles.length) return undefined;
+
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-view"
+ >
+ <ide-sidebar/>
+ <div
+ class="multi-file-edit-pane"
+ >
+ <template
+ v-if="activeFile">
+ <repo-tabs/>
+ <component
+ class="multi-file-edit-pane-content"
+ :is="currentBlobView"
+ />
+ <repo-file-buttons/>
+ <ide-status-bar
+ :file="selectedFile"/>
+ </template>
+ <template
+ v-else>
+ <div class="ide-empty-state">
+ <div class="row js-empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content svg-250">
+ <img :src="emptyStateSvgPath">
+ </div>
+ </div>
+ <div class="col-xs-12">
+ <div class="text-content text-center">
+ <h4>
+ Welcome to the GitLab IDE
+ </h4>
+ <p>
+ You can select a file in the left sidebar to begin editing and use the right sidebar to commit your changes.
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+ </div>
+ <ide-contextbar/>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
new file mode 100644
index 00000000000..78c01272af6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -0,0 +1,108 @@
+<script>
+import { mapGetters, mapState, mapActions } from 'vuex';
+import repoCommitSection from './repo_commit_section.vue';
+import icon from '../../vue_shared/components/icon.vue';
+import panelResizer from '../../vue_shared/components/panel_resizer.vue';
+
+export default {
+ data() {
+ return {
+ width: 290,
+ };
+ },
+ components: {
+ repoCommitSection,
+ icon,
+ panelResizer,
+ },
+ computed: {
+ ...mapState([
+ 'rightPanelCollapsed',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ ]),
+ currentIcon() {
+ return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
+ },
+ maxSize() {
+ return window.innerWidth / 2;
+ },
+ panelStyle() {
+ if (!this.rightPanelCollapsed) {
+ return { width: `${this.width}px` };
+ }
+ return {};
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setPanelCollapsedStatus',
+ 'setResizingStatus',
+ ]),
+ toggleCollapsed() {
+ this.setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !this.rightPanelCollapsed,
+ });
+ },
+ resizingStarted() {
+ this.setResizingStatus(true);
+ },
+ resizingEnded() {
+ this.setResizingStatus(false);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ :style="panelStyle"
+ >
+ <div
+ class="multi-file-commit-panel-section">
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <div
+ class="multi-file-commit-panel-header-title"
+ v-if="!rightPanelCollapsed">
+ <icon
+ name="list-bulleted"
+ :size="18"
+ />
+ Staged
+ </div>
+ <button
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <icon
+ :name="currentIcon"
+ :size="18"
+ />
+ </button>
+ </header>
+ <repo-commit-section
+ class=""/>
+ </div>
+ <panel-resizer
+ :size.sync="width"
+ :enabled="!rightPanelCollapsed"
+ :start-size="290"
+ :min-size="200"
+ :max-size="maxSize"
+ @resize-start="resizingStarted"
+ @resize-end="resizingEnded"
+ side="left"/>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
new file mode 100644
index 00000000000..bd3a521ff43
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
@@ -0,0 +1,47 @@
+<script>
+import repoTree from './ide_repo_tree.vue';
+import icon from '../../vue_shared/components/icon.vue';
+import newDropdown from './new_dropdown/index.vue';
+
+export default {
+ components: {
+ repoTree,
+ icon,
+ newDropdown,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ branch: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="branch-container">
+ <div class="branch-header">
+ <div class="branch-header-title">
+ <icon
+ name="branch"
+ :size="12">
+ </icon>
+ {{ branch.name }}
+ </div>
+ <div class="branch-header-btns">
+ <new-dropdown
+ :project-id="projectId"
+ :branch="branch.name"
+ path=""/>
+ </div>
+ </div>
+ <div>
+ <repo-tree
+ :treeId="branch.treeId"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
new file mode 100644
index 00000000000..61daba6d176
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_tree.vue
@@ -0,0 +1,47 @@
+<script>
+import branchesTree from './ide_project_branches_tree.vue';
+import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
+
+export default {
+ components: {
+ branchesTree,
+ projectAvatarImage,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="projects-sidebar">
+ <div class="context-header">
+ <a
+ :title="project.name"
+ :href="project.web_url">
+ <div class="avatar-container s40 project-avatar">
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="project.path"
+ :img-src="project.avatar_url"
+ :img-alt="project.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="sidebar-context-title">
+ {{ project.name }}
+ </div>
+ </a>
+ </div>
+ <div class="multi-file-commit-panel-inner-scroll">
+ <branches-tree
+ v-for="(branch, index) in project.branches"
+ :key="branch.name"
+ :project-id="project.path_with_namespace"
+ :branch="branch"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
new file mode 100644
index 00000000000..bd89ebe47d9
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue
@@ -0,0 +1,71 @@
+<script>
+import { mapState } from 'vuex';
+import repoPreviousDirectory from './repo_prev_directory.vue';
+import repoFile from './repo_file.vue';
+import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
+import { treeList } from '../stores/utils';
+
+export default {
+ components: {
+ repoPreviousDirectory,
+ repoFile,
+ skeletonLoadingContainer,
+ },
+ props: {
+ treeId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'trees',
+ 'isRoot',
+ ]),
+ ...mapState({
+ projectName(state) {
+ return state.project.name;
+ },
+ }),
+ fetchedList() {
+ return treeList(this.$store.state, this.treeId);
+ },
+ hasPreviousDirectory() {
+ return !this.isRoot && this.fetchedList.length;
+ },
+ showLoading() {
+ if (this.trees[this.treeId]) {
+ return this.trees[this.treeId].loading;
+ }
+ return true;
+ },
+ },
+};
+</script>
+
+<template>
+<div>
+ <div class="ide-file-list">
+ <table class="table">
+ <tbody
+ v-if="treeId">
+ <repo-previous-directory
+ v-if="hasPreviousDirectory"
+ />
+ <div
+ class="multi-file-loading-container"
+ v-if="showLoading"
+ v-for="n in 3"
+ :key="n">
+ <skeleton-loading-container/>
+ </div>
+ <repo-file
+ v-for="file in fetchedList"
+ :key="file.key"
+ :file="file"
+ />
+ </tbody>
+ </table>
+ </div>
+</div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
new file mode 100644
index 00000000000..c30018e04b0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -0,0 +1,108 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import projectTree from './ide_project_tree.vue';
+import icon from '../../vue_shared/components/icon.vue';
+import panelResizer from '../../vue_shared/components/panel_resizer.vue';
+import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
+
+export default {
+ data() {
+ return {
+ width: 290,
+ };
+ },
+ components: {
+ projectTree,
+ icon,
+ panelResizer,
+ skeletonLoadingContainer,
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'projects',
+ 'leftPanelCollapsed',
+ ]),
+ currentIcon() {
+ return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
+ },
+ maxSize() {
+ return window.innerWidth / 2;
+ },
+ panelStyle() {
+ if (!this.leftPanelCollapsed) {
+ return { width: `${this.width}px` };
+ }
+ return {};
+ },
+ showLoading() {
+ return this.loading;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setPanelCollapsedStatus',
+ 'setResizingStatus',
+ ]),
+ toggleCollapsed() {
+ this.setPanelCollapsedStatus({
+ side: 'left',
+ collapsed: !this.leftPanelCollapsed,
+ });
+ },
+ resizingStarted() {
+ this.setResizingStatus(true);
+ },
+ resizingEnded() {
+ this.setResizingStatus(false);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': leftPanelCollapsed,
+ }"
+ :style="panelStyle"
+ >
+ <div class="multi-file-commit-panel-inner">
+ <div
+ class="multi-file-loading-container"
+ v-if="showLoading"
+ v-for="n in 3"
+ :key="n">
+ <skeleton-loading-container/>
+ </div>
+ <project-tree
+ v-for="(project, index) in projects"
+ :key="project.id"
+ :project="project"/>
+ </div>
+ <button
+ type="button"
+ class="btn btn-transparent left-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <icon
+ :name="currentIcon"
+ :size="18"
+ />
+ <span
+ v-if="!leftPanelCollapsed"
+ class="collapse-text"
+ >Collapse sidebar</span>
+ </button>
+ <panel-resizer
+ :size.sync="width"
+ :enabled="!leftPanelCollapsed"
+ :start-size="290"
+ :min-size="200"
+ :max-size="maxSize"
+ @resize-start="resizingStarted"
+ @resize-end="resizingEnded"
+ side="right"/>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
new file mode 100644
index 00000000000..a24abadd936
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -0,0 +1,71 @@
+<script>
+import { mapState } from 'vuex';
+import icon from '../../vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import timeAgoMixin from '../../vue_shared/mixins/timeago';
+
+export default {
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [
+ timeAgoMixin,
+ ],
+ computed: {
+ ...mapState([
+ 'selectedFile',
+ ]),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-status-bar">
+ <div>
+ <icon
+ name="branch"
+ :size="12">
+ </icon>
+ {{ selectedFile.branchId }}
+ </div>
+ <div>
+ <div
+ v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
+ Last commit:
+ <a
+ v-tooltip
+ :title="selectedFile.lastCommit.message"
+ :href="selectedFile.lastCommit.url">
+ {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
+ {{ selectedFile.lastCommit.author }}
+ </a>
+ </div>
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.name }}
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.eol }}
+ </div>
+ <div
+ class="text-right">
+ {{ file.editorRow }}:{{ file.editorColumn }}
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.fileLanguage }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue
index ba7090e4a9d..2119d373d31 100644
--- a/app/assets/javascripts/repo/components/new_branch_form.vue
+++ b/app/assets/javascripts/ide/components/new_branch_form.vue
@@ -44,7 +44,7 @@
this.branchName = '';
if (this.dropdownText) {
- this.dropdownText.textContent = this.currentBranch;
+ this.dropdownText.textContent = this.currentBranchId;
}
this.toggleDropdown();
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
new file mode 100644
index 00000000000..d475813c4f7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -0,0 +1,101 @@
+<script>
+ import newModal from './modal.vue';
+ import upload from './upload.vue';
+ import icon from '../../../vue_shared/components/icon.vue';
+
+ export default {
+ props: {
+ branch: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ default: null,
+ },
+ },
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ };
+ },
+ methods: {
+ createNewItem(type) {
+ this.modalType = type;
+ this.openModal = true;
+ },
+ hideModal() {
+ this.openModal = false;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="repo-new-btn pull-right">
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-sm btn-default dropdown-toggle add-to-tree"
+ data-toggle="dropdown"
+ aria-label="Create new file or directory"
+ >
+ <icon
+ name="plus"
+ :size="12"
+ css-classes="pull-left"
+ />
+ <icon
+ name="arrow-down"
+ :size="12"
+ css-classes="pull-left"
+ />
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('blob')"
+ >
+ {{ __('New file') }}
+ </a>
+ </li>
+ <li>
+ <upload
+ :branch-id="branch"
+ :path="path"
+ :parent="parent"
+ />
+ </li>
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('tree')"
+ >
+ {{ __('New directory') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ <new-modal
+ v-if="openModal"
+ :type="modalType"
+ :branch-id="branch"
+ :path="path"
+ :parent="parent"
+ @hide="hideModal"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index ac1f613bb71..0312f56efbd 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,10 +1,18 @@
<script>
- import { mapActions } from 'vuex';
+ import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
- import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
+ import modal from '../../../vue_shared/components/modal.vue';
export default {
props: {
+ branchId: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ default: null,
+ },
type: {
type: String,
required: true,
@@ -20,7 +28,7 @@
};
},
components: {
- popupDialog,
+ modal,
},
methods: {
...mapActions([
@@ -28,17 +36,23 @@
]),
createEntryInStore() {
this.createTempEntry({
+ projectId: this.currentProjectId,
+ branchId: this.branchId,
+ parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
- this.toggleModalOpen();
+ this.hideModal();
},
- toggleModalOpen() {
- this.$emit('toggle');
+ hideModal() {
+ this.$emit('hide');
},
},
computed: {
+ ...mapState([
+ 'currentProjectId',
+ ]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
@@ -68,11 +82,11 @@
</script>
<template>
- <popup-dialog
+ <modal
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
- @toggle="toggleModalOpen"
+ @cancel="hideModal"
@submit="createEntryInStore"
>
<form
@@ -94,5 +108,5 @@
</div>
</fieldset>
</form>
- </popup-dialog>
+ </modal>
</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 14ad32f4ae0..2a2f2a241fc 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -1,12 +1,22 @@
<script>
- import { mapActions } from 'vuex';
+ import { mapActions, mapState } from 'vuex';
export default {
props: {
- path: {
+ branchId: {
type: String,
required: true,
},
+ parent: {
+ type: Object,
+ default: null,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'trees',
+ 'currentProjectId',
+ ]),
},
methods: {
...mapActions([
@@ -22,6 +32,9 @@
this.createTempEntry({
name,
+ projectId: this.currentProjectId,
+ branchId: this.branchId,
+ parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
@@ -42,6 +55,9 @@
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
+ startFileUpload() {
+ this.$refs.fileUpload.click();
+ },
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
@@ -53,16 +69,19 @@
</script>
<template>
- <label
- role="button"
- class="menu-item"
- >
- {{ __('Upload file') }}
+ <div>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="startFileUpload"
+ >
+ {{ __('Upload file') }}
+ </a>
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
- </label>
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d3344d0c8dc..979721dcb5a 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -2,12 +2,12 @@
import { mapGetters, mapState, mapActions } from 'vuex';
import tooltip from '../../vue_shared/directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
-import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import modal from '../../vue_shared/components/modal.vue';
import commitFilesList from './commit_sidebar/list.vue';
export default {
components: {
- PopupDialog,
+ modal,
icon,
commitFilesList,
},
@@ -16,16 +16,17 @@ export default {
},
data() {
return {
- showNewBranchDialog: false,
+ showNewBranchModal: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
- collapsed: true,
};
},
computed: {
...mapState([
- 'currentBranch',
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
@@ -42,12 +43,13 @@ export default {
'checkCommitStatus',
'commitChanges',
'getTreeData',
+ 'setPanelCollapsedStatus',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
const payload = {
- branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
+ branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
@@ -55,16 +57,21 @@ export default {
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
- start_branch: createNewBranch ? this.currentBranch : undefined,
+ start_branch: createNewBranch ? this.currentBranchId : undefined,
};
- this.showNewBranchDialog = false;
+ this.showNewBranchModal = false;
this.submitCommitsLoading = true;
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
- this.getTreeData();
+ this.$store.dispatch('getTreeData', {
+ projectId: this.currentProjectId,
+ branch: this.currentBranchId,
+ endpoint: `/tree/${this.currentBranchId}`,
+ force: true,
+ });
})
.catch(() => {
this.submitCommitsLoading = false;
@@ -76,7 +83,7 @@ export default {
this.checkCommitStatus()
.then((branchChanged) => {
if (branchChanged) {
- this.showNewBranchDialog = true;
+ this.showNewBranchModal = true;
} else {
this.makeCommit();
}
@@ -86,50 +93,36 @@ export default {
});
},
toggleCollapsed() {
- this.collapsed = !this.collapsed;
+ this.setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !this.rightPanelCollapsed,
+ });
},
},
};
</script>
<template>
-<div
- class="multi-file-commit-panel"
- :class="{
- 'is-collapsed': collapsed,
- }"
->
- <popup-dialog
- v-if="showNewBranchDialog"
+<div class="multi-file-commit-panel-section">
+ <modal
+ v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
- @toggle="showNewBranchDialog = false"
+ @cancel="showNewBranchModal = false"
@submit="makeCommit(true)"
/>
- <button
- v-if="collapsed"
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
- @click="toggleCollapsed"
- >
- <i
- aria-hidden="true"
- class="fa fa-angle-double-left"
- >
- </i>
- </button>
<commit-files-list
title="Staged"
:file-list="changedFiles"
- :collapsed="collapsed"
+ :collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
- v-if="!collapsed"
+ v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue
index 6c1bb4b8566..42d5d709209 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/ide/components/repo_edit_button.vue
@@ -1,10 +1,10 @@
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
-import popupDialog from '../../vue_shared/components/popup_dialog.vue';
+import modal from '../../vue_shared/components/modal.vue';
export default {
components: {
- popupDialog,
+ modal,
},
computed: {
...mapState([
@@ -43,14 +43,14 @@ export default {
{{buttonLabel}}
</span>
</button>
- <popup-dialog
+ <modal
v-if="discardPopupOpen"
class="text-left"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
- @toggle="closeDiscardPopup"
+ @cancel="closeDiscardPopup"
@submit="toggleEditMode(true)"
/>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f37cbd1e961..343fd0a5300 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
<script>
/* global monaco */
-import { mapGetters, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@@ -24,6 +24,9 @@ export default {
...mapActions([
'getRawFileData',
'changeFileContent',
+ 'setFileLanguage',
+ 'setEditorPosition',
+ 'setFileEOL',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@@ -43,12 +46,36 @@ export default {
const model = this.editor.createModel(this.activeFile);
this.editor.attachModel(model);
+
model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
content: m.getValue(),
});
});
+
+ // Handle Cursor Position
+ this.editor.onPositionChange((instance, e) => {
+ this.setEditorPosition({
+ editorRow: e.position.lineNumber,
+ editorColumn: e.position.column,
+ });
+ });
+
+ this.editor.setPosition({
+ lineNumber: this.activeFile.editorRow,
+ column: this.activeFile.editorColumn,
+ });
+
+ // Handle File Language
+ this.setFileLanguage({
+ fileLanguage: model.language,
+ });
+
+ // Get File eol
+ this.setFileEOL({
+ eol: model.eol,
+ });
},
},
watch: {
@@ -57,12 +84,28 @@ export default {
this.initMonaco();
}
},
+ leftPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
+ rightPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
+ panelResizing(isResizing) {
+ if (isResizing === false) {
+ this.editor.updateDimensions();
+ }
+ },
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
+ ...mapState([
+ 'leftPanelCollapsed',
+ 'rightPanelCollapsed',
+ 'panelResizing',
+ ]),
shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw;
},
@@ -76,13 +119,14 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-show="shouldHideEditor"
+ v-if="shouldHideEditor"
v-html="activeFile.html"
>
</div>
<div
v-show="!shouldHideEditor"
ref="editor"
+ class="multi-file-editor-holder"
>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 75787ad6103..c8b0441d81c 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -1,7 +1,9 @@
<script>
- import { mapActions, mapGetters } from 'vuex';
+ import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
+ import newDropdown from './new_dropdown/index.vue';
+ import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
mixins: [
@@ -9,26 +11,28 @@
],
components: {
skeletonLoadingContainer,
+ newDropdown,
+ fileIcon,
},
props: {
file: {
type: Object,
required: true,
},
+ showExtraColumns: {
+ type: Boolean,
+ default: false,
+ },
},
computed: {
- ...mapGetters([
- 'isCollapsed',
+ ...mapState([
+ 'leftPanelCollapsed',
]),
isSubmodule() {
return this.file.type === 'submodule';
},
- fileIcon() {
- return {
- 'fa-spinner fa-spin': this.file.loading,
- [this.file.icon]: !this.file.loading,
- 'fa-folder-open': !this.file.loading && this.file.opened,
- };
+ isTree() {
+ return this.file.type === 'tree';
},
levelIndentation() {
return {
@@ -39,13 +43,39 @@
return this.file.id.substr(0, 8);
},
submoduleColSpan() {
- return !this.isCollapsed && this.isSubmodule ? 3 : 1;
+ return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
+ },
+ fileClass() {
+ if (this.file.type === 'blob') {
+ if (this.file.active) {
+ return 'file-open file-active';
+ }
+ return this.file.opened ? 'file-open' : '';
+ }
+ return '';
+ },
+ changedClass() {
+ return {
+ 'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
+ };
},
},
methods: {
- ...mapActions([
- 'clickedTreeRow',
- ]),
+ clickFile(row) {
+ // Manual Action if a tree is selected/opened
+ if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
+ this.$store.dispatch('toggleTreeOpen', {
+ endpoint: this.file.url,
+ tree: this.file,
+ });
+ }
+ this.$router.push(`/project${row.url}`);
+ },
+ },
+ updated() {
+ if (this.file.type === 'blob' && this.file.active) {
+ this.$el.scrollIntoView();
+ }
},
};
</script>
@@ -53,24 +83,39 @@
<template>
<tr
class="file"
- @click.prevent="clickedTreeRow(file)">
+ :class="fileClass"
+ @click="clickFile(file)">
<td
class="multi-file-table-name"
:colspan="submoduleColSpan"
>
- <i
- class="fa fa-fw file-icon"
- :class="fileIcon"
- :style="levelIndentation"
- aria-hidden="true"
- >
- </i>
<a
- :href="file.url"
class="repo-file-name"
>
+ <file-icon
+ :file-name="file.name"
+ :loading="file.loading"
+ :folder="file.type === 'tree'"
+ :opened="file.opened"
+ :style="levelIndentation"
+ :size="16"
+ >
+ </file-icon>
{{ file.name }}
</a>
+ <new-dropdown
+ v-if="isTree"
+ :project-id="file.projectId"
+ :branch="file.branchId"
+ :path="file.path"
+ :parent="file"/>
+ <i
+ class="fa"
+ v-if="changedClass"
+ :class="changedClass"
+ aria-hidden="true"
+ >
+ </i>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
@@ -84,7 +129,7 @@
</template>
</td>
- <template v-if="!isCollapsed && !isSubmodule">
+ <template v-if="showExtraColumns && !isSubmodule">
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a
v-if="file.lastCommit.message"
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
index 34f0d51819a..34f0d51819a 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
index 8fa637d771f..7eb840c7608 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -1,5 +1,5 @@
<script>
- import { mapGetters } from 'vuex';
+ import { mapState } from 'vuex';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
@@ -7,8 +7,8 @@
skeletonLoadingContainer,
},
computed: {
- ...mapGetters([
- 'isCollapsed',
+ ...mapState([
+ 'leftPanelCollapsed',
]),
},
};
@@ -24,7 +24,7 @@
:small="true"
/>
</td>
- <template v-if="!isCollapsed">
+ <template v-if="!leftPanelCollapsed">
<td
class="hidden-sm hidden-xs">
<skeleton-loading-container
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/ide/components/repo_prev_directory.vue
index a2b305bbd05..7cd359ea4ed 100644
--- a/app/assets/javascripts/repo/components/repo_prev_directory.vue
+++ b/app/assets/javascripts/ide/components/repo_prev_directory.vue
@@ -1,16 +1,14 @@
<script>
- import { mapGetters, mapState, mapActions } from 'vuex';
+ import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState([
'parentTreeUrl',
- ]),
- ...mapGetters([
- 'isCollapsed',
+ 'leftPanelCollapsed',
]),
colSpanCondition() {
- return this.isCollapsed ? undefined : 3;
+ return this.leftPanelCollapsed ? undefined : 3;
},
},
methods: {
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue
index 6ce9267f598..3d1e0297bd5 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/ide/components/repo_preview.vue
@@ -1,6 +1,7 @@
<script>
-/* global LineHighlighter */
import { mapGetters } from 'vuex';
+import LineHighlighter from '../../line_highlighter';
+import syntaxHighlight from '../../syntax_highlight';
export default {
computed: {
@@ -13,7 +14,7 @@ export default {
},
methods: {
highlightFile() {
- $(this.$el).find('.file-content').syntaxHighlight();
+ syntaxHighlight($(this.$el).find('.file-content'));
},
},
mounted() {
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index fb29a60df66..e7684884b2c 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
+import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
props: {
@@ -8,7 +9,9 @@ export default {
required: true,
},
},
-
+ components: {
+ fileIcon,
+ },
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
@@ -27,16 +30,18 @@ export default {
methods: {
...mapActions([
- 'setFileActive',
'closeFile',
]),
+ clickFile(tab) {
+ this.$router.push(`/project${tab.url}`);
+ },
},
};
</script>
<template>
<li
- @click="setFileActive(tab)"
+ @click="clickFile(tab)"
>
<button
type="button"
@@ -61,6 +66,11 @@ export default {
:class="{active : tab.active }"
:title="tab.url"
>
+ <file-icon
+ :file-name="tab.name"
+ :size="16"
+ >
+ </file-icon>
{{ tab.name }}
</div>
</li>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index ab0bef4f0ac..ab0bef4f0ac 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
new file mode 100644
index 00000000000..a9cbf8e370f
--- /dev/null
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import store from './stores';
+import flash from '../flash';
+import {
+ getTreeEntry,
+} from './stores/utils';
+
+Vue.use(VueRouter);
+
+/**
+ * Routes below /-/ide/:
+
+/project/h5bp/html5-boilerplate/blob/master
+/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
+
+/project/h5bp/html5-boilerplate/mr/123
+/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
+
+/workspace/123
+/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
+/workspace/project/h5bp/html5-boilerplate/mr/123
+
+/ = /workspace
+
+/settings
+*/
+
+// Unfortunately Vue Router doesn't work without at least a fake component
+// If you do only data handling
+const EmptyRouterComponent = {
+ render(createElement) {
+ return createElement('div');
+ },
+};
+
+const router = new VueRouter({
+ mode: 'history',
+ base: `${gon.relative_url_root}/-/ide/`,
+ routes: [
+ {
+ path: '/project/:namespace/:project',
+ component: EmptyRouterComponent,
+ children: [
+ {
+ path: ':targetmode/:branch/*',
+ component: EmptyRouterComponent,
+ },
+ {
+ path: 'mr/:mrid',
+ component: EmptyRouterComponent,
+ },
+ ],
+ },
+ ],
+});
+
+router.beforeEach((to, from, next) => {
+ if (to.params.namespace && to.params.project) {
+ store.dispatch('getProjectData', {
+ namespace: to.params.namespace,
+ projectId: to.params.project,
+ })
+ .then(() => {
+ const fullProjectId = `${to.params.namespace}/${to.params.project}`;
+
+ if (to.params.branch) {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: to.params.branch,
+ });
+
+ store.dispatch('getTreeData', {
+ projectId: fullProjectId,
+ branch: to.params.branch,
+ endpoint: `/tree/${to.params.branch}`,
+ })
+ .then(() => {
+ if (to.params[0]) {
+ const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
+ if (treeEntry) {
+ store.dispatch('handleTreeEntryAction', treeEntry);
+ }
+ }
+ })
+ .catch((e) => {
+ flash('Error while loading the branch files. Please try again.');
+ throw e;
+ });
+ }
+ })
+ .catch((e) => {
+ flash('Error while loading the project data. Please try again.');
+ throw e;
+ });
+ }
+
+ next();
+});
+
+export default router;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
new file mode 100644
index 00000000000..e8a19f47cee
--- /dev/null
+++ b/app/assets/javascripts/ide/index.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import ide from './components/ide.vue';
+import store from './stores';
+import router from './ide_router';
+import Translate from '../vue_shared/translate';
+
+function initIde(el) {
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ store,
+ router,
+ components: {
+ ide,
+ },
+ render(createElement) {
+ return createElement('ide', {
+ props: {
+ emptyStateSvgPath: el.dataset.emptyStateSvgPath,
+ },
+ });
+ },
+ });
+}
+
+const ideElement = document.getElementById('ide');
+
+Vue.use(Translate);
+
+initIde(ideElement);
diff --git a/app/assets/javascripts/repo/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js
index 84b29bdb600..84b29bdb600 100644
--- a/app/assets/javascripts/repo/lib/common/disposable.js
+++ b/app/assets/javascripts/ide/lib/common/disposable.js
diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 23c4811e6c0..14d9fe4771e 100644
--- a/app/assets/javascripts/repo/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -28,6 +28,14 @@ export default class Model {
return this.model.uri.toString();
}
+ get language() {
+ return this.model.getModeId();
+ }
+
+ get eol() {
+ return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
+ }
+
get path() {
return this.file.path;
}
diff --git a/app/assets/javascripts/repo/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index fd462252795..fd462252795 100644
--- a/app/assets/javascripts/repo/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
diff --git a/app/assets/javascripts/repo/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 0954b7973c4..0954b7973c4 100644
--- a/app/assets/javascripts/repo/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index dc0b1c95e59..dc0b1c95e59 100644
--- a/app/assets/javascripts/repo/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
diff --git a/app/assets/javascripts/repo/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
index 0e37f5c4704..0e37f5c4704 100644
--- a/app/assets/javascripts/repo/lib/diff/diff.js
+++ b/app/assets/javascripts/ide/lib/diff/diff.js
diff --git a/app/assets/javascripts/repo/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index e74c4046330..e74c4046330 100644
--- a/app/assets/javascripts/repo/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index db499444402..51e202b9348 100644
--- a/app/assets/javascripts/repo/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -22,6 +22,11 @@ export default class Editor {
this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this),
);
+
+ this.debouncedUpdate = _.debounce(() => {
+ this.updateDimensions();
+ }, 200);
+ window.addEventListener('resize', this.debouncedUpdate, false);
}
createInstance(domElement) {
@@ -32,6 +37,9 @@ export default class Editor {
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
+ minimap: {
+ enabled: false,
+ },
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
@@ -70,10 +78,32 @@ export default class Editor {
dispose() {
this.disposable.dispose();
+ window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance
if (this.instance) {
this.instance = null;
}
}
+
+ updateDimensions() {
+ this.instance.layout();
+ }
+
+ setPosition({ lineNumber, column }) {
+ this.instance.revealPositionInCenter({
+ lineNumber,
+ column,
+ });
+ this.instance.setPosition({
+ lineNumber,
+ column,
+ });
+ }
+
+ onPositionChange(cb) {
+ this.disposable.add(
+ this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
+ );
+ }
}
diff --git a/app/assets/javascripts/repo/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 701affc466e..701affc466e 100644
--- a/app/assets/javascripts/repo/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
diff --git a/app/assets/javascripts/repo/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
index af83a1ec0b4..af83a1ec0b4 100644
--- a/app/assets/javascripts/repo/monaco_loader.js
+++ b/app/assets/javascripts/ide/monaco_loader.js
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/ide/services/index.js
index 994d325e991..1fb24e93f2e 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -23,8 +23,11 @@ export default {
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
- getBranchData(projectId, currentBranch) {
- return Api.branchSingle(projectId, currentBranch);
+ getProjectData(namespace, project) {
+ return Api.project(`${namespace}/${project}`);
+ },
+ getBranchData(projectId, currentBranchId) {
+ return Api.branchSingle(projectId, currentBranchId);
},
createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
new file mode 100644
index 00000000000..335882bb6d7
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -0,0 +1,183 @@
+import Vue from 'vue';
+import { visitUrl } from '../../lib/utils/url_utility';
+import flash from '../../flash';
+import service from '../services';
+import * as types from './mutation_types';
+
+export const redirectToUrl = (_, url) => visitUrl(url);
+
+export const setInitialData = ({ commit }, data) =>
+ commit(types.SET_INITIAL_DATA, data);
+
+export const closeDiscardPopup = ({ commit }) =>
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+
+export const discardAllChanges = ({ commit, getters, dispatch }) => {
+ const changedFiles = getters.changedFiles;
+
+ changedFiles.forEach((file) => {
+ commit(types.DISCARD_FILE_CHANGES, file);
+
+ if (file.tempFile) {
+ dispatch('closeFile', { file, force: true });
+ }
+ });
+};
+
+export const closeAllFiles = ({ state, dispatch }) => {
+ state.openFiles.forEach(file => dispatch('closeFile', { file }));
+};
+
+export const toggleEditMode = (
+ { state, commit, getters, dispatch },
+ force = false,
+) => {
+ const changedFiles = getters.changedFiles;
+
+ if (changedFiles.length && !force) {
+ commit(types.TOGGLE_DISCARD_POPUP, true);
+ } else {
+ commit(types.TOGGLE_EDIT_MODE);
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+ dispatch('toggleBlobView');
+
+ if (!state.editMode) {
+ dispatch('discardAllChanges');
+ }
+ }
+};
+
+export const toggleBlobView = ({ commit, state }) => {
+ if (state.editMode) {
+ commit(types.SET_EDIT_MODE);
+ } else {
+ commit(types.SET_PREVIEW_MODE);
+ }
+};
+
+export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
+ if (side === 'left') {
+ commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
+ } else {
+ commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
+ }
+};
+
+export const setResizingStatus = ({ commit }, resizing) => {
+ commit(types.SET_RESIZING_STATUS, resizing);
+};
+
+export const checkCommitStatus = ({ state }) =>
+ service
+ .getBranchData(state.currentProjectId, state.currentBranchId)
+ .then((data) => {
+ const { id } = data.commit;
+ const selectedBranch =
+ state.projects[state.currentProjectId].branches[state.currentBranchId];
+
+ if (selectedBranch.workingReference !== id) {
+ return true;
+ }
+
+ return false;
+ })
+ .catch(() => flash('Error checking branch data. Please try again.'));
+
+export const commitChanges = (
+ { commit, state, dispatch, getters },
+ { payload, newMr },
+) =>
+ service
+ .commit(state.currentProjectId, payload)
+ .then((data) => {
+ const { branch } = payload;
+ if (!data.short_id) {
+ flash(data.message);
+ return;
+ }
+
+ const selectedProject = state.projects[state.currentProjectId];
+ const lastCommit = {
+ commit_path: `${selectedProject.web_url}/commit/${data.id}`,
+ commit: {
+ message: data.message,
+ authored_date: data.committed_date,
+ },
+ };
+
+ flash(
+ `Your changes have been committed. Commit ${data.short_id} with ${
+ data.stats.additions
+ } additions, ${data.stats.deletions} deletions.`,
+ 'notice',
+ );
+
+ if (newMr) {
+ dispatch(
+ 'redirectToUrl',
+ `${
+ selectedProject.web_url
+ }/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
+ );
+ } else {
+ commit(types.SET_BRANCH_WORKING_REFERENCE, {
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ reference: data.id,
+ });
+
+ getters.changedFiles.forEach((entry) => {
+ commit(types.SET_LAST_COMMIT_DATA, {
+ entry,
+ lastCommit,
+ });
+ });
+
+ dispatch('discardAllChanges');
+ dispatch('closeAllFiles');
+
+ window.scrollTo(0, 0);
+ }
+ })
+ .catch(() => flash('Error committing changes. Please try again.'));
+
+export const createTempEntry = (
+ { state, dispatch },
+ { projectId, branchId, parent, name, type, content = '', base64 = false },
+) => {
+ const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
+ if (type === 'tree') {
+ dispatch('createTempTree', {
+ projectId,
+ branchId,
+ parent: selectedParent,
+ name,
+ });
+ } else if (type === 'blob') {
+ dispatch('createTempFile', {
+ projectId,
+ branchId,
+ parent: selectedParent,
+ name,
+ base64,
+ content,
+ });
+ }
+};
+
+export const scrollToTab = () => {
+ Vue.nextTick(() => {
+ const tabs = document.getElementById('tabs');
+
+ if (tabs) {
+ const tabEl = tabs.querySelector('.active .repo-tab');
+
+ tabEl.focus();
+ }
+ });
+};
+
+export * from './actions/tree';
+export * from './actions/file';
+export * from './actions/project';
+export * from './actions/branch';
diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js
new file mode 100644
index 00000000000..32bdf7fec22
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/branch.js
@@ -0,0 +1,43 @@
+import service from '../../services';
+import flash from '../../../flash';
+import * as types from '../mutation_types';
+
+export const getBranchData = (
+ { commit, state, dispatch },
+ { projectId, branchId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if ((typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId])
+ || force) {
+ service.getBranchData(`${projectId}`, branchId)
+ .then((data) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading branch data. Please try again.');
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+});
+
+export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
+ state.currentProjectId,
+ {
+ branch,
+ ref: state.currentBranchId,
+ },
+)
+.then(res => res.json())
+.then((data) => {
+ const branchName = data.name;
+ const url = location.href.replace(state.currentBranchId, branchName);
+
+ if (this.$router) this.$router.push(url);
+
+ commit(types.SET_CURRENT_BRANCH, branchName);
+});
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 5bae4fa826a..0f27d5bf1c3 100644
--- a/app/assets/javascripts/repo/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
+import router from '../../ide_router';
import {
findEntry,
- pushState,
setPageTitle,
createTemp,
findIndexOfFile,
@@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) {
- pushState(file.parentTreeUrl);
+ router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
@@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
// reset hash for line highlighting
location.hash = '';
+
+ commit(types.SET_CURRENT_PROJECT, file.projectId);
+ commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
@@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file);
-
- pushState(file.url);
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
@@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
-export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
+export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
+ commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
+};
+
+export const setFileEOL = ({ state, commit }, { eol }) => {
+ commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
+};
+
+export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
+ commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
+};
+
+export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
+ const path = parent.path !== undefined ? parent.path : '';
+ // We need to do the replacement otherwise the web_url + file.url duplicate
+ const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
- name: name.replace(`${state.path}/`, ''),
- path: tree.path,
+ projectId,
+ branchId,
+ name: name.replace(`${path}/`, ''),
+ path,
type: 'blob',
- level: tree.level !== undefined ? tree.level + 1 : 0,
+ level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
+ url: newUrl,
});
- if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
+ if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
commit(types.CREATE_TMP_FILE, {
- parent: tree,
+ parent,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
@@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten
dispatch('toggleEditMode', true);
}
+ router.push(`/project${file.url}`);
+
return Promise.resolve(file);
};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
new file mode 100644
index 00000000000..02d4bd87ab0
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -0,0 +1,27 @@
+import service from '../../services';
+import flash from '../../../flash';
+import * as types from '../mutation_types';
+
+// eslint-disable-next-line import/prefer-default-export
+export const getProjectData = (
+ { commit, state, dispatch },
+ { namespace, projectId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
+ commit(types.TOGGLE_LOADING, state);
+ service.getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then((data) => {
+ commit(types.TOGGLE_LOADING, state);
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading project data. Please try again.');
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
new file mode 100644
index 00000000000..25909400a75
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -0,0 +1,188 @@
+import { visitUrl } from '../../../lib/utils/url_utility';
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import router from '../../ide_router';
+import {
+ setPageTitle,
+ findEntry,
+ createTemp,
+ createOrMergeEntry,
+} from '../utils';
+
+export const getTreeData = (
+ { commit, state, dispatch },
+ { endpoint, tree = null, projectId, branch, force = false } = {},
+) => new Promise((resolve, reject) => {
+ // We already have the base tree so we resolve immediately
+ if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
+ resolve();
+ } else {
+ if (tree) commit(types.TOGGLE_LOADING, tree);
+ const selectedProject = state.projects[projectId];
+ // We are merging the web_url that we got on the project info with the endpoint
+ // we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
+ const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
+ if (completeEndpoint && (!tree || !tree.tempFile)) {
+ service.getTreeData(completeEndpoint)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ if (!state.isInitialRoot) {
+ commit(types.SET_ROOT, data.path === '/');
+ }
+
+ dispatch('updateDirectoryData', { data, tree, projectId, branch });
+ const selectedTree = tree || state.trees[`${projectId}/${branch}`];
+
+ commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
+ if (tree) commit(types.TOGGLE_LOADING, selectedTree);
+
+ const prevLastCommitPath = selectedTree.lastCommitPath;
+ if (prevLastCommitPath !== null) {
+ dispatch('getLastCommitData', selectedTree);
+ }
+ resolve(data);
+ })
+ .catch((e) => {
+ flash('Error loading tree data. Please try again.');
+ if (tree) commit(types.TOGGLE_LOADING, tree);
+ reject(e);
+ });
+ } else {
+ resolve();
+ }
+ }
+});
+
+export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
+ if (tree.opened) {
+ // send empty data to clear the tree
+ const data = { trees: [], blobs: [], submodules: [] };
+
+ dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
+ } else {
+ dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
+ }
+
+ commit(types.TOGGLE_TREE_OPEN, tree);
+};
+
+export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
+ if (row.type === 'tree') {
+ dispatch('toggleTreeOpen', {
+ endpoint: row.url,
+ tree: row,
+ });
+ } else if (row.type === 'submodule') {
+ commit(types.TOGGLE_LOADING, row);
+ visitUrl(row.url);
+ } else if (row.type === 'blob' && row.opened) {
+ dispatch('setFileActive', row);
+ } else {
+ dispatch('getFileData', row);
+ }
+};
+
+export const createTempTree = (
+ { state, commit, dispatch },
+ { projectId, branchId, parent, name },
+) => {
+ let selectedTree = parent;
+ const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
+
+ dirNames.forEach((dirName) => {
+ const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
+
+ if (!foundEntry) {
+ const path = selectedTree.path !== undefined ? selectedTree.path : '';
+ const tmpEntry = createTemp({
+ projectId,
+ branchId,
+ name: dirName,
+ path,
+ type: 'tree',
+ level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
+ tree: [],
+ url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
+ });
+
+ commit(types.CREATE_TMP_TREE, {
+ parent: selectedTree,
+ tmpEntry,
+ });
+ commit(types.TOGGLE_TREE_OPEN, tmpEntry);
+
+ router.push(`/project${tmpEntry.url}`);
+
+ selectedTree = tmpEntry;
+ } else {
+ selectedTree = foundEntry;
+ }
+ });
+};
+
+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.'));
+};
+
+export const updateDirectoryData = (
+ { commit, state },
+ { data, tree, projectId, branch },
+) => {
+ if (!tree) {
+ const existingTree = state.trees[`${projectId}/${branch}`];
+ if (!existingTree) {
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
+ }
+ }
+
+ const selectedTree = tree || state.trees[`${projectId}/${branch}`];
+ const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+ const createEntry = (entry, type) => createOrMergeEntry({
+ tree: selectedTree,
+ projectId: `${projectId}`,
+ branchId: branch,
+ entry,
+ level,
+ type,
+ parentTreeUrl,
+ });
+
+ const formattedData = [
+ ...data.trees.map(t => createEntry(t, 'tree')),
+ ...data.submodules.map(m => createEntry(m, 'submodule')),
+ ...data.blobs.map(b => createEntry(b, 'blob')),
+ ];
+
+ commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
+};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
new file mode 100644
index 00000000000..6b51ccff817
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -0,0 +1,19 @@
+export const changedFiles = state => state.openFiles.filter(file => file.changed);
+
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
+
+export const activeFileExtension = (state) => {
+ const file = activeFile(state);
+ return file ? `.${file.path.split('.').pop()}` : '';
+};
+
+export const canEditFile = (state) => {
+ const currentActiveFile = activeFile(state);
+
+ return state.canCommit &&
+ (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
+};
+
+export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
+
+export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 6ac9bfd8189..6ac9bfd8189 100644
--- a/app/assets/javascripts/repo/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index bc3390f1506..69b218a5e7d 100644
--- a/app/assets/javascripts/repo/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -1,16 +1,28 @@
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
-export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
+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';
+
+// Project Mutation Types
+export const SET_PROJECT = 'SET_PROJECT';
+export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
+export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+
+// Branch Mutation Types
+export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
+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 CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
+export const CREATE_TREE = 'CREATE_TREE';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
@@ -18,6 +30,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
+export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
+export const SET_FILE_POSITION = 'SET_FILE_POSITION';
+export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
@@ -28,3 +43,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
+
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index ae2ba5bedf7..03d81be10a1 100644
--- a/app/assets/javascripts/repo/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,4 +1,5 @@
import * as types from './mutation_types';
+import projectMutations from './mutations/project';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -32,29 +33,37 @@ export default {
discardPopupOpen,
});
},
- [types.SET_COMMIT_REF](state, ref) {
- Object.assign(state, {
- currentRef: ref,
- });
- },
[types.SET_ROOT](state, isRoot) {
Object.assign(state, {
isRoot,
isInitialRoot: isRoot,
});
},
- [types.SET_PREVIOUS_URL](state, previousUrl) {
+ [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
+ Object.assign(state, {
+ leftPanelCollapsed: collapsed,
+ });
+ },
+ [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
+ Object.assign(state, {
+ rightPanelCollapsed: collapsed,
+ });
+ },
+ [types.SET_RESIZING_STATUS](state, resizing) {
Object.assign(state, {
- previousUrl,
+ panelResizing: resizing,
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, {
+ id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
});
},
+ ...projectMutations,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
new file mode 100644
index 00000000000..04b9582c5bb
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -0,0 +1,28 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_BRANCH](state, currentBranchId) {
+ Object.assign(state, {
+ currentBranchId,
+ });
+ },
+ [types.SET_BRANCH](state, { projectPath, branchName, branch }) {
+ // Add client side properties
+ Object.assign(branch, {
+ treeId: `${projectPath}/${branchName}`,
+ active: true,
+ workingReference: '',
+ });
+
+ Object.assign(state.projects[projectPath], {
+ branches: {
+ [branchName]: branch,
+ },
+ });
+ },
+ [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ workingReference: reference,
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index f9ba80b9dc2..5f3655b0092 100644
--- a/app/assets/javascripts/repo/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -6,6 +6,10 @@ export default {
Object.assign(file, {
active,
});
+
+ Object.assign(state, {
+ selectedFile: file,
+ });
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
@@ -42,6 +46,22 @@ export default {
changed,
});
},
+ [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
+ Object.assign(file, {
+ fileLanguage,
+ });
+ },
+ [types.SET_FILE_EOL](state, { file, eol }) {
+ Object.assign(file, {
+ eol,
+ });
+ },
+ [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
+ Object.assign(file, {
+ editorRow,
+ editorColumn,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
new file mode 100644
index 00000000000..2816562a919
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -0,0 +1,23 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_PROJECT](state, currentProjectId) {
+ Object.assign(state, {
+ currentProjectId,
+ });
+ },
+ [types.SET_PROJECT](state, { projectPath, project }) {
+ // Add client side properties
+ Object.assign(project, {
+ tree: [],
+ branches: {},
+ active: true,
+ });
+
+ Object.assign(state, {
+ projects: Object.assign({}, state.projects, {
+ [projectPath]: project,
+ }),
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 130221c9fda..4fe438ab465 100644
--- a/app/assets/javascripts/repo/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -6,6 +6,15 @@ export default {
opened: !tree.opened,
});
},
+ [types.CREATE_TREE](state, { treePath }) {
+ Object.assign(state, {
+ trees: Object.assign({}, state.trees, {
+ [treePath]: {
+ tree: [],
+ },
+ }),
+ });
+ },
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 0068834831e..61d12096946 100644
--- a/app/assets/javascripts/repo/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,10 +1,10 @@
export default () => ({
canCommit: false,
- currentBranch: '',
- currentBlobView: 'repo-preview',
- currentRef: '',
+ currentProjectId: '',
+ currentBranchId: '',
+ currentBlobView: 'repo-editor',
discardPopupOpen: false,
- editMode: false,
+ editMode: true,
endpoints: {},
isRoot: false,
isInitialRoot: false,
@@ -12,13 +12,12 @@ export default () => ({
loading: false,
onTopOfBranch: false,
openFiles: [],
+ selectedFile: null,
path: '',
- project: {
- id: 0,
- name: '',
- url: '',
- },
parentTreeUrl: '',
- previousUrl: '',
- tree: [],
+ trees: {},
+ projects: {},
+ leftPanelCollapsed: false,
+ rightPanelCollapsed: true,
+ panelResizing: false,
});
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index fae1f4439a9..29e3ab5d040 100644
--- a/app/assets/javascripts/repo/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -2,6 +2,8 @@ export const dataStructure = () => ({
id: '',
key: '',
type: '',
+ projectId: '',
+ branchId: '',
name: '',
url: '',
path: '',
@@ -15,9 +17,11 @@ export const dataStructure = () => ({
changed: false,
lastCommitPath: '',
lastCommit: {
+ id: '',
url: '',
message: '',
updatedAt: '',
+ author: '',
},
tree_url: '',
blamePath: '',
@@ -31,11 +35,17 @@ export const dataStructure = () => ({
parentTreeUrl: '',
renderError: false,
base64: false,
+ editorRow: 1,
+ editorColumn: 1,
+ fileLanguage: '',
+ eol: '',
});
export const decorateData = (entity) => {
const {
id,
+ projectId,
+ branchId,
type,
url,
name,
@@ -56,6 +66,8 @@ export const decorateData = (entity) => {
return {
...dataStructure(),
id,
+ projectId,
+ branchId,
key: `${name}-${type}-${id}`,
type,
name,
@@ -75,24 +87,51 @@ export const decorateData = (entity) => {
};
};
-export const findEntry = (state, type, name) => state.tree.find(
+/*
+ Takes the multi-dimensional tree and returns a flattened array.
+ This allows for the table to recursively render the table rows but keeps the data
+ structure nested to make it easier to add new files/directories.
+*/
+export const treeList = (state, treeId) => {
+ const baseTree = state.trees[treeId];
+ if (baseTree) {
+ const mapTree = arr => (!arr.tree || !arr.tree.length ?
+ [] : _.map(arr.tree, a => [a, mapTree(a)]));
+
+ return _.chain(baseTree.tree)
+ .map(arr => [arr, mapTree(arr)])
+ .flatten()
+ .value();
+ }
+ return [];
+};
+
+export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
+
+export const getTreeEntry = (store, treeId, path) => {
+ const fileList = treeList(store.state, treeId);
+ return fileList ? fileList.find(file => file.path === path) : null;
+};
+
+export const findEntry = (tree, type, name) => tree.find(
f => f.type === type && f.name === name,
);
+
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
document.title = title;
};
-export const pushState = (url) => {
- history.pushState({ url }, '', url);
-};
-
-export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
+export const createTemp = ({
+ projectId, branchId, name, path, type, level, changed, content, base64, url,
+}) => {
const treePath = path ? `${path}/${name}` : name;
return decorateData({
id: new Date().getTime().toString(),
+ projectId,
+ branchId,
name,
type,
tempFile: true,
@@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
level,
base64,
renderError: base64,
+ url,
});
};
-export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
- const found = findEntry(tree, type, entry.name);
+export const createOrMergeEntry = ({ tree,
+ projectId,
+ branchId,
+ entry,
+ type,
+ parentTreeUrl,
+ level }) => {
+ const found = findEntry(tree.tree || tree, type, entry.name);
if (found) {
return Object.assign({}, found, {
@@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level })
return decorateData({
...entry,
+ projectId,
+ branchId,
type,
parentTreeUrl,
level,
diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js
index 6a6a668308d..eddaeda9578 100644
--- a/app/assets/javascripts/image_diff/helpers/badge_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js
@@ -19,12 +19,9 @@ export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
}
export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
- const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']);
- const iconEl = document.createElement('i');
- iconEl.className = 'fa fa-comment-o';
- iconEl.setAttribute('aria-label', 'comment');
+ const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge']);
+ buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark');
- buttonEl.appendChild(iconEl);
containerEl.appendChild(buttonEl);
}
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index ada693afc46..e61b37a2d1f 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,8 +1,9 @@
/* eslint-disable no-new */
-/* global MilestoneSelect */
+
+import MilestoneSelect from './milestone_select';
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
-/* global Sidebar */
+import Sidebar from './right_sidebar';
import DueDateSelectors from './due_date_select';
@@ -15,5 +16,5 @@ export default () => {
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
new DueDateSelectors();
- window.sidebar = new Sidebar();
+ Sidebar.initialize();
};
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
index 2cbb70220d0..b6ff97d1279 100644
--- a/app/assets/javascripts/init_legacy_filters.js
+++ b/app/assets/javascripts/init_legacy_filters.js
@@ -1,9 +1,9 @@
/* eslint-disable no-new */
import LabelsSelect from './labels_select';
-/* global MilestoneSelect */
import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
+import MilestoneSelect from './milestone_select';
export default () => {
new UsersSelect();
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
index 3a8b4360cb6..882aedfcc76 100644
--- a/app/assets/javascripts/init_notes.js
+++ b/app/assets/javascripts/init_notes.js
@@ -1,4 +1,4 @@
-/* global Notes */
+import Notes from './notes';
export default () => {
const dataEl = document.querySelector('.js-notes-data');
@@ -10,5 +10,7 @@ export default () => {
autocomplete,
} = JSON.parse(dataEl.innerHTML);
- window.notes = new Notes(notesUrl, notesIds, now, diffView, autocomplete);
+ // 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);
};
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index ba2b6737988..2056efe701b 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,8 +1,7 @@
/* eslint-disable class-methods-use-this, no-new */
-/* global MilestoneSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-import './milestone_select';
+import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
@@ -21,7 +20,7 @@ export default class IssuableBulkUpdateSidebar {
}
initDomElements() {
- this.$page = $('.page-with-sidebar');
+ this.$page = $('.layout-page');
this.$sidebar = $('.right-sidebar');
this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 7de07e9403d..411c820cc43 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper';
export default class Issue {
constructor() {
- if ($('a.btn-close').length) {
- this.taskList = new TaskList({
- dataType: 'issue',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: (result) => {
- document.querySelector('#task_status').innerText = result.task_status;
- document.querySelector('#task_status_short').innerText = result.task_status_short;
- }
- });
- this.initIssueBtnEventListeners();
- }
+ if ($('a.btn-close').length) this.initIssueBtnEventListeners();
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
@@ -59,7 +48,7 @@ export default class Issue {
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
- const isClosedBadge = $('div.status-box-closed');
+ const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 5bdc7c99503..fc10a43d1bf 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,5 +1,6 @@
<script>
import Visibility from 'visibilityjs';
+import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
@@ -8,7 +9,7 @@ import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
-import '../../lib/utils/url_utility';
+import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
props: {
@@ -31,7 +32,7 @@ export default {
showInlineEditButton: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
showDeleteButton: {
type: Boolean,
@@ -149,6 +150,11 @@ export default {
editedComponent,
formComponent,
},
+
+ mixins: [
+ recaptchaModalImplementor,
+ ],
+
methods: {
openForm() {
if (!this.showForm) {
@@ -164,34 +170,49 @@ export default {
closeForm() {
this.showForm = false;
},
+
updateIssuable() {
- this.service.updateIssuable(this.store.formState)
- .then(res => res.json())
+ return this.service.updateIssuable(this.store.formState)
+ .then(res => res.data)
+ .then(data => this.checkForSpam(data))
.then((data) => {
if (location.pathname !== data.web_url) {
- gl.utils.visitUrl(data.web_url);
+ visitUrl(data.web_url);
}
return this.service.getData();
})
- .then(res => res.json())
+ .then(res => res.data)
.then((data) => {
this.store.updateState(data);
eventHub.$emit('close.form');
})
- .catch(() => {
- eventHub.$emit('close.form');
- window.Flash(`Error updating ${this.issuableType}`);
+ .catch((error) => {
+ if (error && error.name === 'SpamError') {
+ this.openRecaptcha();
+ } else {
+ eventHub.$emit('close.form');
+ window.Flash(`Error updating ${this.issuableType}`);
+ }
});
},
+
+ closeRecaptchaModal() {
+ this.store.setFormState({
+ updateLoading: false,
+ });
+
+ this.closeRecaptcha();
+ },
+
deleteIssuable() {
this.service.deleteIssuable()
- .then(res => res.json())
+ .then(res => res.data)
.then((data) => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
- gl.utils.visitUrl(data.web_url);
+ visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
@@ -204,7 +225,7 @@ export default {
this.poll = new Poll({
resource: this.service,
method: 'getData',
- successCallback: res => res.json().then(data => this.store.updateState(data)),
+ successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
@@ -237,9 +258,9 @@ export default {
</script>
<template>
- <div>
+<div>
+ <div v-if="canUpdate && showForm">
<form-component
- v-if="canUpdate && showForm"
:form-state="formState"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
@@ -251,30 +272,37 @@ export default {
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
- <div v-else>
- <title-component
- :issuable-ref="issuableRef"
- :can-update="canUpdate"
- :title-html="state.titleHtml"
- :title-text="state.titleText"
- :show-inline-edit-button="showInlineEditButton"
- />
- <description-component
- v-if="state.descriptionHtml"
- :can-update="canUpdate"
- :description-html="state.descriptionHtml"
- :description-text="state.descriptionText"
- :updated-at="state.updatedAt"
- :task-status="state.taskStatus"
- :issuable-type="issuableType"
- :update-url="updateEndpoint"
- />
- <edited-component
- v-if="hasUpdated"
- :updated-at="state.updatedAt"
- :updated-by-name="state.updatedByName"
- :updated-by-path="state.updatedByPath"
- />
- </div>
+
+ <recaptcha-modal
+ v-show="showRecaptcha"
+ :html="recaptchaHTML"
+ @close="closeRecaptchaModal"
+ />
+ </div>
+ <div v-else>
+ <title-component
+ :issuable-ref="issuableRef"
+ :can-update="canUpdate"
+ :title-html="state.titleHtml"
+ :title-text="state.titleText"
+ :show-inline-edit-button="showInlineEditButton"
+ />
+ <description-component
+ v-if="state.descriptionHtml"
+ :can-update="canUpdate"
+ :description-html="state.descriptionHtml"
+ :description-text="state.descriptionText"
+ :updated-at="state.updatedAt"
+ :task-status="state.taskStatus"
+ :issuable-type="issuableType"
+ :update-url="updateEndpoint"
+ />
+ <edited-component
+ v-if="hasUpdated"
+ :updated-at="state.updatedAt"
+ :updated-by-name="state.updatedByName"
+ :updated-by-path="state.updatedByPath"
+ />
</div>
+</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index b7559ced946..c3f2bf130bb 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,9 +1,14 @@
<script>
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
+ import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
- mixins: [animateMixin],
+ mixins: [
+ animateMixin,
+ recaptchaModalImplementor,
+ ],
+
props: {
canUpdate: {
type: Boolean,
@@ -51,6 +56,7 @@
this.updateTaskStatusText();
},
},
+
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
@@ -61,9 +67,19 @@
dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
+ onSuccess: this.taskListUpdateSuccess.bind(this),
});
}
},
+
+ taskListUpdateSuccess(data) {
+ try {
+ this.checkForSpam(data);
+ } catch (error) {
+ if (error && error.name === 'SpamError') this.openRecaptcha();
+ }
+ },
+
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
@@ -109,5 +125,11 @@
:data-update-url="updateUrl"
>
</textarea>
+
+ <recaptcha-modal
+ v-show="showRecaptcha"
+ :html="recaptchaHTML"
+ @close="closeRecaptcha"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 52fe4ecd08b..4e577546551 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -53,7 +53,7 @@
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
- data-supports-quick-actionss="false"
+ data-supports-quick-actions="false"
aria-label="Description"
v-model="formState.description"
ref="textarea"
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index a363d06d950..b7e6eadd440 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -79,7 +79,7 @@
v-tooltip
v-if="showInlineEditButton && canUpdate"
type="button"
- class="btn btn-default btn-edit btn-svg"
+ class="btn btn-default btn-edit btn-svg js-issuable-edit"
v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index a21ce41e65e..75dfdedcf1b 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import eventHub from './event_hub';
import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
@@ -7,12 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
- $('.issuable-edit').on('click', (e) => {
- e.preventDefault();
-
- eventHub.$emit('open.form');
- });
-
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
index 6f0fd0b1768..9546eb22c27 100644
--- a/app/assets/javascripts/issue_show/services/index.js
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -1,29 +1,20 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class Service {
constructor(endpoint) {
- this.endpoint = endpoint;
-
- this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
- realtimeChanges: {
- method: 'GET',
- url: `${this.endpoint}/realtime_changes`,
- },
- });
+ this.endpoint = `${endpoint}.json`;
+ this.realtimeEndpoint = `${endpoint}/realtime_changes`;
}
getData() {
- return this.resource.realtimeChanges();
+ return axios.get(this.realtimeEndpoint);
}
deleteIssuable() {
- return this.resource.delete();
+ return axios.delete(this.endpoint);
}
updateIssuable(data) {
- return this.resource.update(data);
+ return axios.put(this.endpoint, data);
}
}
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index 85ea6330ee9..8f32dcc94e2 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -1,7 +1,9 @@
import _ from 'underscore';
+import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
-import { bytesToKiB } from './lib/utils/number_utils';
+import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
+import { timeFor } from './lib/utils/datetime_utility';
export default class Job {
constructor(options) {
@@ -94,14 +96,15 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this
canScroll() {
- return this.$document.height() > this.$window.height();
+ return $(document).height() > $(window).height();
}
toggleScroll() {
- const currentPosition = this.$document.scrollTop();
- const scrollHeight = this.$document.height();
+ const $document = $(document);
+ const currentPosition = $document.scrollTop();
+ const scrollHeight = $document.height();
- const windowHeight = this.$window.height();
+ const windowHeight = $(window).height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
@@ -125,18 +128,22 @@ export default class Job {
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
}
-
+ // eslint-disable-next-line class-methods-use-this
isScrolledToBottom() {
- const currentPosition = this.$document.scrollTop();
- const scrollHeight = this.$document.height();
+ const $document = $(document);
+
+ const currentPosition = $document.scrollTop();
+ const scrollHeight = $document.height();
+
+ const windowHeight = $(window).height();
- const windowHeight = this.$window.height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this
scrollDown() {
- this.$document.scrollTop(this.$document.height());
+ const $document = $(document);
+ $document.scrollTop($document.height());
}
scrollToBottom() {
@@ -146,7 +153,7 @@ export default class Job {
}
scrollToTop() {
- this.$document.scrollTop(0);
+ $(document).scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
}
@@ -191,7 +198,7 @@ export default class Job {
// we need to show a message warning the user about that.
if (this.logBytes < log.total) {
// size is in bytes, we need to calculate KiB
- const size = bytesToKiB(this.logBytes);
+ const size = numberToHumanSize(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
} else {
@@ -209,7 +216,7 @@ export default class Job {
}
if (log.status !== this.buildStatus) {
- gl.utils.visitUrl(this.pagePath);
+ visitUrl(this.pagePath);
}
})
.fail(() => {
@@ -260,7 +267,7 @@ export default class Job {
if ($date.length) {
const date = $date.text();
return $date.text(
- gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
+ timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3'))),
);
}
}
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 6d671845f8e..c660828b30e 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -30,6 +30,9 @@
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
+ jobStarted() {
+ return this.job.started;
+ },
},
methods: {
getActions() {
@@ -63,8 +66,9 @@
:time="job.created_at"
:user="job.user"
:actions="actions"
- :hasSidebarButton="true"
- />
+ :has-sidebar-button="true"
+ :should-render-triggered-label="jobStarted"
+ />
<loading-icon
v-if="isLoading"
size="2"
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index a6f82b247e2..ab3cc29146a 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,59 +1,51 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
-import _ from 'underscore';
-import Cookies from 'js-cookie';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
-(function() {
- var hideEndFade;
+function hideEndFade($scrollingTabs) {
+ $scrollingTabs.each(function scrollTabsLoop() {
+ const $this = $(this);
+ $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
+ });
+}
- hideEndFade = function($scrollingTabs) {
- return $scrollingTabs.each(function() {
- var $this;
- $this = $(this);
- return $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
- });
- };
+export default function initLayoutNav() {
+ const contextualSidebar = new ContextualSidebar();
+ contextualSidebar.bindEvents();
+
+ initFlyOutNav();
$(document).on('init.scrolling-tabs', () => {
const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
$scrollingTabs.addClass('is-initialized');
- hideEndFade($scrollingTabs);
- $(window).off('resize.nav').on('resize.nav', function() {
- return hideEndFade($scrollingTabs);
- });
- $scrollingTabs.off('scroll').on('scroll', function(event) {
- var $this, currentPosition, maxPosition;
- $this = $(this);
- currentPosition = $this.scrollLeft();
- maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+ $(window).on('resize.nav', () => {
+ hideEndFade($scrollingTabs);
+ }).trigger('resize.nav');
+
+ $scrollingTabs.on('scroll', function tabsScrollEvent() {
+ const $this = $(this);
+ const currentPosition = $this.scrollLeft();
+ const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
- return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
+ $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
});
- $scrollingTabs.each(function () {
- var $this = $(this);
- var scrollingTabWidth = $this.width();
- var $active = $this.find('.active');
- var activeWidth = $active.width();
+ $scrollingTabs.each(function scrollTabsEachLoop() {
+ const $this = $(this);
+ const scrollingTabWidth = $this.width();
+ const $active = $this.find('.active');
+ const activeWidth = $active.width();
if ($active.length) {
- var offset = $active.offset().left + activeWidth;
+ const offset = $active.offset().left + activeWidth;
if (offset > scrollingTabWidth - 30) {
- var scrollLeft = scrollingTabWidth / 2;
- scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
+ const scrollLeft = (offset - (scrollingTabWidth / 2)) - (activeWidth / 2);
+
$this.scrollLeft(scrollLeft);
}
}
});
- });
-
- $(() => {
- const contextualSidebar = new ContextualSidebar();
- contextualSidebar.bindEvents();
-
- initFlyOutNav();
- });
-}).call(window);
+ }).trigger('init.scrolling-tabs');
+}
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 7aeeca3b283..8aff0556011 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -2,6 +2,8 @@ import axios from 'axios';
import csrf from './csrf';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
+// Used by Rails to check if it is a valid XHR request
+axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb
diff --git a/app/assets/javascripts/lib/utils/cache.js b/app/assets/javascripts/lib/utils/cache.js
index 3141f1eeafc..596bd1e388a 100644
--- a/app/assets/javascripts/lib/utils/cache.js
+++ b/app/assets/javascripts/lib/utils/cache.js
@@ -1,4 +1,4 @@
-class Cache {
+export default class Cache {
constructor() {
this.internalStorage = { };
}
@@ -15,5 +15,3 @@ class Cache {
delete this.internalStorage[key];
}
}
-
-export default Cache;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 33cc807912c..03918762842 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,3 +1,4 @@
+import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
@@ -65,7 +66,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
export const handleLocationHash = () => {
- let hash = window.gl.utils.getLocationHash();
+ let hash = getLocationHash();
if (!hash) return;
// This is required to handle non-unicode characters in hash
@@ -231,7 +232,7 @@ export const nodeMatchesSelector = (node, selector) => {
export const normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
- Object.keys(headers).forEach((e) => {
+ Object.keys(headers || {}).forEach((e) => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 7a72509d234..9a61003ef30 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,3 +1,2 @@
-/* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024;
export const HIDDEN_CLASS = 'hidden';
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index d0578b230b1..1fa6715180e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,132 +1,141 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
-
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
-
import {
- lang,
+ languageCode,
s__,
} from '../../locale';
window.timeago = timeago;
window.dateFormat = dateFormat;
-(function() {
- (function(w) {
- var base;
- var timeagoInstance;
+/**
+ * Given a date object returns the day of the week in English
+ * @param {date} date
+ * @returns {String}
+ */
+export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+/**
+ * @example
+ * dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000"
+ * @param {date} datetime
+ * @returns {String}
+ */
+export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- w.gl.utils.formatDate = function(datetime) {
- return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- };
+/**
+ * Timeago uses underscores instead of dashes to separate language from country code.
+ *
+ * see https://github.com/hustcc/timeago.js/tree/v3.0.0/locales
+ */
+const timeagoLanguageCode = languageCode().replace(/-/g, '_');
- w.gl.utils.getDayName = function(date) {
- return this.days[date.getDay()];
+let timeagoInstance;
+
+/**
+ * Sets a timeago Instance
+ */
+export function getTimeago() {
+ if (!timeagoInstance) {
+ const localeRemaining = function getLocaleRemaining(number, index) {
+ return [
+ [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
+ [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
+ [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
+ [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|%s days ago'), s__('Timeago|%s days remaining')],
+ [s__('Timeago|a 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|%s months ago'), s__('Timeago|%s months remaining')],
+ [s__('Timeago|a 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|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
+ [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
+ [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
+ [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|%s days ago'), s__('Timeago|in %s days')],
+ [s__('Timeago|a 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|%s months ago'), s__('Timeago|in %s months')],
+ [s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
+ [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
+ ][index];
};
- w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
- $timeagoEls.each((i, el) => {
- if (setTimeago) {
- // 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>'
- });
- }
+ timeago.register(timeagoLanguageCode, locale);
+ timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
+ timeagoInstance = timeago();
+ }
- el.classList.add('js-timeago-render');
- });
+ return timeagoInstance;
+}
- gl.utils.renderTimeago($timeagoEls);
- };
+/**
+ * For the given element, renders a timeago instance.
+ * @param {jQuery} $els
+ */
+export const renderTimeago = ($els) => {
+ const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
- w.gl.utils.getTimeago = function() {
- var locale;
-
- if (!timeagoInstance) {
- const localeRemaining = function(number, index) {
- return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
- [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
- [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
- [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|%s days ago'), s__('Timeago|%s days remaining')],
- [s__('Timeago|a 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|%s months ago'), s__('Timeago|%s months remaining')],
- [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')],
- [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')]
- ][index];
- };
- locale = function(number, index) {
- return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
- [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
- [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
- [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|%s days ago'), s__('Timeago|in %s days')],
- [s__('Timeago|a 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|%s months ago'), s__('Timeago|in %s months')],
- [s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
- [s__('Timeago|%s years ago'), s__('Timeago|in %s years')]
- ][index];
- };
-
- timeago.register(lang, locale);
- timeago.register(`${lang}-remaining`, localeRemaining);
- timeagoInstance = timeago();
- }
-
- return timeagoInstance;
- };
+ // timeago.js sets timeouts internally for each timeago value to be updated in real time
+ getTimeago().render(timeagoEls, timeagoLanguageCode);
+};
- w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
- var timefor;
- if (!time) {
- return '';
- }
- if (new Date(time) < new Date()) {
- expiredLabel || (expiredLabel = s__('Timeago|Past due'));
- timefor = expiredLabel;
- } else {
- timefor = gl.utils.getTimeago().format(time, `${lang}-remaining`).trim();
- }
- return timefor;
- };
+/**
+ * For the given elements, sets a tooltip with a formatted date.
+ * @param {jQuery}
+ * @param {Boolean} setTimeago
+ */
+export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
+ $timeagoEls.each((i, el) => {
+ if (setTimeago) {
+ // 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>',
+ });
+ }
- w.gl.utils.renderTimeago = function($els) {
- const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
+ el.classList.add('js-timeago-render');
+ });
- // timeago.js sets timeouts internally for each timeago value to be updated in real time
- gl.utils.getTimeago().render(timeagoEls, lang);
- };
+ renderTimeago($timeagoEls);
+};
- w.gl.utils.getDayDifference = function(a, b) {
- var millisecondsPerDay = 1000 * 60 * 60 * 24;
- var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
- var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+/**
+ * Returns remaining or passed time over the given time.
+ * @param {*} time
+ * @param {*} expiredLabel
+ */
+export const timeFor = (time, expiredLabel) => {
+ if (!time) {
+ return '';
+ }
+ if (new Date(time) < new Date()) {
+ return expiredLabel || s__('Timeago|Past due');
+ }
+ return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim();
+};
- return Math.floor((date2 - date1) / millisecondsPerDay);
- };
- })(window);
-}).call(window);
+export const getDayDifference = (a, b) => {
+ const millisecondsPerDay = 1000 * 60 * 60 * 24;
+ const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+};
/**
* Port of ruby helper time_interval_in_words.
@@ -162,3 +171,10 @@ export function dateInWords(date, abbreviated = false) {
return `${monthName} ${date.getDate()}, ${year}`;
}
+
+window.gl = window.gl || {};
+window.gl.utils = {
+ ...(window.gl.utils || {}),
+ getTimeago,
+ localTimeAgo,
+};
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 9280b7f150c..cb6e06ea584 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength -
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
+
+/**
+ * Replaces all html tags from a string with the given replacement.
+ *
+ * @param {String} string
+ * @param {*} replace
+ * @returns {String}
+ */
+export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
diff --git a/app/assets/javascripts/lib/utils/tick_formats.js b/app/assets/javascripts/lib/utils/tick_formats.js
new file mode 100644
index 00000000000..0c10a85e336
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/tick_formats.js
@@ -0,0 +1,39 @@
+import { createDateTimeFormat } from '../../locale';
+
+let dateTimeFormats;
+
+export const initDateFormats = () => {
+ const dayFormat = createDateTimeFormat({ month: 'short', day: 'numeric' });
+ const monthFormat = createDateTimeFormat({ month: 'long' });
+ const yearFormat = createDateTimeFormat({ year: 'numeric' });
+
+ dateTimeFormats = {
+ dayFormat,
+ monthFormat,
+ yearFormat,
+ };
+};
+
+initDateFormats();
+
+/**
+ Formats a localized date in way that it can be used for d3.js axis.tickFormat().
+
+ That is, it displays
+ - 4-digit for first of January
+ - full month name for first of every month
+ - day and abbreviated month otherwise
+
+ see also https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#tickFormat
+ */
+export const dateTickFormat = (date) => {
+ if (date.getDate() !== 1) {
+ return dateTimeFormats.dayFormat.format(date);
+ }
+
+ if (date.getMonth() > 0) {
+ return dateTimeFormats.monthFormat.format(date);
+ }
+
+ return dateTimeFormats.yearFormat.format(date);
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 17236c91490..a266bb6771f 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,93 +1,69 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
-
-var base;
-var w = window;
-if (w.gl == null) {
- w.gl = {};
-}
-if ((base = w.gl).utils == null) {
- base.utils = {};
-}
// Returns an array containing the value(s) of the
// of the key passed as an argument
-w.gl.utils.getParameterValues = function(sParam) {
- var i, sPageURL, sParameterName, sURLVariables, values;
- sPageURL = decodeURIComponent(window.location.search.substring(1));
- sURLVariables = sPageURL.split('&');
- sParameterName = void 0;
- values = [];
- i = 0;
- while (i < sURLVariables.length) {
- sParameterName = sURLVariables[i].split('=');
+export function getParameterValues(sParam) {
+ const sPageURL = decodeURIComponent(window.location.search.substring(1));
+
+ return sPageURL.split('&').reduce((acc, urlParam) => {
+ const sParameterName = urlParam.split('=');
+
if (sParameterName[0] === sParam) {
- values.push(sParameterName[1].replace(/\+/g, ' '));
+ acc.push(sParameterName[1].replace(/\+/g, ' '));
}
- i += 1;
- }
- return values;
-};
+
+ return acc;
+ }, []);
+}
+
// @param {Object} params - url keys and value to merge
// @param {String} url
-w.gl.utils.mergeUrlParams = function(params, url) {
- var lastChar, newUrl, paramName, paramValue, pattern;
- newUrl = decodeURIComponent(url);
- for (paramName in params) {
- paramValue = params[paramName];
- pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
- if (paramValue == null) {
- newUrl = newUrl.replace(pattern, '');
+export function mergeUrlParams(params, url) {
+ let newUrl = Object.keys(params).reduce((acc, paramName) => {
+ const paramValue = encodeURIComponent(params[paramName]);
+ const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
+
+ if (paramValue === null) {
+ return acc.replace(pattern, '');
} else if (url.search(pattern) !== -1) {
- newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
- } else {
- newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
+ return acc.replace(pattern, `$1${paramValue}$2`);
}
- }
+
+ return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`;
+ }, decodeURIComponent(url));
+
// Remove a trailing ampersand
- lastChar = newUrl[newUrl.length - 1];
+ const lastChar = newUrl[newUrl.length - 1];
+
if (lastChar === '&') {
newUrl = newUrl.slice(0, -1);
}
+
return newUrl;
-};
-// removes parameter query string from url. returns the modified url
-w.gl.utils.removeParamQueryString = function(url, param) {
- var urlVariables, variables;
- url = decodeURIComponent(url);
- urlVariables = url.split('&');
- return ((function() {
- var j, len, results;
- results = [];
- for (j = 0, len = urlVariables.length; j < len; j += 1) {
- variables = urlVariables[j];
- if (variables.indexOf(param) === -1) {
- results.push(variables);
- }
- }
- return results;
- })()).join('&');
-};
-w.gl.utils.removeParams = (params) => {
+}
+
+export function removeParamQueryString(url, param) {
+ const decodedUrl = decodeURIComponent(url);
+ const urlVariables = decodedUrl.split('&');
+
+ return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
+}
+
+export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
+
params.forEach((param) => {
- url.search = w.gl.utils.removeParamQueryString(url.search, param);
+ url.search = removeParamQueryString(url.search, param);
});
+
return url.href;
-};
-w.gl.utils.getLocationHash = function(url) {
- var hashIndex;
- if (typeof url === 'undefined') {
- // Note: We can't use window.location.hash here because it's
- // not consistent across browsers - Firefox will pre-decode it
- url = window.location.href;
- }
- hashIndex = url.indexOf('#');
- return hashIndex === -1 ? null : url.substring(hashIndex + 1);
-};
+}
-w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);
+export function getLocationHash(url = window.location.href) {
+ const hashIndex = url.indexOf('#');
+
+ return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+}
-// eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) {
if (external) {
// Simulate `target="blank" rel="noopener noreferrer"`
@@ -100,12 +76,10 @@ export function visitUrl(url, external = false) {
}
}
+export function refreshCurrentPage() {
+ visitUrl(window.location.href);
+}
+
export function redirectTo(url) {
return window.location.assign(url);
}
-
-window.gl = window.gl || {};
-window.gl.utils = {
- ...(window.gl.utils || {}),
- visitUrl,
-};
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index a75d1a4b8d0..fbd381d8ff7 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -175,4 +175,4 @@ LineHighlighter.prototype.__setLocationHash__ = function(value) {
}, document.title, value);
};
-window.LineHighlighter = LineHighlighter;
+export default LineHighlighter;
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 1003b9ba0af..2f4328b56e1 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -1,8 +1,7 @@
import Jed from 'jed';
import sprintf from './sprintf';
-const langAttribute = document.querySelector('html').getAttribute('lang');
-const lang = (langAttribute || 'en').replace(/-/g, '_');
+const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en';
const locale = new Jed(window.translations || {});
delete window.translations;
@@ -47,9 +46,19 @@ const pgettext = (keyOrContext, key) => {
return translated[translated.length - 1];
};
-export { lang };
+/**
+ Creates an instance of Intl.DateTimeFormat for the current locale.
+
+ @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+ @returns {Intl.DateTimeFormat}
+*/
+const createDateTimeFormat =
+ formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions);
+
+export { languageCode };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
export { sprintf };
+export { createDateTimeFormat };
export default locale;
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index dcc0fa63b63..59bfa482bb0 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global ConfirmDangerModal */
-/* global Aside */
import jQuery from 'jquery';
import _ from 'underscore';
@@ -28,50 +27,28 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
-import './lib/utils/datetime_utility';
-import './lib/utils/url_utility';
+import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility';
+import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// behaviors
import './behaviors/';
// everything else
-import './activities';
-import './admin';
-import './aside';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
-import './commits';
-import './compare';
-import './compare_autocomplete';
import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
-import './gl_field_error';
-import './gl_field_errors';
-import './gl_form';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
-import './layout_nav';
+import initLayoutNav from './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import initLogoAnimation from './logo';
-import './merge_request';
-import './merge_request_tabs';
import './milestone_select';
-import './notes';
-import './notifications_dropdown';
-import './notifications_form';
-import './pager';
import './preview_markdown';
-import './project_find_file';
-import './project_import';
import './projects_dropdown';
-import './projects_list';
-import './syntax_highlight';
import './render_gfm';
-import './right_sidebar';
-import './search';
-import './search_autocomplete';
import initBreadcrumbs from './breadcrumb';
import './dispatcher';
@@ -112,6 +89,7 @@ $(function () {
var fitSidebarForSize;
initBreadcrumbs();
+ initLayoutNav();
initImporterStatus();
initTodoToggle();
initLogoAnimation();
@@ -122,13 +100,13 @@ $(function () {
// `hashchange` is not triggered when link target is already in window.location
$body.on('click', 'a[href^="#"]', function() {
var href = this.getAttribute('href');
- if (href.substr(1) === gl.utils.getLocationHash()) {
+ if (href.substr(1) === getLocationHash()) {
setTimeout(handleLocationHash, 1);
}
});
if (bootstrapBreakpoint === 'xs') {
- const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar');
+ const $rightSidebar = $('aside.right-sidebar, .layout-page');
$rightSidebar
.removeClass('right-sidebar-expanded')
@@ -188,13 +166,13 @@ $(function () {
trigger: 'focus',
// set the viewport to the main content, excluding the navigation bar, so
// the navigation can't overlap the popover
- viewport: '.page-with-sidebar'
+ viewport: '.layout-page'
});
$('.trigger-submit').on('change', function () {
return $(this).parents('form').submit();
// Form submitter
});
- gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ localTimeAgo($('abbr.timeago, .js-timeago'), true);
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
@@ -281,11 +259,8 @@ $(function () {
return fitSidebarForSize();
});
loadAwardsHandler();
- new Aside();
- gl.utils.renderTimeago();
-
- $(document).trigger('init.scrolling-tabs');
+ renderTimeago();
$('form.filter-form').on('submit', function (event) {
const link = document.createElement('a');
@@ -294,7 +269,7 @@ $(function () {
const action = `${this.action}${link.search === '' ? '?' : '&'}`;
event.preventDefault();
- gl.utils.visitUrl(`${action}${$(this).serialize()}`);
+ visitUrl(`${action}${$(this).serialize()}`);
});
const flashContainer = document.querySelector('.flash-container');
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 17591829b76..94561d6b7c3 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -10,6 +10,7 @@ import './mixins/line_conflict_actions';
import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
+import syntaxHighlight from '../syntax_highlight';
$(() => {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
@@ -53,7 +54,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
- $('.js-syntax-highlight').syntaxHighlight();
+ syntaxHighlight($('.js-syntax-highlight'));
});
});
},
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index a9c08df4f93..cb3cdea8111 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,148 +1,143 @@
/* 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 */
-/* global MergeRequestTabs */
import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
-import './merge_request_tabs';
+import MergeRequestTabs from './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
import { addDelimiter } from './lib/utils/text_utility';
-(function() {
- this.MergeRequest = (function() {
- function MergeRequest(opts) {
- // Initialize MergeRequest behavior
- //
- // Options:
- // action - String, current controller action
- //
- this.opts = opts != null ? opts : {};
- this.submitNoteForm = this.submitNoteForm.bind(this);
- this.$el = $('.merge-request');
- this.$('.show-all-commits').on('click', (function(_this) {
- return function() {
- return _this.showAllCommits();
- };
- })(this));
-
- this.initTabs();
- this.initMRBtnListeners();
- this.initCommitMessageListeners();
- this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
-
- if ($("a.btn-close").length) {
- this.taskList = new TaskList({
- dataType: 'merge_request',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: (result) => {
- document.querySelector('#task_status').innerText = result.task_status;
- document.querySelector('#task_status_short').innerText = result.task_status_short;
- }
- });
- }
- }
-
- // Local jQuery finder
- MergeRequest.prototype.$ = function(selector) {
- return this.$el.find(selector);
+function MergeRequest(opts) {
+ // Initialize MergeRequest behavior
+ //
+ // Options:
+ // action - String, current controller action
+ //
+ this.opts = opts != null ? opts : {};
+ this.submitNoteForm = this.submitNoteForm.bind(this);
+ this.$el = $('.merge-request');
+ this.$('.show-all-commits').on('click', (function(_this) {
+ return function() {
+ return _this.showAllCommits();
};
-
- MergeRequest.prototype.initTabs = function() {
- if (window.mrTabs) {
- window.mrTabs.unbindEvents();
+ })(this));
+
+ this.initTabs();
+ this.initMRBtnListeners();
+ this.initCommitMessageListeners();
+ this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
+
+ if ($("a.btn-close").length) {
+ this.taskList = new TaskList({
+ dataType: 'merge_request',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: (result) => {
+ document.querySelector('#task_status').innerText = result.task_status;
+ document.querySelector('#task_status_short').innerText = result.task_status_short;
}
- window.mrTabs = new gl.MergeRequestTabs(this.opts);
- };
-
- MergeRequest.prototype.showAllCommits = function() {
- this.$('.first-commits').remove();
- return this.$('.all-commits').removeClass('hide');
- };
-
- MergeRequest.prototype.initMRBtnListeners = function() {
- var _this;
- _this = this;
- return $('a.btn-close, a.btn-reopen').on('click', function(e) {
- var $this, shouldSubmit;
- $this = $(this);
- shouldSubmit = $this.hasClass('btn-comment');
- if (shouldSubmit && $this.data('submitted')) {
- return;
- }
-
- if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
-
- if (shouldSubmit) {
- if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
- e.preventDefault();
- e.stopImmediatePropagation();
-
- _this.submitNoteForm($this.closest('form'), $this);
- }
- }
- });
- };
-
- MergeRequest.prototype.submitNoteForm = function(form, $button) {
- var noteText;
- noteText = form.find("textarea.js-note-text").val();
- if (noteText.trim().length > 0) {
- form.submit();
- $button.data('submitted', true);
- return $button.trigger('click');
- }
- };
-
- MergeRequest.prototype.initCommitMessageListeners = function() {
- $(document).on('click', 'a.js-with-description-link', function(e) {
- var textarea = $('textarea.js-commit-message');
- e.preventDefault();
+ });
+ }
+}
+
+// Local jQuery finder
+MergeRequest.prototype.$ = function(selector) {
+ return this.$el.find(selector);
+};
+
+MergeRequest.prototype.initTabs = function() {
+ if (window.mrTabs) {
+ window.mrTabs.unbindEvents();
+ }
+ window.mrTabs = new MergeRequestTabs(this.opts);
+};
+
+MergeRequest.prototype.showAllCommits = function() {
+ this.$('.first-commits').remove();
+ return this.$('.all-commits').removeClass('hide');
+};
+
+MergeRequest.prototype.initMRBtnListeners = function() {
+ var _this;
+ _this = this;
+ return $('a.btn-close, a.btn-reopen').on('click', function(e) {
+ var $this, shouldSubmit;
+ $this = $(this);
+ shouldSubmit = $this.hasClass('btn-comment');
+ if (shouldSubmit && $this.data('submitted')) {
+ return;
+ }
- textarea.val(textarea.data('messageWithDescription'));
- $('.js-with-description-hint').hide();
- $('.js-without-description-hint').show();
- });
+ if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
- $(document).on('click', 'a.js-without-description-link', function(e) {
- var textarea = $('textarea.js-commit-message');
+ if (shouldSubmit) {
+ if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
e.preventDefault();
+ e.stopImmediatePropagation();
- textarea.val(textarea.data('messageWithoutDescription'));
- $('.js-with-description-hint').show();
- $('.js-without-description-hint').hide();
- });
- };
-
- MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) {
- $('.detail-page-header .status-box')
- .removeClass(classToRemove)
- .addClass(classToAdd)
- .find('span')
- .text(newStatusText);
- };
-
- MergeRequest.prototype.decreaseCounter = function(by = 1) {
- const $el = $('.nav-links .js-merge-counter');
- const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
-
- $el.text(addDelimiter(count));
- };
-
- MergeRequest.prototype.hideCloseButton = function() {
- const el = document.querySelector('.merge-request .js-issuable-actions');
- const closeDropdownItem = el.querySelector('li.close-item');
- if (closeDropdownItem) {
- closeDropdownItem.classList.add('hidden');
- // Selects the next dropdown item
- el.querySelector('li.report-item').click();
- } else {
- // No dropdown just hide the Close button
- el.querySelector('.btn-close').classList.add('hidden');
+ _this.submitNoteForm($this.closest('form'), $this);
}
- // Dropdown for mobile screen
- el.querySelector('li.js-close-item').classList.add('hidden');
- };
-
- return MergeRequest;
- })();
-}).call(window);
+ }
+ });
+};
+
+MergeRequest.prototype.submitNoteForm = function(form, $button) {
+ var noteText;
+ noteText = form.find("textarea.js-note-text").val();
+ if (noteText.trim().length > 0) {
+ form.submit();
+ $button.data('submitted', true);
+ return $button.trigger('click');
+ }
+};
+
+MergeRequest.prototype.initCommitMessageListeners = function() {
+ $(document).on('click', 'a.js-with-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithDescription'));
+ $('.js-with-description-hint').hide();
+ $('.js-without-description-hint').show();
+ });
+
+ $(document).on('click', 'a.js-without-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithoutDescription'));
+ $('.js-with-description-hint').show();
+ $('.js-without-description-hint').hide();
+ });
+};
+
+MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) {
+ $('.detail-page-header .status-box')
+ .removeClass(classToRemove)
+ .addClass(classToAdd)
+ .find('span')
+ .text(newStatusText);
+};
+
+MergeRequest.prototype.decreaseCounter = function(by = 1) {
+ const $el = $('.nav-links .js-merge-counter');
+ const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
+
+ $el.text(addDelimiter(count));
+};
+
+MergeRequest.prototype.hideCloseButton = function() {
+ const el = document.querySelector('.merge-request .js-issuable-actions');
+ const closeDropdownItem = el.querySelector('li.close-item');
+ if (closeDropdownItem) {
+ closeDropdownItem.classList.add('hidden');
+ // Selects the next dropdown item
+ el.querySelector('li.report-item').click();
+ } else {
+ // No dropdown just hide the Close button
+ el.querySelector('.btn-close').classList.add('hidden');
+ }
+ // Dropdown for mobile screen
+ el.querySelector('li.js-close-item').classList.add('hidden');
+};
+
+export default MergeRequest;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 54c1b7a268e..acfc62fe5cb 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,5 +1,4 @@
/* eslint-disable no-new, class-methods-use-this */
-/* global notes */
import Cookies from 'js-cookie';
import Flash from './flash';
@@ -11,8 +10,12 @@ import {
handleLocationHash,
isMetaClick,
} from './lib/utils/common_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';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -60,387 +63,382 @@ import Diff from './diff';
//
/* eslint-enable max-len */
-(() => {
- // Store the `location` object, allowing for easier stubbing in tests
- let location = window.location;
+// Store the `location` object, allowing for easier stubbing in tests
+let location = window.location;
- class MergeRequestTabs {
+export default class MergeRequestTabs {
- constructor({ action, setUrl, stubLocation } = {}) {
- const mergeRequestTabs = document.querySelector('.js-tabs-affix');
- const navbar = document.querySelector('.navbar-gitlab');
- const paddingTop = 16;
+ constructor({ action, setUrl, stubLocation } = {}) {
+ const mergeRequestTabs = document.querySelector('.js-tabs-affix');
+ const navbar = document.querySelector('.navbar-gitlab');
+ const paddingTop = 16;
- this.diffsLoaded = false;
- this.pipelinesLoaded = false;
- this.commitsLoaded = false;
- this.fixedLayoutPref = null;
+ this.diffsLoaded = false;
+ this.pipelinesLoaded = false;
+ this.commitsLoaded = false;
+ this.fixedLayoutPref = null;
- 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.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
+ 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.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
- if (mergeRequestTabs) {
- this.stickyTop += mergeRequestTabs.offsetHeight;
- }
-
- if (stubLocation) {
- location = stubLocation;
- }
+ if (mergeRequestTabs) {
+ this.stickyTop += mergeRequestTabs.offsetHeight;
+ }
- this.bindEvents();
- this.activateTab(action);
- this.initAffix();
+ if (stubLocation) {
+ location = stubLocation;
}
- bindEvents() {
- $(document)
- .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .on('click', '.js-show-tab', this.showTab);
+ this.bindEvents();
+ this.activateTab(action);
+ this.initAffix();
+ }
- $('.merge-request-tabs a[data-toggle="tab"]')
- .on('click', this.clickTab);
- }
+ bindEvents() {
+ $(document)
+ .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .on('click', '.js-show-tab', this.showTab);
- // 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"]')
+ .on('click', this.clickTab);
+ }
- $('.merge-request-tabs a[data-toggle="tab"]')
- .off('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);
- destroyPipelinesView() {
- if (this.commitPipelinesTable) {
- this.commitPipelinesTable.$destroy();
- this.commitPipelinesTable = null;
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .off('click', this.clickTab);
+ }
- document.querySelector('#commit-pipeline-table-view').innerHTML = '';
- }
+ destroyPipelinesView() {
+ if (this.commitPipelinesTable) {
+ this.commitPipelinesTable.$destroy();
+ this.commitPipelinesTable = null;
+
+ document.querySelector('#commit-pipeline-table-view').innerHTML = '';
}
+ }
- showTab(e) {
+ showTab(e) {
+ e.preventDefault();
+ this.activateTab($(e.target).data('action'));
+ }
+
+ clickTab(e) {
+ if (e.currentTarget && isMetaClick(e)) {
+ const targetLink = e.currentTarget.getAttribute('href');
+ e.stopImmediatePropagation();
e.preventDefault();
- this.activateTab($(e.target).data('action'));
+ window.open(targetLink, '_blank');
}
+ }
- clickTab(e) {
- if (e.currentTarget && isMetaClick(e)) {
- const targetLink = e.currentTarget.getAttribute('href');
- e.stopImmediatePropagation();
- e.preventDefault();
- 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(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();
- }
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
- }
- this.destroyPipelinesView();
- } else if (action === 'pipelines') {
- this.resetViewContainer();
- this.mountPipelinesView();
- } else {
- if (bp.getBreakpointSize() !== 'xs') {
- this.expandView();
- }
- this.resetViewContainer();
- this.destroyPipelinesView();
-
- initDiscussionTab();
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
}
- if (this.setUrl) {
- this.setCurrentAction(action);
+ this.destroyPipelinesView();
+ } else if (action === 'pipelines') {
+ this.resetViewContainer();
+ this.mountPipelinesView();
+ } else {
+ if (bp.getBreakpointSize() !== 'xs') {
+ this.expandView();
}
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+
+ initDiscussionTab();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
}
+ }
- scrollToElement(container) {
- if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
- const $el = $(`${container} ${location.hash}:not(.match)`);
- if ($el.length) {
- $.scrollTo($el[0], { offset });
- }
+ scrollToElement(container) {
+ if (location.hash) {
+ const offset = 0 - (
+ $('.navbar-gitlab').outerHeight() +
+ $('.js-tabs-affix').outerHeight()
+ );
+ const $el = $(`${container} ${location.hash}:not(.match)`);
+ if ($el.length) {
+ $.scrollTo($el[0], { offset });
}
}
+ }
- // 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');
+ // 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
+ // `MergeRequests#show` route.
+ //
+ // Examples:
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ // setCurrentAction('diffs')
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('show')
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('commits')
+ // location.pathname # => "/namespace/project/merge_requests/1/commits"
+ //
+ // Returns the new URL String
+ setCurrentAction(action) {
+ this.currentAction = action;
+
+ // Remove a trailing '/commits' '/diffs' '/pipelines'
+ let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
+
+ // Append the new action if we're on a tab other than 'notes'
+ if (this.currentAction !== 'show' && this.currentAction !== 'new') {
+ newState += `/${this.currentAction}`;
}
- // 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
- // `MergeRequests#show` route.
- //
- // Examples:
- //
- // location.pathname # => "/namespace/project/merge_requests/1"
- // setCurrentAction('diffs')
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('show')
- // location.pathname # => "/namespace/project/merge_requests/1"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('commits')
- // location.pathname # => "/namespace/project/merge_requests/1/commits"
- //
- // Returns the new URL String
- setCurrentAction(action) {
- this.currentAction = action;
+ // Ensure parameters and hash come along for the ride
+ newState += location.search + location.hash;
- // Remove a trailing '/commits' '/diffs' '/pipelines'
- let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
+ // TODO: Consider refactoring in light of turbolinks removal.
- // Append the new action if we're on a tab other than 'notes'
- if (this.currentAction !== 'show' && this.currentAction !== 'new') {
- newState += `/${this.currentAction}`;
- }
+ // Replace the current history state with the new one without breaking
+ // Turbolinks' history.
+ //
+ // See https://github.com/rails/turbolinks/issues/363
+ window.history.replaceState({
+ url: newState,
+ }, document.title, newState);
- // Ensure parameters and hash come along for the ride
- newState += location.search + location.hash;
+ return newState;
+ }
- // TODO: Consider refactoring in light of turbolinks removal.
+ loadCommits(source) {
+ if (this.commitsLoaded) {
+ return;
+ }
+ this.ajaxGet({
+ url: `${source}.json`,
+ success: (data) => {
+ document.querySelector('div#commits').innerHTML = data.html;
+ localTimeAgo($('.js-timeago', 'div#commits'));
+ this.commitsLoaded = true;
+ this.scrollToElement('#commits');
+ },
+ });
+ }
- // Replace the current history state with the new one without breaking
- // Turbolinks' history.
- //
- // See https://github.com/rails/turbolinks/issues/363
- window.history.replaceState({
- url: newState,
- }, document.title, newState);
+ mountPipelinesView() {
+ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ const CommitPipelinesTable = gl.CommitPipelinesTable;
+ this.commitPipelinesTable = new CommitPipelinesTable({
+ propsData: {
+ endpoint: pipelineTableViewEl.dataset.endpoint,
+ helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
+ errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ },
+ }).$mount();
+
+ // $mount(el) replaces the el with the new rendered component. We need it in order to mount
+ // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
+ pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
+ }
- return newState;
+ loadDiff(source) {
+ if (this.diffsLoaded) {
+ document.dispatchEvent(new CustomEvent('scroll'));
+ return;
}
- loadCommits(source) {
- if (this.commitsLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- document.querySelector('div#commits').innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
- this.commitsLoaded = true;
- this.scrollToElement('#commits');
- },
- });
- }
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ const urlPathname = parseUrlPathname(source);
- mountPipelinesView() {
- const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const CommitPipelinesTable = gl.CommitPipelinesTable;
- this.commitPipelinesTable = new CommitPipelinesTable({
- propsData: {
- endpoint: pipelineTableViewEl.dataset.endpoint,
- helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
- emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
- errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
- autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
- },
- }).$mount();
+ this.ajaxGet({
+ url: `${urlPathname}.json${location.search}`,
+ success: (data) => {
+ const $container = $('#diffs');
+ $container.html(data.html);
- // $mount(el) replaces the el with the new rendered component. We need it in order to mount
- // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
- pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
- }
+ initChangesDropdown(this.stickyTop);
- loadDiff(source) {
- if (this.diffsLoaded) {
- document.dispatchEvent(new CustomEvent('scroll'));
- return;
- }
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
+ }
+
+ localTimeAgo($('.js-timeago', 'div#diffs'));
+ syntaxHighlight($('#diffs .js-syntax-highlight'));
- // We extract pathname for the current Changes tab anchor href
- // some pages like MergeRequestsController#new has query parameters on that anchor
- const urlPathname = parseUrlPathname(source);
-
- this.ajaxGet({
- url: `${urlPathname}.json${location.search}`,
- success: (data) => {
- const $container = $('#diffs');
- $container.html(data.html);
-
- initChangesDropdown(this.stickyTop);
-
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
-
- gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
- $('#diffs .js-syntax-highlight').syntaxHighlight();
-
- if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
- this.expandViewContainer();
- }
- this.diffsLoaded = true;
-
- new Diff();
- this.scrollToElement('#diffs');
-
- $('.diff-file').each((i, el) => {
- new BlobForkSuggestion({
- openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
- forkButtons: $(el).find('.js-fork-suggestion-button'),
- cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
- suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
- actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
- })
- .init();
+ if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
+ this.expandViewContainer();
+ }
+ this.diffsLoaded = true;
+
+ new Diff();
+ this.scrollToElement('#diffs');
+
+ $('.diff-file').each((i, el) => {
+ new BlobForkSuggestion({
+ openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
+ forkButtons: $(el).find('.js-fork-suggestion-button'),
+ cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
+ suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
+ actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
+ })
+ .init();
+ });
+
+ // Scroll any linked note into view
+ // Similar to `toggler_behavior` in the discussion tab
+ const hash = getLocationHash();
+ const anchor = hash && $container.find(`.note[id="${hash}"]`);
+ if (anchor && anchor.length > 0) {
+ const notesContent = anchor.closest('.notes_content');
+ const lineType = notesContent.hasClass('new') ? 'new' : 'old';
+ Notes.instance.toggleDiffNote({
+ target: anchor,
+ lineType,
+ forceShow: true,
});
+ anchor[0].scrollIntoView();
+ handleLocationHash();
+ // We have multiple elements on the page with `#note_xxx`
+ // (discussion and diff tabs) and `:target` only applies to the first
+ anchor.addClass('target');
+ }
+ },
+ });
+ }
- // Scroll any linked note into view
- // Similar to `toggler_behavior` in the discussion tab
- const hash = window.gl.utils.getLocationHash();
- const anchor = hash && $container.find(`.note[id="${hash}"]`);
- if (anchor && anchor.length > 0) {
- const notesContent = anchor.closest('.notes_content');
- const lineType = notesContent.hasClass('new') ? 'new' : 'old';
- notes.toggleDiffNote({
- target: anchor,
- lineType,
- forceShow: true,
- });
- anchor[0].scrollIntoView();
- handleLocationHash();
- // We have multiple elements on the page with `#note_xxx`
- // (discussion and diff tabs) and `:target` only applies to the first
- anchor.addClass('target');
- }
- },
- });
- }
+ // Show or hide the loading spinner
+ //
+ // status - Boolean, true to show, false to hide
+ toggleLoading(status) {
+ $('.mr-loading-status .loading').toggle(status);
+ }
- // Show or hide the loading spinner
- //
- // status - Boolean, true to show, false to hide
- toggleLoading(status) {
- $('.mr-loading-status .loading').toggle(status);
- }
+ ajaxGet(options) {
+ const defaults = {
+ beforeSend: () => this.toggleLoading(true),
+ error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ };
+ $.ajax($.extend({}, defaults, options));
+ }
- ajaxGet(options) {
- const defaults = {
- beforeSend: () => this.toggleLoading(true),
- error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
- complete: () => this.toggleLoading(false),
- dataType: 'json',
- type: 'GET',
- };
- $.ajax($.extend({}, defaults, options));
- }
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
- diffViewType() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- }
+ isDiffAction(action) {
+ return action === 'diffs' || action === 'new/diffs';
+ }
- isDiffAction(action) {
- return action === 'diffs' || action === 'new/diffs';
+ expandViewContainer() {
+ const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
+ if (this.fixedLayoutPref === null) {
+ this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
+ $wrapper.removeClass('container-limited');
+ }
- expandViewContainer() {
- const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
- if (this.fixedLayoutPref === null) {
- this.fixedLayoutPref = $wrapper.hasClass('container-limited');
- }
- $wrapper.removeClass('container-limited');
+ resetViewContainer() {
+ if (this.fixedLayoutPref !== null) {
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', this.fixedLayoutPref);
}
+ }
- resetViewContainer() {
- if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
- }
- }
+ shrinkView() {
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
- shrinkView() {
- const $gutterIcon = $('.js-sidebar-toggle i:visible');
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is expanded
+ if ($gutterIcon.is('.fa-angle-double-right')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
- // Wait until listeners are set
- setTimeout(() => {
- // Only when sidebar is expanded
- if ($gutterIcon.is('.fa-angle-double-right')) {
- $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
+ // Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView() {
+ if (Cookies.get('collapsed_gutter') === 'true') {
+ return;
}
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
- // Expand the issuable sidebar unless the user explicitly collapsed it
- expandView() {
- if (Cookies.get('collapsed_gutter') === 'true') {
- return;
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is collapsed
+ if ($gutterIcon.is('.fa-angle-double-left')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
}
- const $gutterIcon = $('.js-sidebar-toggle i:visible');
+ }, 0);
+ }
- // Wait until listeners are set
- setTimeout(() => {
- // Only when sidebar is collapsed
- if ($gutterIcon.is('.fa-angle-double-left')) {
- $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
- }
+ 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
+ if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
+
+ /**
+ 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: '' }));
- 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
- if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
-
- /**
- 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');
- }
+ // Fix bug when reloading the page already scrolling
+ if ($tabs.hasClass('affix')) {
+ $tabs.trigger('affix.bs.affix');
}
}
-
- window.gl = window.gl || {};
- window.gl.MergeRequestTabs = MergeRequestTabs;
-})();
+}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 74e5a4f1cea..0e854295fe3 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,236 +1,228 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, 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 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 */
/* global Issuable */
/* global ListMilestone */
import _ from 'underscore';
+import { timeFor } from './lib/utils/datetime_utility';
-(function() {
- this.MilestoneSelect = (function() {
- function MilestoneSelect(currentProject, els, options = {}) {
- var _this, $els;
- if (currentProject != null) {
- _this = this;
- this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
- }
+export default class MilestoneSelect {
+ constructor(currentProject, els, options = {}) {
+ if (currentProject !== null) {
+ this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
+ }
- $els = $(els);
+ this.init(els, options);
+ }
- if (!els) {
- $els = $('.js-milestone-select');
- }
+ init(els, options) {
+ let $els = $(els);
- $els.each(function(i, dropdown) {
- var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
- $dropdown = $(dropdown);
- projectId = $dropdown.data('project-id');
- milestonesUrl = $dropdown.data('milestones');
- issueUpdateURL = $dropdown.data('issueUpdate');
- showNo = $dropdown.data('show-no');
- showAny = $dropdown.data('show-any');
- showMenuAbove = $dropdown.data('showMenuAbove');
- showUpcoming = $dropdown.data('show-upcoming');
- showStarted = $dropdown.data('show-started');
- useId = $dropdown.data('use-id');
- defaultLabel = $dropdown.data('default-label');
- defaultNo = $dropdown.data('default-no');
- issuableId = $dropdown.data('issuable-id');
- abilityName = $dropdown.data('ability-name');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
- $value = $block.find('.value');
- $loading = $block.find('.block-loading').fadeOut();
- selectedMilestoneDefault = (showAny ? '' : null);
- selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
- selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
- if (issueUpdateURL) {
- milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
- milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
- }
- return $dropdown.glDropdown({
- showMenuAbove: showMenuAbove,
- data: function(term, callback) {
- return $.ajax({
- url: milestonesUrl
- }).done(function(data) {
- var extraOptions = [];
- if (showAny) {
- extraOptions.push({
- id: 0,
- name: '',
- title: 'Any Milestone'
- });
- }
- if (showNo) {
- extraOptions.push({
- id: -1,
- name: 'No Milestone',
- title: 'No Milestone'
- });
- }
- if (showUpcoming) {
- extraOptions.push({
- id: -2,
- name: '#upcoming',
- title: 'Upcoming'
- });
- }
- if (showStarted) {
- extraOptions.push({
- id: -3,
- name: '#started',
- title: 'Started'
- });
- }
- if (extraOptions.length) {
- extraOptions.push('divider');
- }
+ if (!els) {
+ $els = $('.js-milestone-select');
+ }
- callback(extraOptions.concat(data));
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
- });
- },
- renderRow: function(milestone) {
- return `
- <li data-milestone-id="${milestone.name}">
- <a href='#' class='dropdown-menu-milestone-link'>
- ${_.escape(milestone.title)}
- </a>
- </li>
- `;
- },
- filterable: true,
- search: {
- fields: ['title']
- },
- selectable: true,
- toggleLabel: function(selected, el, e) {
- if (selected && 'id' in selected && $(el).hasClass('is-active')) {
- return selected.title;
- } else {
- return defaultLabel;
- }
- },
- defaultLabel: defaultLabel,
- fieldName: $dropdown.data('field-name'),
- text: function(milestone) {
- return _.escape(milestone.title);
- },
- id: function(milestone) {
- if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
- return milestone.name;
- } else {
- return milestone.id;
- }
- },
- isSelected: function(milestone) {
- return milestone.name === selectedMilestone;
- },
- hidden: function() {
- $selectbox.hide();
- // display:block overrides the hide-collapse rule
- return $value.css('display', '');
- },
- opened: function(e) {
- const $el = $(e.currentTarget);
- if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
- selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
- }
- $('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
- },
- vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(clickEvent) {
- const { $el, e } = clickEvent;
- let selected = clickEvent.selectedObj;
+ $els.each((i, dropdown) => {
+ let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
+ const $dropdown = $(dropdown);
+ const projectId = $dropdown.data('project-id');
+ const milestonesUrl = $dropdown.data('milestones');
+ const issueUpdateURL = $dropdown.data('issueUpdate');
+ const showNo = $dropdown.data('show-no');
+ const showAny = $dropdown.data('show-any');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const showUpcoming = $dropdown.data('show-upcoming');
+ const showStarted = $dropdown.data('show-started');
+ const useId = $dropdown.data('use-id');
+ const defaultLabel = $dropdown.data('default-label');
+ const defaultNo = $dropdown.data('default-no');
+ const issuableId = $dropdown.data('issuable-id');
+ const abilityName = $dropdown.data('ability-name');
+ const $selectBox = $dropdown.closest('.selectbox');
+ const $block = $selectBox.closest('.block');
+ const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
+ const $value = $block.find('.value');
+ const $loading = $block.find('.block-loading').fadeOut();
+ selectedMilestoneDefault = (showAny ? '' : null);
+ selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
+ selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
- var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
- if (!selected) return;
+ if (issueUpdateURL) {
+ milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
+ collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
+ }
+ return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
+ data: (term, callback) => $.ajax({
+ url: milestonesUrl
+ }).done((data) => {
+ const extraOptions = [];
+ if (showAny) {
+ extraOptions.push({
+ id: 0,
+ name: '',
+ title: 'Any Milestone'
+ });
+ }
+ if (showNo) {
+ extraOptions.push({
+ id: -1,
+ name: 'No Milestone',
+ title: 'No Milestone'
+ });
+ }
+ if (showUpcoming) {
+ extraOptions.push({
+ id: -2,
+ name: '#upcoming',
+ title: 'Upcoming'
+ });
+ }
+ if (showStarted) {
+ extraOptions.push({
+ id: -3,
+ name: '#started',
+ title: 'Started'
+ });
+ }
+ if (extraOptions.length) {
+ extraOptions.push('divider');
+ }
- if (options.handleClick) {
- e.preventDefault();
- options.handleClick(selected);
- return;
- }
+ callback(extraOptions.concat(data));
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
+ $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
+ }),
+ renderRow: milestone => `
+ <li data-milestone-id="${milestone.name}">
+ <a href='#' class='dropdown-menu-milestone-link'>
+ ${_.escape(milestone.title)}
+ </a>
+ </li>
+ `,
+ filterable: true,
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ toggleLabel: (selected, el, e) => {
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
+ return selected.title;
+ } else {
+ return defaultLabel;
+ }
+ },
+ defaultLabel: defaultLabel,
+ fieldName: $dropdown.data('field-name'),
+ text: milestone => _.escape(milestone.title),
+ id: (milestone) => {
+ if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
+ return milestone.name;
+ } else {
+ return milestone.id;
+ }
+ },
+ isSelected: milestone => milestone.name === selectedMilestone,
+ hidden: () => {
+ $selectBox.hide();
+ // display:block overrides the hide-collapse rule
+ return $value.css('display', '');
+ },
+ opened: (e) => {
+ const $el = $(e.currentTarget);
+ if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
+ selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
+ }
+ $('a.is-active', $el).removeClass('is-active');
+ $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
+ },
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
+ clicked: (clickEvent) => {
+ const { $el, e } = clickEvent;
+ let selected = clickEvent.selectedObj;
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = (page === page && page === 'projects:merge_requests:index');
- isSelecting = (selected.name !== selectedMilestone);
- selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
- if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
- e.preventDefault();
- return;
- }
+ let data, boardsStore;
+ if (!selected) return;
- if ($dropdown.closest('.add-issues-modal').length) {
- boardsStore = gl.issueBoards.ModalStore.store.filter;
- }
+ if (options.handleClick) {
+ e.preventDefault();
+ options.handleClick(selected);
+ return;
+ }
- if (boardsStore) {
- boardsStore[$dropdown.data('field-name')] = selected.name;
- e.preventDefault();
- } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- return Issuable.filterResults($dropdown.closest('form'));
- } else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
- } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if (selected.id !== -1 && isSelecting) {
- gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
- id: selected.id,
- title: selected.name
- }));
- } else {
- gl.issueBoards.boardStoreIssueDelete('milestone');
- }
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = (page === page && page === 'projects:merge_requests:index');
+ const isSelecting = (selected.name !== selectedMilestone);
+ selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ e.preventDefault();
+ return;
+ }
- $dropdown.trigger('loading.gl.dropdown');
- $loading.removeClass('hidden').fadeIn();
+ if ($dropdown.closest('.add-issues-modal').length) {
+ boardsStore = gl.issueBoards.ModalStore.store.filter;
+ }
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
- .then(function () {
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.fadeOut();
- })
- .catch(() => {
- $loading.fadeOut();
- });
+ if (boardsStore) {
+ boardsStore[$dropdown.data('field-name')] = selected.name;
+ e.preventDefault();
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ return Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if (selected.id !== -1 && isSelecting) {
+ gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
+ id: selected.id,
+ title: selected.name
+ }));
} else {
- selected = $selectbox.find('input[type="hidden"]').val();
- data = {};
- data[abilityName] = {};
- data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.removeClass('hidden').fadeIn();
- $dropdown.trigger('loading.gl.dropdown');
- return $.ajax({
- type: 'PUT',
- url: issueUpdateURL,
- data: data
- }).done(function(data) {
+ gl.issueBoards.boardStoreIssueDelete('milestone');
+ }
+
+ $dropdown.trigger('loading.gl.dropdown');
+ $loading.removeClass('hidden').fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
- $selectbox.hide();
- $value.css('display', '');
- if (data.milestone != null) {
- data.milestone.full_path = _this.currentProject.full_path;
- data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
- data.milestone.name = data.milestone.title;
- $value.html(milestoneLinkTemplate(data.milestone));
- return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
- } else {
- $value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue.find('span').text('No');
- }
+ })
+ .catch(() => {
+ $loading.fadeOut();
});
- }
+ } else {
+ selected = $selectBox.find('input[type="hidden"]').val();
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].milestone_id = selected != null ? selected : null;
+ $loading.removeClass('hidden').fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+ return $.ajax({
+ type: 'PUT',
+ url: issueUpdateURL,
+ data: data
+ }).done((data) => {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ $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));
+ return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ } else {
+ $value.html(milestoneLinkNoneTemplate);
+ return $sidebarCollapsedValue.find('span').text('No');
+ }
+ });
}
- });
+ }
});
- }
-
- return MilestoneSelect;
- })();
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index cbe24c0915b..8da723ced03 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -21,6 +21,8 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
+ tagsPath: metricsData.tagsPath,
+ projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
@@ -112,6 +114,8 @@
:hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
+ :project-path="projectPath"
+ :tags-path="tagsPath"
/>
</graph-group>
</div>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index f8782fde927..a50b80c23d0 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -1,5 +1,8 @@
<script>
- import d3 from 'd3';
+ import { scaleLinear, scaleTime } from 'd3-scale';
+ import { axisLeft, axisBottom } from 'd3-axis';
+ import { max, extent } from 'd3-array';
+ import { select } from 'd3-selection';
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
@@ -7,10 +10,12 @@
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
- import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
+ import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';
+ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
+
export default {
props: {
graphData: {
@@ -30,6 +35,14 @@
required: false,
default: () => ({}),
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ tagsPath: {
+ type: String,
+ required: true,
+ },
},
mixins: [MonitoringMixin],
@@ -56,8 +69,8 @@
currentFlagPosition: 0,
showFlag: false,
showFlagContent: false,
- showDeployInfo: true,
timeSeries: [],
+ realPixelRatio: 1,
};
},
@@ -74,10 +87,7 @@
},
innerViewBox() {
- if ((this.baseGraphWidth - 150) > 0) {
- return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
- }
- return '0 0 0 0';
+ return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
},
axisTransform() {
@@ -89,6 +99,10 @@
paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
};
},
+
+ deploymentFlagData() {
+ return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
+ },
},
methods: {
@@ -109,6 +123,10 @@
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight;
this.baseGraphWidth = this.graphWidth;
+
+ // pixel offsets inside the svg and outside are not 1:1
+ this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth);
+
this.renderAxesPaths();
this.formatDeployments();
},
@@ -148,25 +166,22 @@
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
- const axisXScale = d3.time.scale()
+ const axisXScale = d3.scaleTime()
.range([0, this.graphWidth - 70]);
- const axisYScale = d3.scale.linear()
+ const axisYScale = d3.scaleLinear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
- const xAxis = d3.svg.axis()
+ const xAxis = d3.axisBottom()
.scale(axisXScale)
- .ticks(d3.time.minute, 60)
- .tickFormat(timeScaleFormat)
- .orient('bottom');
+ .tickFormat(timeScaleFormat);
- const yAxis = d3.svg.axis()
+ const yAxis = d3.axisLeft()
.scale(axisYScale)
- .ticks(measurements.yTicks)
- .orient('left');
+ .ticks(measurements.yTicks);
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
@@ -252,20 +267,9 @@
:area-color="path.areaColor"
/>
<graph-deployment
- :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
- :graph-width="graphWidth"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- />
- <graph-flag
- v-if="showFlag"
- :current-x-coordinate="currentXCoordinate"
- :current-data="currentData"
- :current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
- :show-flag-content="showFlagContent"
/>
<rect
class="prometheus-graph-overlay"
@@ -277,6 +281,19 @@
</rect>
</svg>
</svg>
+ <graph-flag
+ :real-pixel-ratio="realPixelRatio"
+ :current-x-coordinate="currentXCoordinate"
+ :current-data="currentData"
+ :graph-height="graphHeight"
+ :graph-height-offset="graphHeightOffset"
+ :show-flag-content="showFlagContent"
+ :time-series="timeSeries"
+ :unit-of-display="unitOfDisplay"
+ :current-data-index="currentDataIndex"
+ :legend-title="legendTitle"
+ :deployment-flag-data="deploymentFlagData"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index e3b8be0c7fb..8d6393d4ce5 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -1,12 +1,6 @@
<script>
- import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
-
export default {
props: {
- showDeployInfo: {
- type: Boolean,
- required: true,
- },
deploymentData: {
type: Array,
required: true,
@@ -19,10 +13,6 @@
type: Number,
required: true,
},
- graphWidth: {
- type: Number,
- required: true,
- },
},
computed: {
@@ -32,44 +22,17 @@
},
methods: {
- refText(d) {
- return d.tag ? d.ref : d.sha.slice(0, 6);
- },
-
- formatTime(deploymentTime) {
- return timeFormat(deploymentTime);
- },
-
- formatDate(deploymentTime) {
- return dateFormat(deploymentTime);
- },
-
- nameDeploymentClass(deployment) {
- return `deploy-info-${deployment.id}`;
- },
-
transformDeploymentGroup(deployment) {
- return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
- },
-
- positionFlag(deployment) {
- let xPosition = 3;
- if (deployment.xPos > (this.graphWidth - 200)) {
- xPosition = -97;
- }
- return xPosition;
+ return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
},
};
</script>
<template>
- <g
- class="deploy-info"
- v-if="showDeployInfo">
+ <g class="deploy-info">
<g
v-for="(deployment, index) in deploymentData"
:key="index"
- :class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
<rect
x="0"
@@ -86,41 +49,6 @@
:y2="calculatedHeight"
stroke="#000">
</line>
- <svg
- v-if="deployment.showDeploymentFlag"
- class="js-deploy-info-box"
- :x="positionFlag(deployment)"
- y="0"
- width="92"
- height="60">
- <rect
- class="rect-text-metric deploy-info-rect rect-metric"
- x="1"
- y="1"
- rx="2"
- width="90"
- height="58">
- </rect>
- <g
- transform="translate(5, 2)">
- <text
- class="deploy-info-text text-metric-bold">
- {{refText(deployment)}}
- </text>
- </g>
- <text
- class="deploy-info-text"
- y="18"
- transform="translate(5, 2)">
- {{formatDate(deployment.time)}}
- </text>
- <text
- class="deploy-info-text text-metric-bold"
- y="38"
- transform="translate(5, 2)">
- {{formatTime(deployment.time)}}
- </text>
- </svg>
</g>
<svg
height="0"
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 10fb7ff6803..62ebc3f419c 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -1,5 +1,7 @@
<script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
+ import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+ import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
@@ -7,14 +9,15 @@
type: Number,
required: true,
},
- currentFlagPosition: {
- type: Number,
- required: true,
- },
currentData: {
type: Object,
required: true,
},
+ deploymentFlagData: {
+ type: Object,
+ required: false,
+ default: null,
+ },
graphHeight: {
type: Number,
required: true,
@@ -23,71 +26,173 @@
type: Number,
required: true,
},
+ realPixelRatio: {
+ type: Number,
+ required: true,
+ },
showFlagContent: {
type: Boolean,
required: true,
},
+ timeSeries: {
+ type: Array,
+ required: true,
+ },
+ unitOfDisplay: {
+ type: String,
+ required: true,
+ },
+ currentDataIndex: {
+ type: Number,
+ required: true,
+ },
+ legendTitle: {
+ type: String,
+ required: true,
+ },
},
- data() {
- return {
- circleColorRgb: '#8fbce8',
- };
+ components: {
+ Icon,
},
computed: {
formatTime() {
- return timeFormat(this.currentData.time);
+ return this.deploymentFlagData ?
+ timeFormat(this.deploymentFlagData.time) :
+ timeFormat(this.currentData.time);
},
formatDate() {
- return dateFormat(this.currentData.time);
+ return this.deploymentFlagData ?
+ dateFormat(this.deploymentFlagData.time) :
+ dateFormat(this.currentData.time);
+ },
+
+ cursorStyle() {
+ const xCoordinate = this.deploymentFlagData ?
+ this.deploymentFlagData.xPos :
+ this.currentXCoordinate;
+
+ const offsetTop = 20 * this.realPixelRatio;
+ const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
+ const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
+
+ return {
+ top: `${offsetTop}px`,
+ left: `${offsetLeft}px`,
+ height: `${height}px`,
+ };
+ },
+
+ flagOrientation() {
+ if (this.currentXCoordinate * this.realPixelRatio > 120) {
+ return 'left';
+ }
+ return 'right';
+ },
+ },
+
+ methods: {
+ seriesMetricValue(series) {
+ const index = this.deploymentFlagData ?
+ this.deploymentFlagData.seriesIndex :
+ this.currentDataIndex;
+ const value = series.values[index] &&
+ series.values[index].value;
+ if (isNaN(value)) {
+ return '-';
+ }
+ return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
+ seriesMetricLabel(index, series) {
+ if (this.timeSeries.length < 2) {
+ return this.legendTitle;
+ }
+ if (series.metricTag) {
+ return series.metricTag;
+ }
+ return `series ${index + 1}`;
+ },
+
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
},
},
};
</script>
+
<template>
- <g class="mouse-over-flag">
- <line
- class="selected-metric-line"
- :x1="currentXCoordinate"
- :y1="0"
- :x2="currentXCoordinate"
- :y2="calculatedHeight"
- transform="translate(-5, 20)">
- </line>
- <svg
+ <div
+ class="prometheus-graph-cursor"
+ :style="cursorStyle"
+ >
+ <div
v-if="showFlagContent"
- class="rect-text-metric"
- :x="currentFlagPosition"
- y="0">
- <rect
- class="rect-metric"
- x="4"
- y="1"
- rx="2"
- width="90"
- height="40"
- transform="translate(-3, 20)">
- </rect>
- <text
- class="text-metric text-metric-bold"
- x="16"
- y="35"
- transform="translate(-5, 20)">
- {{formatTime}}
- </text>
- <text
- class="text-metric"
- x="16"
- y="15"
- transform="translate(-5, 20)">
- {{formatDate}}
- </text>
- </svg>
- </g>
+ class="prometheus-graph-flag popover"
+ :class="flagOrientation"
+ >
+ <div class="arrow"></div>
+ <div class="popover-title">
+ <h5 v-if="this.deploymentFlagData">
+ Deployed
+ </h5>
+ {{formatDate}} at
+ <strong>{{formatTime}}</strong>
+ </div>
+ <div
+ v-if="this.deploymentFlagData"
+ class="popover-content deploy-meta-content"
+ >
+ <div>
+ <icon
+ name="commit"
+ :size="12">
+ </icon>
+ <a :href="deploymentFlagData.commitUrl">
+ {{deploymentFlagData.sha.slice(0, 8)}}
+ </a>
+ </div>
+ <div
+ v-if="deploymentFlagData.tag">
+ <icon
+ name="label"
+ :size="12">
+ </icon>
+ <a :href="deploymentFlagData.tagUrl">
+ {{deploymentFlagData.ref}}
+ </a>
+ </div>
+ </div>
+ <div class="popover-content">
+ <table>
+ <tr
+ v-for="(series, index) in timeSeries"
+ :key="index"
+ >
+ <td>
+ <svg width="15" height="6">
+ <line
+ :stroke="series.lineColor"
+ :stroke-dasharray="strokeDashArray(series.lineStyle)"
+ stroke-width="4"
+ x1="0"
+ x2="15"
+ y1="2"
+ y2="2">
+ </line>
+ </svg>
+ </td>
+ <td>{{seriesMetricLabel(index, series)}}</td>
+ <td>
+ <strong>{{seriesMetricValue(series)}}</strong>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 31f38aca5d6..6cc67ba57ee 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -29,13 +29,18 @@ const mixins = {
time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) {
+ const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
+
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
+ commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag,
+ tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
ref: deployment.ref.name,
xPos,
+ seriesIndex,
showDeploymentFlag: false,
});
}
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index c4c6b1ac1f5..f3c9acdd93e 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -1,16 +1,42 @@
-import d3 from 'd3';
+import { timeFormat as time } from 'd3-time-format';
+import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time';
+import { bisector } from 'd3-array';
-export const dateFormat = d3.time.format('%b %-d, %Y');
-export const timeFormat = d3.time.format('%-I:%M%p');
+const d3 = {
+ time,
+ bisector,
+ timeSecond,
+ timeMinute,
+ timeHour,
+ timeDay,
+ timeWeek,
+ timeMonth,
+ timeYear,
+};
+
+export const dateFormat = d3.time('%a, %b %-d');
+export const timeFormat = d3.time('%-I:%M%p');
+export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
-export const timeScaleFormat = d3.time.format.multi([
- ['.%L', d => d.getMilliseconds()],
- [':%S', d => d.getSeconds()],
- ['%-I:%M', d => d.getMinutes()],
- ['%-I %p', d => d.getHours()],
- ['%a %-d', d => d.getDay() && d.getDate() !== 1],
- ['%b %-d', d => d.getDate() !== 1],
- ['%B', d => d.getMonth()],
- ['%Y', () => true],
-]);
+export function timeScaleFormat(date) {
+ let formatFunction;
+ if (d3.timeSecond(date) < date) {
+ formatFunction = d3.time('.%L');
+ } else if (d3.timeMinute(date) < date) {
+ formatFunction = d3.time(':%S');
+ } else if (d3.timeHour(date) < date) {
+ formatFunction = d3.time('%-I:%M');
+ } else if (d3.timeDay(date) < date) {
+ formatFunction = d3.time('%-I %p');
+ } else if (d3.timeWeek(date) < date) {
+ formatFunction = d3.time('%a %d');
+ } else if (d3.timeMonth(date) < date) {
+ formatFunction = d3.time('%b %d');
+ } else if (d3.timeYear(date) < date) {
+ formatFunction = d3.time('%B');
+ } else {
+ formatFunction = d3.time('%Y');
+ }
+ return formatFunction(date);
+}
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index d21a265bd43..4ce3dad440c 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -1,5 +1,10 @@
-import d3 from 'd3';
import _ from 'underscore';
+import { scaleLinear, scaleTime } from 'd3-scale';
+import { line, area, curveLinear } from 'd3-shape';
+import { extent, max } from 'd3-array';
+import { timeMinute } from 'd3-time';
+
+const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'],
@@ -38,27 +43,27 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
let lineColor = '';
let areaColor = '';
- const timeSeriesScaleX = d3.time.scale()
+ const timeSeriesScaleX = d3.scaleTime()
.range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scale.linear()
+ const timeSeriesScaleY = d3.scaleLinear()
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(xDom);
- timeSeriesScaleX.ticks(d3.time.minute, 60);
+ timeSeriesScaleX.ticks(d3.timeMinute, 60);
timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null;
- const lineFunction = d3.svg.line()
+ const lineFunction = d3.line()
.defined(defined)
- .interpolate('linear')
+ .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
- const areaFunction = d3.svg.area()
+ const areaFunction = d3.area()
.defined(defined)
- .interpolate('linear')
+ .curve(d3.curveLinear)
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value));
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 1d496c64e53..aa377327107 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api';
-import './lib/utils/url_utility';
+import { mergeUrlParams } from './lib/utils/url_utility';
export default class NamespaceSelect {
constructor(opts) {
@@ -50,7 +50,7 @@ export default class NamespaceSelect {
}
},
url(namespace) {
- return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
+ return mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
},
});
}
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 6e152497d20..a2f0a44863f 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -6,11 +6,12 @@ export default class NewCommitForm {
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
- this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.createMergeRequestContainer = form.find(
+ '.js-create-merge-request-container',
+ );
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
-
renderDestination() {
var different;
different = this.branchName.val() !== this.originalBranch.val();
@@ -23,6 +24,6 @@ export default class NewCommitForm {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
}
- return this.wasDifferent = different;
+ return (this.wasDifferent = different);
}
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index e1ab28978e8..a2b8e6f6495 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -16,6 +16,7 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
+import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
@@ -24,6 +25,7 @@ import Autosave from './autosave';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
+import { localTimeAgo } from './lib/utils/datetime_utility';
window.autosize = Autosize;
@@ -35,6 +37,12 @@ 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) {
+ if (!this.instance) {
+ this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
+ }
+ }
+
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
@@ -310,7 +318,7 @@ export default class Notes {
setupNewNote($note) {
// Update datetime format on the recent note
- gl.utils.localTimeAgo($note.find('.js-timeago'), false);
+ localTimeAgo($note.find('.js-timeago'), false);
this.collapseLongCommitList();
this.taskList.init();
@@ -330,7 +338,7 @@ export default class Notes {
}
static updateNoteTargetSelector($note) {
- const hash = gl.utils.getLocationHash();
+ const hash = getLocationHash();
// Needs to be an explicit true/false for the jQuery `toggleClass(force)`
const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0);
$note.toggleClass('target', addTargetClass);
@@ -462,7 +470,7 @@ export default class Notes {
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
}
- gl.utils.localTimeAgo($('.js-timeago'), false);
+ localTimeAgo($('.js-timeago'), false);
Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
}
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 78986a450c2..e594377bc40 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -15,7 +15,7 @@
import issuableStateMixin from '../mixins/issuable_state';
export default {
- name: 'issueCommentForm',
+ name: 'commentForm',
data() {
return {
note: '',
diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index a16c5f6a785..ac4e1ffe53a 100644
--- a/app/assets/javascripts/notes/components/issue_note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -2,7 +2,7 @@
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
- import issueNoteForm from './issue_note_form.vue';
+ import noteForm from './note_form.vue';
import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
@@ -29,7 +29,7 @@
noteEditedText,
noteAwardsList,
noteAttachment,
- issueNoteForm,
+ noteForm,
},
computed: {
noteBody() {
@@ -87,7 +87,7 @@
<div
v-html="note.note_html"
class="note-text md"></div>
- <issue-note-form
+ <note-form
v-if="isEditing"
ref="noteForm"
@handleFormUpdate="handleFormUpdate"
diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 4d527cb6643..4d527cb6643 100644
--- a/app/assets/javascripts/notes/components/issue_note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 460fde9b62a..11e8f805635 100644
--- a/app/assets/javascripts/notes/components/issue_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -2,12 +2,12 @@
import { mapActions, mapGetters } from 'vuex';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
- import issueNote from './issue_note.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
- import issueNoteForm from './issue_note_form.vue';
+ import noteForm from './note_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
@@ -25,12 +25,12 @@
};
},
components: {
- issueNote,
+ noteableNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
noteEditedText,
- issueNoteForm,
+ noteForm,
placeholderNote,
placeholderSystemNote,
},
@@ -86,7 +86,7 @@
return placeholderNote;
}
- return issueNote;
+ return noteableNote;
},
componentData(note) {
return note.isPlaceholderNote ? note.notes[0] : note;
@@ -209,7 +209,7 @@
type="button"
class="js-vue-discussion-reply btn btn-text-field"
title="Add a reply">Reply...</button>
- <issue-note-form
+ <note-form
v-if="isReplying"
save-button-title="Comment"
:discussion="note"
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 8c81c5d6df3..9186d6ff64a 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -1,10 +1,11 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+ import { escape } from 'underscore';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
- import issueNoteBody from './issue_note_body.vue';
+ import noteBody from './note_body.vue';
import eventHub from '../event_hub';
export default {
@@ -25,7 +26,7 @@
userAvatarLink,
noteHeader,
noteActions,
- issueNoteBody,
+ noteBody,
},
computed: {
...mapGetters([
@@ -85,7 +86,7 @@
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
- this.note.note_html = noteText;
+ this.note.note_html = escape(noteText);
this.updateNote(data)
.then(() => {
@@ -122,9 +123,7 @@
// 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;
- if (this.$refs.noteBody) {
- this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
- }
+ this.$refs.noteBody.$refs.noteForm.note = noteText;
},
},
created() {
@@ -173,7 +172,7 @@
@handleDelete="deleteHandler"
/>
</div>
- <issue-note-body
+ <note-body
:note="note"
:can-edit="note.current_user.can_edit"
:is-editing="isEditing"
diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 4cfcffa2391..c4cae4b3b6f 100644
--- a/app/assets/javascripts/notes/components/issue_notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,18 +1,19 @@
<script>
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 issueNote from './issue_note.vue';
- import issueDiscussion from './issue_discussion.vue';
+ import noteableNote from './noteable_note.vue';
+ import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
- import issueCommentForm from './issue_comment_form.vue';
+ import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
- name: 'issueNotesApp',
+ name: 'notesApp',
props: {
noteableData: {
type: Object,
@@ -35,10 +36,10 @@
};
},
components: {
- issueNote,
- issueDiscussion,
+ noteableNote,
+ noteableDiscussion,
systemNote,
- issueCommentForm,
+ commentForm,
loadingIcon,
placeholderNote,
placeholderSystemNote,
@@ -68,10 +69,10 @@
}
return placeholderNote;
} else if (note.individual_note) {
- return note.notes[0].system ? systemNote : issueNote;
+ return note.notes[0].system ? systemNote : noteableNote;
}
- return issueDiscussion;
+ return noteableDiscussion;
},
getComponentData(note) {
return note.individual_note ? note.notes[0] : note;
@@ -86,7 +87,7 @@
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
- Flash('Something went wrong while fetching issue comments. Please try again.');
+ Flash('Something went wrong while fetching comments. Please try again.');
});
},
initPolling() {
@@ -95,7 +96,7 @@
this.poll();
},
checkLocationHash() {
- const hash = gl.utils.getLocationHash();
+ const hash = getLocationHash();
const element = document.getElementById(hash);
if (hash && element) {
@@ -146,6 +147,6 @@
/>
</ul>
- <issue-comment-form />
+ <comment-form />
</div>
</template>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index a94163a5f87..d250dd8d25b 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import issueNotesApp from './components/issue_notes_app.vue';
+import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-vue-notes',
components: {
- issueNotesApp,
+ notesApp,
},
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
@@ -32,7 +32,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
};
},
render(createElement) {
- return createElement('issue-notes-app', {
+ return createElement('notes-app', {
props: {
noteableData: this.noteableData,
notesData: this.notesData,
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index f90ac2d9f71..9570d1c00aa 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,31 +1,25 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
import Flash from './flash';
-(function() {
- this.NotificationsDropdown = (function() {
- function NotificationsDropdown() {
- $(document).off('click', '.update-notification').on('click', '.update-notification', function(e) {
- var form, label, notificationLevel;
- e.preventDefault();
- if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') {
- return;
- }
- notificationLevel = $(this).data('notification-level');
- label = $(this).data('notification-title');
- form = $(this).parents('.notification-form:first');
- form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
- form.find('#notification_setting_level').val(notificationLevel);
- return form.submit();
- });
- $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) {
- if (data.saved) {
- return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
- } else {
- return new Flash('Failed to save new settings', 'alert');
- }
- });
+export default function notificationsDropdown() {
+ $(document).on('click', '.update-notification', function updateNotificationCallback(e) {
+ e.preventDefault();
+ if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') {
+ return;
}
- return NotificationsDropdown;
- })();
-}).call(window);
+ const notificationLevel = $(this).data('notification-level');
+ const form = $(this).parents('.notification-form:first');
+
+ form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
+ form.find('#notification_setting_level').val(notificationLevel);
+ form.submit();
+ });
+
+ $(document).on('ajax:success', '.notification-form', (e, data) => {
+ if (data.saved) {
+ $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
+ } else {
+ Flash('Failed to save new settings', 'alert');
+ }
+ });
+}
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 2ab9c4fed2c..4534360d577 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,55 +1,50 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, max-len */
-(function() {
- this.NotificationsForm = (function() {
- function NotificationsForm() {
- this.toggleCheckbox = this.toggleCheckbox.bind(this);
- this.removeEventListeners();
- this.initEventListeners();
- }
+export default class NotificationsForm {
+ constructor() {
+ this.toggleCheckbox = this.toggleCheckbox.bind(this);
+ this.initEventListeners();
+ }
- NotificationsForm.prototype.removeEventListeners = function() {
- return $(document).off('change', '.js-custom-notification-event');
- };
+ initEventListeners() {
+ $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox);
+ }
- NotificationsForm.prototype.initEventListeners = function() {
- return $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox);
- };
+ toggleCheckbox(e) {
+ const $checkbox = $(e.currentTarget);
+ const $parent = $checkbox.closest('.checkbox');
- NotificationsForm.prototype.toggleCheckbox = function(e) {
- var $checkbox, $parent;
- $checkbox = $(e.currentTarget);
- $parent = $checkbox.closest('.checkbox');
- return this.saveEvent($checkbox, $parent);
- };
+ this.saveEvent($checkbox, $parent);
+ }
- NotificationsForm.prototype.showCheckboxLoadingSpinner = function($parent) {
- return $parent.addClass('is-loading').find('.custom-notification-event-loading').removeClass('fa-check').addClass('fa-spin fa-spinner').removeClass('is-done');
- };
+ // eslint-disable-next-line class-methods-use-this
+ showCheckboxLoadingSpinner($parent) {
+ $parent.addClass('is-loading')
+ .find('.custom-notification-event-loading')
+ .removeClass('fa-check')
+ .addClass('fa-spin fa-spinner')
+ .removeClass('is-done');
+ }
- NotificationsForm.prototype.saveEvent = function($checkbox, $parent) {
- var form;
- form = $parent.parents('form:first');
- return $.ajax({
- url: form.attr('action'),
- method: form.attr('method'),
- dataType: 'json',
- data: form.serialize(),
- beforeSend: (function(_this) {
- return function() {
- return _this.showCheckboxLoadingSpinner($parent);
- };
- })(this)
- }).done(function(data) {
- $checkbox.enable();
- if (data.saved) {
- $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
- return setTimeout(function() {
- return $parent.removeClass('is-loading').find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
- }, 2000);
- }
- });
- };
+ saveEvent($checkbox, $parent) {
+ const form = $parent.parents('form:first');
- return NotificationsForm;
- })();
-}).call(window);
+ return $.ajax({
+ url: form.attr('action'),
+ method: form.attr('method'),
+ dataType: 'json',
+ data: form.serialize(),
+ beforeSend: () => {
+ this.showCheckboxLoadingSpinner($parent);
+ },
+ }).done((data) => {
+ $checkbox.enable();
+ if (data.saved) {
+ $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
+ setTimeout(() => {
+ $parent.removeClass('is-loading')
+ .find('.custom-notification-event-loading')
+ .toggleClass('fa-spin fa-spinner fa-check is-done');
+ }, 2000);
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index e3fc1e2fc2f..6552a88b606 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,78 +1,74 @@
import { getParameterByName } from '~/lib/utils/common_utils';
-import '~/lib/utils/url_utility';
+import { removeParams } from './lib/utils/url_utility';
-(() => {
- const ENDLESS_SCROLL_BOTTOM_PX = 400;
- const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
+const ENDLESS_SCROLL_BOTTOM_PX = 400;
+const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
- const Pager = {
- init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
- this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
- this.limit = limit;
- this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
- this.disable = disable;
- this.prepareData = prepareData;
- this.callback = callback;
- this.loading = $('.loading').first();
- if (preload) {
- this.offset = 0;
- this.getOld();
- }
- this.initLoadMore();
- },
+export default {
+ init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
+ this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
+ this.limit = limit;
+ this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
+ this.disable = disable;
+ this.prepareData = prepareData;
+ this.callback = callback;
+ this.loading = $('.loading').first();
+ if (preload) {
+ this.offset = 0;
+ this.getOld();
+ }
+ this.initLoadMore();
+ },
- getOld() {
- this.loading.show();
- $.ajax({
- type: 'GET',
- url: this.url,
- data: `limit=${this.limit}&offset=${this.offset}`,
- dataType: 'json',
- error: () => this.loading.hide(),
- success: (data) => {
- this.append(data.count, this.prepareData(data.html));
- this.callback();
+ getOld() {
+ this.loading.show();
+ $.ajax({
+ type: 'GET',
+ url: this.url,
+ data: `limit=${this.limit}&offset=${this.offset}`,
+ dataType: 'json',
+ error: () => this.loading.hide(),
+ success: (data) => {
+ this.append(data.count, this.prepareData(data.html));
+ this.callback();
- // keep loading until we've filled the viewport height
- if (!this.disable && !this.isScrollable()) {
- this.getOld();
- } else {
- this.loading.hide();
- }
- },
- });
- },
+ // keep loading until we've filled the viewport height
+ if (!this.disable && !this.isScrollable()) {
+ this.getOld();
+ } else {
+ this.loading.hide();
+ }
+ },
+ });
+ },
- append(count, html) {
- $('.content_list').append(html);
- if (count > 0) {
- this.offset += count;
- } else {
- this.disable = true;
- }
- },
+ append(count, html) {
+ $('.content_list').append(html);
+ if (count > 0) {
+ this.offset += count;
+ } else {
+ this.disable = true;
+ }
+ },
- isScrollable() {
- const $w = $(window);
- return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
- },
+ isScrollable() {
+ const $w = $(window);
+ return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
+ },
- initLoadMore() {
- $(document).unbind('scroll');
- $(document).endlessScroll({
- bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
- fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
- fireOnce: true,
- ceaseFire: () => this.disable === true,
- callback: () => {
- if (!this.loading.is(':visible')) {
- this.loading.show();
- this.getOld();
- }
- },
- });
- },
- };
-
- window.Pager = Pager;
-})();
+ initLoadMore() {
+ $(document).unbind('scroll');
+ $(document).endlessScroll({
+ bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
+ fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
+ fireOnce: true,
+ ceaseFire: () => this.disable === true,
+ callback: () => {
+ if (!this.loading.is(':visible')) {
+ this.loading.show();
+ this.getOld();
+ }
+ },
+ });
+ },
+};
diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js
new file mode 100644
index 00000000000..f18f98b4e9a
--- /dev/null
+++ b/app/assets/javascripts/pages/users/show/index.js
@@ -0,0 +1,3 @@
+import UserCallout from '~/user_callout';
+
+export default () => new UserCallout();
diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js
index 9bbdf7f513c..0562a681c4b 100644
--- a/app/assets/javascripts/performance_bar.js
+++ b/app/assets/javascripts/performance_bar.js
@@ -1,5 +1,6 @@
import 'vendor/peek';
import 'vendor/peek.performance_bar';
+import { getParameterValues } from './lib/utils/url_utility';
export default class PerformanceBar {
constructor(opts) {
@@ -39,7 +40,7 @@ export default class PerformanceBar {
}
handleLineProfileLink(e) {
- const lineProfilerParameter = gl.utils.getParameterValues('lineprofiler');
+ const lineProfilerParameter = getParameterValues('lineprofiler');
const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
const shouldToggleModal = lineProfilerParameter.length > 0 &&
lineProfilerParameterRegex.test(e.currentTarget.href);
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index 0eaac8dd64f..78322f30685 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,36 +1,41 @@
<script>
-export default {
- props: {
- helpPagePath: {
- type: String,
- required: true,
+ export default {
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- },
-};
+ };
</script>
-
<template>
<div class="row empty-state js-empty-state">
<div class="col-xs-12">
- <div class="svg-content">
- <img :src="emptyStateSvgPath"/>
+ <div class="svg-content svg-250">
+ <img :src="emptyStateSvgPath" />
</div>
</div>
- <div class="col-xs-12 text-center">
+ <div class="col-xs-12">
<div class="text-content">
- <h4>Build with confidence</h4>
+ <h4 class="text-center">
+ {{ s__("Pipelines|Build with confidence") }}
+ </h4>
<p>
- Continous Integration can help catch bugs by running your tests automatically,
- while Continuous Deployment can help you deliver code to your product environment.
+ {{ s__("Pipelines|Continous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.") }}
</p>
- <a :href="helpPagePath" class="btn btn-info">
- Get started with Pipelines
- </a>
+ <div class="text-center">
+ <a
+ :href="helpPagePath"
+ class="btn btn-info"
+ >
+ {{ s__("Pipelines|Get started with Pipelines") }}
+ </a>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 08199b4234a..b01c799643c 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -59,8 +59,26 @@
},
computed: {
+ status() {
+ return this.job && this.job.status ? this.job.status : {};
+ },
+
tooltipText() {
- return `${this.job.name} - ${this.job.status.label}`;
+ const textBuilder = [];
+
+ if (this.job.name) {
+ textBuilder.push(this.job.name);
+ }
+
+ if (this.job.name && this.status.label) {
+ textBuilder.push('-');
+ }
+
+ if (this.status.label) {
+ textBuilder.push(`${this.job.status.label}`);
+ }
+
+ return textBuilder.join(' ');
},
/**
@@ -78,8 +96,8 @@
<div class="ci-job-component">
<a
v-tooltip
- v-if="job.status.has_details"
- :href="job.status.details_path"
+ v-if="status.has_details"
+ :href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
data-container="body"
@@ -95,6 +113,7 @@
<div
v-else
v-tooltip
+ class="js-job-component-tooltip"
:title="tooltipText"
:class="cssClassJobName"
data-container="body"
@@ -108,18 +127,18 @@
<action-component
v-if="hasAction && !isDropdown"
- :tooltip-text="job.status.action.title"
- :link="job.status.action.path"
- :action-icon="job.status.action.icon"
- :action-method="job.status.action.method"
+ :tooltip-text="status.action.title"
+ :link="status.action.path"
+ :action-icon="status.action.icon"
+ :action-method="status.action.method"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
- :tooltip-text="job.status.action.title"
- :link="job.status.action.path"
- :action-icon="job.status.action.icon"
- :action-method="job.status.action.method"
+ :tooltip-text="status.action.title"
+ :link="status.action.path"
+ :action-icon="status.action.icon"
+ :action-method="status.action.method"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index 632fc167f2b..f31a91c3403 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -17,6 +17,11 @@ export default {
required: true,
},
+ resetCachePath: {
+ type: String,
+ required: true,
+ },
+
ciLintPath: {
type: String,
required: true,
@@ -46,6 +51,14 @@ export default {
</a>
<a
+ data-method="post"
+ rel="nofollow"
+ :href="resetCachePath"
+ class="btn btn-default">
+ Clear runner caches
+ </a>
+
+ <a
:href="ciLintPath"
class="btn btn-default">
CI Lint
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index fe1f3b4246a..8fa416168e7 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -50,6 +50,7 @@
canCreatePipeline: pipelinesData.canCreatePipeline,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
+ resetCachePath: pipelinesData.resetCachePath,
state: this.store.state,
scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1',
@@ -220,6 +221,7 @@
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
+ :resetCachePath="resetCachePath"
:ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index 751a20991af..831aa92ac4f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,5 +1,6 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
+ import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@@ -11,6 +12,9 @@
directives: {
tooltip,
},
+ components: {
+ icon,
+ },
};
</script>
<template>
@@ -24,10 +28,9 @@
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts">
- <i
- class="fa fa-download"
- aria-hidden="true">
- </i>
+ <icon
+ name="download">
+ </icon>
<i
class="fa fa-caret-down"
aria-hidden="true">
diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js
index 923d9bfb248..3e4b6eeb5bf 100644
--- a/app/assets/javascripts/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/pipelines/pipelines_bundle.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
import PipelinesStore from './stores/pipelines_store';
import pipelinesComponent from './components/pipelines.vue';
+import Translate from '../vue_shared/translate';
+
+Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipelines-list-vue',
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 141333b2b4d..ffaafb3ee9e 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -117,12 +117,10 @@
}());
markdownPreview = new window.MarkdownPreview();
-
previewButtonSelector = '.js-md-preview-button';
-
writeButtonSelector = '.js-md-write-button';
-
lastTextareaPreviewed = null;
+ const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function () {
var $form = $(this);
@@ -146,6 +144,7 @@
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
+ markdownToolbar.removeClass('active');
markdownPreview.showPreview($form);
});
@@ -167,6 +166,7 @@
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide();
+ markdownToolbar.addClass('active');
markdownPreview.hideReferencedCommands($form);
});
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 6348a2e331d..36ad618aa46 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,7 +1,7 @@
<script>
- import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
- import { __, s__, sprintf } from '../../../locale';
- import csrf from '../../../lib/utils/csrf';
+ import modal from '~/vue_shared/components/modal.vue';
+ import { __, s__, sprintf } from '~/locale';
+ import csrf from '~/lib/utils/csrf';
export default {
props: {
@@ -22,11 +22,10 @@
return {
enteredPassword: '',
enteredUsername: '',
- isOpen: false,
};
},
components: {
- popupDialog,
+ modal,
},
computed: {
csrfToken() {
@@ -69,78 +68,58 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
return this.enteredUsername === this.username;
},
- onSubmit(status) {
- if (status) {
- if (!this.canSubmit()) {
- return;
- }
-
- this.$refs.form.submit();
- }
-
- this.toggleOpen(false);
- },
- toggleOpen(isOpen) {
- this.isOpen = isOpen;
+ onSubmit() {
+ this.$refs.form.submit();
},
},
};
</script>
<template>
- <div>
- <popup-dialog
- v-if="isOpen"
- :title="s__('Profiles|Delete your account?')"
- :text="text"
- :kind="`danger ${!canSubmit() && 'disabled'}`"
- :primary-button-label="s__('Profiles|Delete account')"
- @toggle="toggleOpen"
- @submit="onSubmit">
-
- <template slot="body" slot-scope="props">
- <p v-html="props.text"></p>
+ <modal
+ 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()">
- <form
- ref="form"
- :action="actionUrl"
- method="post">
+ <template slot="body" slot-scope="props">
+ <p v-html="props.text"></p>
- <input
- type="hidden"
- name="_method"
- value="delete" />
- <input
- type="hidden"
- name="authenticity_token"
- :value="csrfToken" />
+ <form
+ ref="form"
+ :action="actionUrl"
+ method="post">
- <p id="input-label" v-html="inputLabel"></p>
+ <input
+ type="hidden"
+ name="_method"
+ value="delete" />
+ <input
+ type="hidden"
+ name="authenticity_token"
+ :value="csrfToken" />
- <input
- v-if="confirmWithPassword"
- name="password"
- class="form-control"
- type="password"
- v-model="enteredPassword"
- aria-labelledby="input-label" />
- <input
- v-else
- name="username"
- class="form-control"
- type="text"
- v-model="enteredUsername"
- aria-labelledby="input-label" />
- </form>
- </template>
+ <p id="input-label" v-html="inputLabel"></p>
- </popup-dialog>
+ <input
+ v-if="confirmWithPassword"
+ name="password"
+ class="form-control"
+ type="password"
+ v-model="enteredPassword"
+ aria-labelledby="input-label" />
+ <input
+ v-else
+ name="username"
+ class="form-control"
+ type="text"
+ v-model="enteredUsername"
+ aria-labelledby="input-label" />
+ </form>
+ </template>
- <button
- type="button"
- class="btn btn-danger"
- @click="toggleOpen(true)">
- {{ s__('Profiles|Delete account') }}
- </button>
- </div>
+ </modal>
</template>
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 635056e0eeb..a93bc935dd0 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -1,7 +1,12 @@
import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+
import deleteAccountModal from './components/delete_account_modal.vue';
+Vue.use(Translate);
+
+const deleteAccountButton = document.getElementById('delete-account-button');
const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new
new Vue({
@@ -9,6 +14,9 @@ new Vue({
components: {
deleteAccountModal,
},
+ mounted() {
+ deleteAccountButton.classList.remove('disabled');
+ },
render(createElement) {
return createElement('delete-account-modal', {
props: {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 0dc02f012e4..ba4ac850346 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,4 +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 Cookies from 'js-cookie';
import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
@@ -7,6 +8,8 @@ import { getPagePath } from '../lib/utils/common_utils';
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user');
+ this.newRepoActivated = Cookies.get('new_repo');
+ this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
}
@@ -25,6 +28,7 @@ import { getPagePath } from '../lib/utils/common_utils';
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+ $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
@@ -82,6 +86,23 @@ import { getPagePath } from '../lib/utils/common_utils';
}
});
}
+
+ setNewRepoCookie() {
+ if (this.value === 'off') {
+ Cookies.remove('new_repo');
+ } else {
+ Cookies.set('new_repo', true, { expires_in: 365 });
+ }
+ }
+
+ setRepoRadio() {
+ const multiEditRadios = $('input[name="user[multi_file]"]');
+ if (this.newRepoActivated || this.newRepoActivated === 'true') {
+ multiEditRadios.filter('[value=on]').prop('checked', true);
+ } else {
+ multiEditRadios.filter('[value=off]').prop('checked', true);
+ }
+ }
}
$(function() {
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 3131e71d9d6..d4f26b81f30 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie';
+import { visitUrl } from './lib/utils/url_utility';
import projectSelect from './project_select';
export default class Project {
@@ -122,7 +123,7 @@ export default class Project {
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) {
- gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
+ visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
},
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 19682b20a4a..0da32b4a3cc 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -2,169 +2,163 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-(function() {
- this.ProjectFindFile = (function() {
- var highlighter;
-
- function ProjectFindFile(element1, options) {
- this.element = element1;
- this.options = options;
- this.goToBlob = this.goToBlob.bind(this);
- this.goToTree = this.goToTree.bind(this);
- this.selectRowDown = this.selectRowDown.bind(this);
- this.selectRowUp = this.selectRowUp.bind(this);
- this.filePaths = {};
- this.inputElement = this.element.find(".file-finder-input");
- // init event
- this.initEvent();
- // focus text input box
- this.inputElement.focus();
- // load file list
- this.load(this.options.url);
+// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
+const highlighter = function(element, text, matches) {
+ var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
+ lastIndex = 0;
+ highlightText = "";
+ matchedChars = [];
+ for (j = 0, len = matches.length; j < len; j += 1) {
+ matchIndex = matches[j];
+ unmatched = text.substring(lastIndex, matchIndex);
+ if (unmatched) {
+ if (matchedChars.length) {
+ element.append(matchedChars.join("").bold());
+ }
+ matchedChars = [];
+ element.append(document.createTextNode(unmatched));
}
-
- ProjectFindFile.prototype.initEvent = function() {
- this.inputElement.off("keyup");
- this.inputElement.on("keyup", (function(_this) {
- return function(event) {
- var oldValue, ref, target, value;
- target = $(event.target);
- value = target.val();
- oldValue = (ref = target.data("oldValue")) != null ? ref : "";
- if (value !== oldValue) {
- target.data("oldValue", value);
- _this.findFile();
- return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus();
- }
- };
- })(this));
- };
-
- ProjectFindFile.prototype.findFile = function() {
- var result, searchText;
- searchText = this.inputElement.val();
- result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
- return this.renderList(result, searchText);
- // find file
- };
+ matchedChars.push(text[matchIndex]);
+ lastIndex = matchIndex + 1;
+ }
+ if (matchedChars.length) {
+ element.append(matchedChars.join("").bold());
+ }
+ return element.append(document.createTextNode(text.substring(lastIndex)));
+};
+
+export default class ProjectFindFile {
+ constructor(element1, options) {
+ this.element = element1;
+ this.options = options;
+ this.goToBlob = this.goToBlob.bind(this);
+ this.goToTree = this.goToTree.bind(this);
+ this.selectRowDown = this.selectRowDown.bind(this);
+ this.selectRowUp = this.selectRowUp.bind(this);
+ this.filePaths = {};
+ this.inputElement = this.element.find(".file-finder-input");
+ // init event
+ this.initEvent();
+ // focus text input box
+ this.inputElement.focus();
+ // load file list
+ this.load(this.options.url);
+ }
+
+ initEvent() {
+ this.inputElement.off("keyup");
+ this.inputElement.on("keyup", (function(_this) {
+ return function(event) {
+ var oldValue, ref, target, value;
+ target = $(event.target);
+ value = target.val();
+ oldValue = (ref = target.data("oldValue")) != null ? ref : "";
+ if (value !== oldValue) {
+ target.data("oldValue", value);
+ _this.findFile();
+ return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus();
+ }
+ };
+ })(this));
+ }
+
+ findFile() {
+ var result, searchText;
+ searchText = this.inputElement.val();
+ result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
+ return this.renderList(result, searchText);
+ // find file
+ }
// files pathes load
- ProjectFindFile.prototype.load = function(url) {
- return $.ajax({
- url: url,
- method: "get",
- dataType: "json",
- success: (function(_this) {
- return function(data) {
- _this.element.find(".loading").hide();
- _this.filePaths = data;
- _this.findFile();
- return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus();
- };
- })(this)
- });
- };
+ load(url) {
+ return $.ajax({
+ url: url,
+ method: "get",
+ dataType: "json",
+ success: (function(_this) {
+ return function(data) {
+ _this.element.find(".loading").hide();
+ _this.filePaths = data;
+ _this.findFile();
+ return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus();
+ };
+ })(this)
+ });
+ }
// render result
- ProjectFindFile.prototype.renderList = function(filePaths, searchText) {
- var blobItemUrl, filePath, html, i, j, len, matches, results;
- this.element.find(".tree-table > tbody").empty();
- results = [];
- for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
- filePath = filePaths[i];
- if (i === 20) {
- break;
- }
- if (searchText) {
- matches = fuzzaldrinPlus.match(filePath, searchText);
- }
- blobItemUrl = this.options.blobUrlTemplate + "/" + filePath;
- html = this.makeHtml(filePath, matches, blobItemUrl);
- results.push(this.element.find(".tree-table > tbody").append(html));
- }
- return results;
- };
-
- // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
- highlighter = function(element, text, matches) {
- var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
- lastIndex = 0;
- highlightText = "";
- matchedChars = [];
- for (j = 0, len = matches.length; j < len; j += 1) {
- matchIndex = matches[j];
- unmatched = text.substring(lastIndex, matchIndex);
- if (unmatched) {
- if (matchedChars.length) {
- element.append(matchedChars.join("").bold());
- }
- matchedChars = [];
- element.append(document.createTextNode(unmatched));
- }
- matchedChars.push(text[matchIndex]);
- lastIndex = matchIndex + 1;
- }
- if (matchedChars.length) {
- element.append(matchedChars.join("").bold());
+ renderList(filePaths, searchText) {
+ var blobItemUrl, filePath, html, i, j, len, matches, results;
+ this.element.find(".tree-table > tbody").empty();
+ results = [];
+ for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
+ filePath = filePaths[i];
+ if (i === 20) {
+ break;
}
- return element.append(document.createTextNode(text.substring(lastIndex)));
- };
-
- // make tbody row html
- ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
- var $tr;
- $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
- if (matches) {
- $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
- } else {
- $tr.find("a").attr("href", blobItemUrl);
- $tr.find(".str-truncated").text(filePath);
+ if (searchText) {
+ matches = fuzzaldrinPlus.match(filePath, searchText);
}
- return $tr;
- };
-
- ProjectFindFile.prototype.selectRow = function(type) {
- var next, rows, selectedRow;
- rows = this.element.find(".files-slider tr.tree-item");
- selectedRow = this.element.find(".files-slider tr.tree-item.selected");
- if (rows && rows.length > 0) {
- if (selectedRow && selectedRow.length > 0) {
- if (type === "UP") {
- next = selectedRow.prev();
- } else if (type === "DOWN") {
- next = selectedRow.next();
- }
- if (next.length > 0) {
- selectedRow.removeClass("selected");
- selectedRow = next;
- }
- } else {
- selectedRow = rows.eq(0);
+ blobItemUrl = this.options.blobUrlTemplate + "/" + filePath;
+ html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
+ results.push(this.element.find(".tree-table > tbody").append(html));
+ }
+ return results;
+ }
+
+ // make tbody row html
+ static makeHtml(filePath, matches, blobItemUrl) {
+ var $tr;
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
+ if (matches) {
+ $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
+ } else {
+ $tr.find("a").attr("href", blobItemUrl);
+ $tr.find(".str-truncated").text(filePath);
+ }
+ return $tr;
+ }
+
+ selectRow(type) {
+ var next, rows, selectedRow;
+ rows = this.element.find(".files-slider tr.tree-item");
+ selectedRow = this.element.find(".files-slider tr.tree-item.selected");
+ if (rows && rows.length > 0) {
+ if (selectedRow && selectedRow.length > 0) {
+ if (type === "UP") {
+ next = selectedRow.prev();
+ } else if (type === "DOWN") {
+ next = selectedRow.next();
+ }
+ if (next.length > 0) {
+ selectedRow.removeClass("selected");
+ selectedRow = next;
}
- return selectedRow.addClass("selected").focus();
+ } else {
+ selectedRow = rows.eq(0);
}
- };
-
- ProjectFindFile.prototype.selectRowUp = function() {
- return this.selectRow("UP");
- };
+ return selectedRow.addClass("selected").focus();
+ }
+ }
- ProjectFindFile.prototype.selectRowDown = function() {
- return this.selectRow("DOWN");
- };
+ selectRowUp() {
+ return this.selectRow("UP");
+ }
- ProjectFindFile.prototype.goToTree = function() {
- return location.href = this.options.treeUrl;
- };
+ selectRowDown() {
+ return this.selectRow("DOWN");
+ }
- ProjectFindFile.prototype.goToBlob = function() {
- var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
+ goToTree() {
+ return location.href = this.options.treeUrl;
+ }
- if ($link.length) {
- $link.get(0).click();
- }
- };
+ goToBlob() {
+ var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
- return ProjectFindFile;
- })();
-}).call(window);
+ if ($link.length) {
+ $link.get(0).click();
+ }
+ }
+}
diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js
deleted file mode 100644
index 567c311f119..00000000000
--- a/app/assets/javascripts/project_variables.js
+++ /dev/null
@@ -1,39 +0,0 @@
-
-const HIDDEN_VALUE_TEXT = '******';
-
-export default class ProjectVariables {
- constructor() {
- this.$revealBtn = $('.js-btn-toggle-reveal-values');
- this.$revealBtn.on('click', this.toggleRevealState.bind(this));
- }
-
- toggleRevealState(e) {
- e.preventDefault();
-
- const oldStatus = this.$revealBtn.attr('data-status');
- let newStatus = 'hidden';
- let newAction = 'Reveal Values';
-
- if (oldStatus === 'hidden') {
- newStatus = 'revealed';
- newAction = 'Hide Values';
- }
-
- this.$revealBtn.attr('data-status', newStatus);
-
- const $variables = $('.variable-value');
-
- $variables.each((_, variable) => {
- const $variable = $(variable);
- let newText = HIDDEN_VALUE_TEXT;
-
- if (newStatus === 'revealed') {
- newText = $variable.attr('data-value');
- }
-
- $variable.text(newText);
- });
-
- this.$revealBtn.text(newAction);
- }
-}
diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js
index c34927499fc..cec6f0dd5a3 100644
--- a/app/assets/javascripts/projects/project_import_gitlab_project.js
+++ b/app/assets/javascripts/projects/project_import_gitlab_project.js
@@ -1,7 +1,7 @@
-import '../lib/utils/url_utility';
+import { getParameterValues } from '../lib/utils/url_utility';
const bindEvents = () => {
- const path = gl.utils.getParameterValues('path')[0];
+ const path = getParameterValues('path')[0];
// get the path url and append it in the inputS
$('.js-path-name').val(path);
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 3ecc0c2a6e5..4710e70d619 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,6 +1,7 @@
let hasUserDefinedProjectPath = false;
-const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
+const deriveProjectPathFromUrl = ($projectImportUrl) => {
+ const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path');
if (hasUserDefinedProjectPath) {
return;
}
@@ -21,7 +22,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
// extract everything after the last slash
const pathMatch = /\/([^/]+)$/.exec(importUrl);
if (pathMatch) {
- $projectPath.val(pathMatch[1]);
+ $currentProjectPath.val(pathMatch[1]);
}
};
@@ -96,7 +97,7 @@ const bindEvents = () => {
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
});
- $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath));
+ $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
};
document.addEventListener('DOMContentLoaded', bindEvents);
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index c91a0d9ba41..5482c55f8bb 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -1,12 +1,12 @@
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
-
+import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math & mermaid diagrams.
//
$.fn.renderGFM = function renderGFM() {
- this.find('.js-syntax-highlight').syntaxHighlight();
+ syntaxHighlight(this.find('.js-syntax-highlight'));
renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid'));
return this;
diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js
index 41942c04a4e..b7cde6fb092 100644
--- a/app/assets/javascripts/render_mermaid.js
+++ b/app/assets/javascripts/render_mermaid.js
@@ -24,7 +24,25 @@ export default function renderMermaid($els) {
});
$els.each((i, el) => {
- mermaid.init(undefined, el);
+ const source = el.textContent;
+
+ mermaid.init(undefined, el, (id) => {
+ const svg = document.getElementById(id);
+
+ svg.classList.add('mermaid');
+
+ // pre > code > svg
+ svg.closest('pre').replaceWith(svg);
+
+ // We need to add the original source into the DOM to allow Copy-as-GFM
+ // to access it.
+ const sourceEl = document.createElement('text');
+ sourceEl.classList.add('source');
+ sourceEl.setAttribute('display', 'none');
+ sourceEl.textContent = source;
+
+ svg.appendChild(sourceEl);
+ });
});
}).catch((err) => {
Flash(`Can't load mermaid module: ${err}`);
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list.vue b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
deleted file mode 100644
index fb862e7bf01..00000000000
--- a/app/assets/javascripts/repo/components/commit_sidebar/list.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
-
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
- collapsed: {
- type: Boolean,
- required: true,
- },
- },
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
- },
- };
-</script>
-
-<template>
- <div class="multi-file-commit-panel-section">
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': collapsed,
- }"
- >
- <icon
- name="list-bulleted"
- :size="18"
- css-classes="append-right-default"
- />
- <template v-if="!collapsed">
- {{ title }}
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click="toggleCollapsed"
- >
- <i
- aria-hidden="true"
- class="fa fa-angle-double-right"
- >
- </i>
- </button>
- </template>
- </header>
- <div class="multi-file-commit-list">
- <list-collapsed
- v-if="collapsed"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="list-unstyled append-bottom-0"
- >
- <li
- v-for="file in fileList"
- :key="file.key"
- >
- <list-item
- :file="file"
- />
- </li>
- </ul>
- <div
- v-else
- class="help-block prepend-top-0"
- >
- No changes
- </div>
- </template>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
deleted file mode 100644
index 781404cf8ca..00000000000
--- a/app/assets/javascripts/repo/components/new_dropdown/index.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import newModal from './modal.vue';
- import upload from './upload.vue';
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- newModal,
- upload,
- },
- data() {
- return {
- openModal: false,
- modalType: '',
- };
- },
- computed: {
- ...mapState([
- 'path',
- ]),
- },
- methods: {
- createNewItem(type) {
- this.modalType = type;
- this.toggleModalOpen();
- },
- toggleModalOpen() {
- this.openModal = !this.openModal;
- },
- },
- };
-</script>
-
-<template>
- <div>
- <ul class="breadcrumb repo-breadcrumb">
- <li class="dropdown">
- <button
- type="button"
- class="btn btn-default dropdown-toggle add-to-tree"
- data-toggle="dropdown"
- aria-label="Create new file or directory"
- >
- <icon
- name="plus"
- css-classes="pull-left"
- />
- <icon
- name="arrow-down"
- css-classes="pull-left"
- />
- </button>
- <ul class="dropdown-menu">
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('blob')"
- >
- {{ __('New file') }}
- </a>
- </li>
- <li>
- <upload
- :path="path"
- />
- </li>
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('tree')"
- >
- {{ __('New directory') }}
- </a>
- </li>
- </ul>
- </li>
- </ul>
- <new-modal
- v-if="openModal"
- :type="modalType"
- :path="path"
- @toggle="toggleModalOpen"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
deleted file mode 100644
index a00e1e9d809..00000000000
--- a/app/assets/javascripts/repo/components/repo.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { mapState, mapGetters } from 'vuex';
-import RepoSidebar from './repo_sidebar.vue';
-import RepoCommitSection from './repo_commit_section.vue';
-import RepoTabs from './repo_tabs.vue';
-import RepoFileButtons from './repo_file_buttons.vue';
-import RepoPreview from './repo_preview.vue';
-import repoEditor from './repo_editor.vue';
-
-export default {
- computed: {
- ...mapState([
- 'currentBlobView',
- ]),
- ...mapGetters([
- 'isCollapsed',
- 'changedFiles',
- ]),
- },
- components: {
- RepoSidebar,
- RepoTabs,
- RepoFileButtons,
- repoEditor,
- RepoCommitSection,
- RepoPreview,
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = (e) => {
- if (!this.changedFiles.length) return undefined;
-
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
- },
-};
-</script>
-
-<template>
- <div
- class="multi-file"
- :class="{
- 'is-collapsed': isCollapsed
- }"
- >
- <repo-sidebar/>
- <div
- v-if="isCollapsed"
- class="multi-file-edit-pane"
- >
- <repo-tabs />
- <component
- class="multi-file-edit-pane-content"
- :is="currentBlobView"
- />
- <repo-file-buttons />
- </div>
- <repo-commit-section />
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
deleted file mode 100644
index 4ea21913129..00000000000
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
-import RepoPreviousDirectory from './repo_prev_directory.vue';
-import RepoFile from './repo_file.vue';
-import RepoLoadingFile from './repo_loading_file.vue';
-
-export default {
- components: {
- 'repo-previous-directory': RepoPreviousDirectory,
- 'repo-file': RepoFile,
- 'repo-loading-file': RepoLoadingFile,
- },
- created() {
- window.addEventListener('popstate', this.popHistoryState);
- },
- destroyed() {
- window.removeEventListener('popstate', this.popHistoryState);
- },
- mounted() {
- this.getTreeData();
- },
- computed: {
- ...mapState([
- 'loading',
- 'isRoot',
- ]),
- ...mapState({
- projectName(state) {
- return state.project.name;
- },
- }),
- ...mapGetters([
- 'treeList',
- 'isCollapsed',
- ]),
- },
- methods: {
- ...mapActions([
- 'getTreeData',
- 'popHistoryState',
- ]),
- },
-};
-</script>
-
-<template>
-<div class="ide-file-list">
- <table class="table">
- <thead>
- <tr>
- <th
- v-if="isCollapsed"
- >
- </th>
- <template v-else>
- <th class="name multi-file-table-name">
- Name
- </th>
- <th class="hidden-sm hidden-xs last-commit">
- Last commit
- </th>
- <th class="hidden-xs last-update text-right">
- Last update
- </th>
- </template>
- </tr>
- </thead>
- <tbody>
- <repo-previous-directory
- v-if="!isRoot && treeList.length"
- />
- <repo-loading-file
- v-if="!treeList.length && loading"
- v-for="n in 5"
- :key="n"
- />
- <repo-file
- v-for="file in treeList"
- :key="file.key"
- :file="file"
- />
- </tbody>
- </table>
-</div>
-</template>
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
deleted file mode 100644
index b6801af7fcb..00000000000
--- a/app/assets/javascripts/repo/index.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import { mapActions } from 'vuex';
-import { convertPermissionToBoolean } from '../lib/utils/common_utils';
-import Repo from './components/repo.vue';
-import RepoEditButton from './components/repo_edit_button.vue';
-import newBranchForm from './components/new_branch_form.vue';
-import newDropdown from './components/new_dropdown/index.vue';
-import store from './stores';
-import Translate from '../vue_shared/translate';
-
-function initRepo(el) {
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- components: {
- repo: Repo,
- },
- methods: {
- ...mapActions([
- 'setInitialData',
- ]),
- },
- created() {
- const data = el.dataset;
-
- this.setInitialData({
- project: {
- id: data.projectId,
- name: data.projectName,
- url: data.projectUrl,
- },
- endpoints: {
- rootEndpoint: data.url,
- newMergeRequestUrl: data.newMergeRequestUrl,
- rootUrl: data.rootUrl,
- },
- canCommit: convertPermissionToBoolean(data.canCommit),
- onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
- currentRef: data.ref,
- path: data.currentPath,
- currentBranch: data.currentBranch,
- isRoot: convertPermissionToBoolean(data.root),
- isInitialRoot: convertPermissionToBoolean(data.root),
- });
- },
- render(createElement) {
- return createElement('repo');
- },
- });
-}
-
-function initRepoEditButton(el) {
- return new Vue({
- el,
- store,
- components: {
- repoEditButton: RepoEditButton,
- },
- render(createElement) {
- return createElement('repo-edit-button');
- },
- });
-}
-
-function initNewDropdown(el) {
- return new Vue({
- el,
- store,
- components: {
- newDropdown,
- },
- render(createElement) {
- return createElement('new-dropdown');
- },
- });
-}
-
-function initNewBranchForm() {
- const el = document.querySelector('.js-new-branch-dropdown');
-
- if (!el) return null;
-
- return new Vue({
- el,
- components: {
- newBranchForm,
- },
- store,
- render(createElement) {
- return createElement('new-branch-form');
- },
- });
-}
-
-const repo = document.getElementById('repo');
-const editButton = document.querySelector('.editable-mode');
-const newDropdownHolder = document.querySelector('.js-new-dropdown');
-
-Vue.use(Translate);
-
-initRepo(repo);
-initRepoEditButton(editButton);
-initNewBranchForm();
-initNewDropdown(newDropdownHolder);
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
deleted file mode 100644
index 120ce96f44d..00000000000
--- a/app/assets/javascripts/repo/stores/actions.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import Vue from 'vue';
-import flash from '../../flash';
-import service from '../services';
-import * as types from './mutation_types';
-
-export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
-
-export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
-
-export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
-
-export const discardAllChanges = ({ commit, getters, dispatch }) => {
- const changedFiles = getters.changedFiles;
-
- changedFiles.forEach((file) => {
- commit(types.DISCARD_FILE_CHANGES, file);
-
- if (file.tempFile) {
- dispatch('closeFile', { file, force: true });
- }
- });
-};
-
-export const closeAllFiles = ({ state, dispatch }) => {
- state.openFiles.forEach(file => dispatch('closeFile', { file }));
-};
-
-export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
- const changedFiles = getters.changedFiles;
-
- if (changedFiles.length && !force) {
- commit(types.TOGGLE_DISCARD_POPUP, true);
- } else {
- commit(types.TOGGLE_EDIT_MODE);
- commit(types.TOGGLE_DISCARD_POPUP, false);
- dispatch('toggleBlobView');
-
- if (!state.editMode) {
- dispatch('discardAllChanges');
- }
- }
-};
-
-export const toggleBlobView = ({ commit, state }) => {
- if (state.editMode) {
- commit(types.SET_EDIT_MODE);
- } else {
- commit(types.SET_PREVIEW_MODE);
- }
-};
-
-export const checkCommitStatus = ({ state }) => service.getBranchData(
- state.project.id,
- state.currentBranch,
-)
- .then((data) => {
- const { id } = data.commit;
-
- if (state.currentRef !== id) {
- return true;
- }
-
- return false;
- })
- .catch(() => flash('Error checking branch data. Please try again.'));
-
-export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
- service.commit(state.project.id, payload)
- .then((data) => {
- const { branch } = payload;
- if (!data.short_id) {
- flash(data.message);
- return;
- }
-
- const lastCommit = {
- commit_path: `${state.project.url}/commit/${data.id}`,
- commit: {
- message: data.message,
- authored_date: data.committed_date,
- },
- };
-
- flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
-
- if (newMr) {
- dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
- } else {
- commit(types.SET_COMMIT_REF, data.id);
-
- getters.changedFiles.forEach((entry) => {
- commit(types.SET_LAST_COMMIT_DATA, {
- entry,
- lastCommit,
- });
- });
-
- dispatch('discardAllChanges');
- dispatch('closeAllFiles');
- dispatch('toggleEditMode');
-
- window.scrollTo(0, 0);
- }
- })
- .catch(() => flash('Error committing changes. Please try again.'));
-
-export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
- if (type === 'tree') {
- dispatch('createTempTree', name);
- } else if (type === 'blob') {
- dispatch('createTempFile', {
- tree: state,
- name,
- base64,
- content,
- });
- }
-};
-
-export const popHistoryState = ({ state, dispatch, getters }) => {
- const treeList = getters.treeList;
- const tree = treeList.find(file => file.url === state.previousUrl);
-
- if (!tree) return;
-
- if (tree.type === 'tree') {
- dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
- }
-};
-
-export const scrollToTab = () => {
- Vue.nextTick(() => {
- const tabs = document.getElementById('tabs');
-
- if (tabs) {
- const tabEl = tabs.querySelector('.active .repo-tab');
-
- tabEl.focus();
- }
- });
-};
-
-export * from './actions/tree';
-export * from './actions/file';
-export * from './actions/branch';
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
deleted file mode 100644
index 61d9a5af3e3..00000000000
--- a/app/assets/javascripts/repo/stores/actions/branch.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import service from '../../services';
-import * as types from '../mutation_types';
-import { pushState } from '../utils';
-
-// eslint-disable-next-line import/prefer-default-export
-export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
- state.project.id,
- {
- branch,
- ref: state.currentBranch,
- },
-).then(res => res.json())
-.then((data) => {
- const branchName = data.name;
- const url = location.href.replace(state.currentBranch, branchName);
-
- pushState(url);
-
- commit(types.SET_CURRENT_BRANCH, branchName);
-});
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
deleted file mode 100644
index aa830e946a2..00000000000
--- a/app/assets/javascripts/repo/stores/actions/tree.js
+++ /dev/null
@@ -1,162 +0,0 @@
-import { normalizeHeaders } from '../../../lib/utils/common_utils';
-import flash from '../../../flash';
-import service from '../../services';
-import * as types from '../mutation_types';
-import {
- pushState,
- setPageTitle,
- findEntry,
- createTemp,
- createOrMergeEntry,
-} from '../utils';
-
-export const getTreeData = (
- { commit, state, dispatch },
- { endpoint = state.endpoints.rootEndpoint, tree = state } = {},
-) => {
- commit(types.TOGGLE_LOADING, tree);
-
- service.getTreeData(endpoint)
- .then((res) => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
- setPageTitle(pageTitle);
-
- return res.json();
- })
- .then((data) => {
- const prevLastCommitPath = tree.lastCommitPath;
- if (!state.isInitialRoot) {
- commit(types.SET_ROOT, data.path === '/');
- }
-
- dispatch('updateDirectoryData', { data, tree });
- commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
- commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
- commit(types.TOGGLE_LOADING, tree);
-
- if (prevLastCommitPath !== null) {
- dispatch('getLastCommitData', tree);
- }
-
- pushState(endpoint);
- })
- .catch(() => {
- flash('Error loading tree data. Please try again.');
- commit(types.TOGGLE_LOADING, tree);
- });
-};
-
-export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
- if (tree.opened) {
- // send empty data to clear the tree
- const data = { trees: [], blobs: [], submodules: [] };
-
- pushState(tree.parentTreeUrl);
-
- commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
- dispatch('updateDirectoryData', { data, tree });
- } else {
- commit(types.SET_PREVIOUS_URL, endpoint);
- dispatch('getTreeData', { endpoint, tree });
- }
-
- commit(types.TOGGLE_TREE_OPEN, tree);
-};
-
-export const clickedTreeRow = ({ commit, dispatch }, row) => {
- if (row.type === 'tree') {
- dispatch('toggleTreeOpen', {
- endpoint: row.url,
- tree: row,
- });
- } else if (row.type === 'submodule') {
- commit(types.TOGGLE_LOADING, row);
-
- gl.utils.visitUrl(row.url);
- } else if (row.type === 'blob' && row.opened) {
- dispatch('setFileActive', row);
- } else {
- dispatch('getFileData', row);
- }
-};
-
-export const createTempTree = ({ state, commit, dispatch }, name) => {
- let tree = state;
- const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
-
- dirNames.forEach((dirName) => {
- const foundEntry = findEntry(tree, 'tree', dirName);
-
- if (!foundEntry) {
- const tmpEntry = createTemp({
- name: dirName,
- path: tree.path,
- type: 'tree',
- level: tree.level !== undefined ? tree.level + 1 : 0,
- });
-
- commit(types.CREATE_TMP_TREE, {
- parent: tree,
- tmpEntry,
- });
- commit(types.TOGGLE_TREE_OPEN, tmpEntry);
-
- tree = tmpEntry;
- } else {
- tree = foundEntry;
- }
- });
-
- if (tree.tempFile) {
- dispatch('createTempFile', {
- tree,
- name: '.gitkeep',
- });
- }
-};
-
-export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
- if (tree.lastCommitPath === null || getters.isCollapsed) return;
-
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
- const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
-
- commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
-
- return res.json();
- })
- .then((data) => {
- data.forEach((lastCommit) => {
- const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
-
- if (entry) {
- commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
- }
- });
-
- dispatch('getLastCommitData', tree);
- })
- .catch(() => flash('Error fetching log data.'));
-};
-
-export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
- const level = tree.level !== undefined ? tree.level + 1 : 0;
- const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
- const createEntry = (entry, type) => createOrMergeEntry({
- tree,
- entry,
- level,
- type,
- parentTreeUrl,
- });
-
- const formattedData = [
- ...data.trees.map(t => createEntry(t, 'tree')),
- ...data.submodules.map(m => createEntry(m, 'submodule')),
- ...data.blobs.map(b => createEntry(b, 'blob')),
- ];
-
- commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
-};
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
deleted file mode 100644
index 5ce9f449905..00000000000
--- a/app/assets/javascripts/repo/stores/getters.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import _ from 'underscore';
-
-/*
- Takes the multi-dimensional tree and returns a flattened array.
- This allows for the table to recursively render the table rows but keeps the data
- structure nested to make it easier to add new files/directories.
-*/
-export const treeList = (state) => {
- const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
-
- return _.chain(state.tree)
- .map(arr => [arr, mapTree(arr)])
- .flatten()
- .value();
-};
-
-export const changedFiles = state => state.openFiles.filter(file => file.changed);
-
-export const activeFile = state => state.openFiles.find(file => file.active);
-
-export const activeFileExtension = (state) => {
- const file = activeFile(state);
- return file ? `.${file.path.split('.').pop()}` : '';
-};
-
-export const isCollapsed = state => !!state.openFiles.length;
-
-export const canEditFile = (state) => {
- const currentActiveFile = activeFile(state);
- const openedFiles = state.openFiles;
-
- return state.canCommit &&
- state.onTopOfBranch &&
- openedFiles.length &&
- (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
-};
-
-export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
-
-export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js
deleted file mode 100644
index d8229e8a620..00000000000
--- a/app/assets/javascripts/repo/stores/mutations/branch.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as types from '../mutation_types';
-
-export default {
- [types.SET_CURRENT_BRANCH](state, currentBranch) {
- Object.assign(state, {
- currentBranch,
- });
- },
-};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index fa7f6825d7e..b830fcf7e80 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -3,226 +3,228 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
-(function() {
- this.Sidebar = (function() {
- function Sidebar(currentUser) {
- this.toggleTodo = this.toggleTodo.bind(this);
- this.sidebar = $('aside');
-
- this.removeListeners();
- this.addEventListeners();
+function Sidebar(currentUser) {
+ this.toggleTodo = this.toggleTodo.bind(this);
+ this.sidebar = $('aside');
+
+ this.removeListeners();
+ this.addEventListeners();
+}
+
+Sidebar.initialize = function(currentUser) {
+ if (!this.instance) {
+ this.instance = new Sidebar(currentUser);
+ }
+};
+
+Sidebar.prototype.removeListeners = function () {
+ this.sidebar.off('click', '.sidebar-collapsed-icon');
+ this.sidebar.off('hidden.gl.dropdown');
+ $('.dropdown').off('loading.gl.dropdown');
+ $('.dropdown').off('loaded.gl.dropdown');
+ $(document).off('click', '.js-sidebar-toggle');
+};
+
+Sidebar.prototype.addEventListeners = function() {
+ const $document = $(document);
+
+ this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
+ this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
+ $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
+ $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
+
+ $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
+ return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
+};
+
+Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
+ var $allGutterToggleIcons, $this, $thisIcon;
+ e.preventDefault();
+ $this = $(this);
+ $thisIcon = $this.find('i');
+ $allGutterToggleIcons = $('.js-sidebar-toggle i');
+ if ($thisIcon.hasClass('fa-angle-double-right')) {
+ $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
+ $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ } else {
+ $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
+ $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+
+ if (gl.lazyLoader) gl.lazyLoader.loadCheck();
+ }
+ if (!triggered) {
+ Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
+ }
+};
+
+Sidebar.prototype.toggleTodo = function(e) {
+ var $btnText, $this, $todoLoading, ajaxType, url;
+ $this = $(e.currentTarget);
+ ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
+ if ($this.attr('data-delete-path')) {
+ url = "" + ($this.attr('data-delete-path'));
+ } else {
+ url = "" + ($this.data('url'));
+ }
+
+ $this.tooltip('hide');
+
+ return $.ajax({
+ url: url,
+ type: ajaxType,
+ dataType: 'json',
+ data: {
+ issuable_id: $this.data('issuable-id'),
+ issuable_type: $this.data('issuable-type')
+ },
+ beforeSend: (function(_this) {
+ return function() {
+ $('.js-issuable-todo').disable()
+ .addClass('is-loading');
+ };
+ })(this)
+ }).done((function(_this) {
+ return function(data) {
+ return _this.todoUpdateDone(data);
+ };
+ })(this));
+};
+
+Sidebar.prototype.todoUpdateDone = function(data) {
+ const deletePath = data.delete_path ? data.delete_path : null;
+ const attrPrefix = deletePath ? 'mark' : 'todo';
+ const $todoBtns = $('.js-issuable-todo');
+
+ $(document).trigger('todo:toggle', data.count);
+
+ $todoBtns.each((i, el) => {
+ const $el = $(el);
+ const $elText = $el.find('.js-issuable-todo-inner');
+
+ $el.removeClass('is-loading')
+ .enable()
+ .attr('aria-label', $el.data(`${attrPrefix}-text`))
+ .attr('data-delete-path', deletePath)
+ .attr('title', $el.data(`${attrPrefix}-text`));
+
+ if ($el.hasClass('has-tooltip')) {
+ $el.tooltip('fixTitle');
}
- Sidebar.prototype.removeListeners = function () {
- this.sidebar.off('click', '.sidebar-collapsed-icon');
- this.sidebar.off('hidden.gl.dropdown');
- $('.dropdown').off('loading.gl.dropdown');
- $('.dropdown').off('loaded.gl.dropdown');
- $(document).off('click', '.js-sidebar-toggle');
- };
-
- Sidebar.prototype.addEventListeners = function() {
- const $document = $(document);
-
- this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
- this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
- $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
- $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
-
- $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
- return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
- };
-
- Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
- e.preventDefault();
- $this = $(this);
- $thisIcon = $this.find('i');
- $allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
- $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
- $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- } else {
- $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
- $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
-
- if (gl.lazyLoader) gl.lazyLoader.loadCheck();
- }
- if (!triggered) {
- Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
- }
- };
-
- Sidebar.prototype.toggleTodo = function(e) {
- var $btnText, $this, $todoLoading, ajaxType, url;
- $this = $(e.currentTarget);
- ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
- if ($this.attr('data-delete-path')) {
- url = "" + ($this.attr('data-delete-path'));
- } else {
- url = "" + ($this.data('url'));
- }
-
- $this.tooltip('hide');
-
- return $.ajax({
- url: url,
- type: ajaxType,
- dataType: 'json',
- data: {
- issuable_id: $this.data('issuable-id'),
- issuable_type: $this.data('issuable-type')
- },
- beforeSend: (function(_this) {
- return function() {
- $('.js-issuable-todo').disable()
- .addClass('is-loading');
- };
- })(this)
- }).done((function(_this) {
- return function(data) {
- return _this.todoUpdateDone(data);
- };
- })(this));
- };
-
- Sidebar.prototype.todoUpdateDone = function(data) {
- const deletePath = data.delete_path ? data.delete_path : null;
- const attrPrefix = deletePath ? 'mark' : 'todo';
- const $todoBtns = $('.js-issuable-todo');
-
- $(document).trigger('todo:toggle', data.count);
-
- $todoBtns.each((i, el) => {
- const $el = $(el);
- const $elText = $el.find('.js-issuable-todo-inner');
-
- $el.removeClass('is-loading')
- .enable()
- .attr('aria-label', $el.data(`${attrPrefix}-text`))
- .attr('data-delete-path', deletePath)
- .attr('title', $el.data(`${attrPrefix}-text`));
-
- if ($el.hasClass('has-tooltip')) {
- $el.tooltip('fixTitle');
- }
-
- if ($el.data(`${attrPrefix}-icon`)) {
- $elText.html($el.data(`${attrPrefix}-icon`));
- } else {
- $elText.text($el.data(`${attrPrefix}-text`));
- }
- });
- };
-
- Sidebar.prototype.sidebarDropdownLoading = function(e) {
- var $loading, $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
- img = $sidebarCollapsedIcon.find('img');
- i = $sidebarCollapsedIcon.find('i');
- $loading = $('<i class="fa fa-spinner fa-spin"></i>');
- if (img.length) {
- img.before($loading);
- return img.hide();
- } else if (i.length) {
- i.before($loading);
- return i.hide();
- }
- };
-
- Sidebar.prototype.sidebarDropdownLoaded = function(e) {
- var $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
- img = $sidebarCollapsedIcon.find('img');
- $sidebarCollapsedIcon.find('i.fa-spin').remove();
- i = $sidebarCollapsedIcon.find('i');
- if (img.length) {
- return img.show();
- } else {
- return i.show();
- }
- };
-
- Sidebar.prototype.sidebarCollapseClicked = function(e) {
- var $block, sidebar;
- if ($(e.currentTarget).hasClass('dont-change-state')) {
- return;
- }
- sidebar = e.data;
- e.preventDefault();
- $block = $(this).closest('.block');
- return sidebar.openDropdown($block);
- };
-
- Sidebar.prototype.openDropdown = function(blockOrName) {
- var $block;
- $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
- if (!this.isOpen()) {
- this.setCollapseAfterUpdate($block);
- this.toggleSidebar('open');
- }
-
- // Wait for the sidebar to trigger('click') open
- // so it doesn't cause our dropdown to close preemptively
- setTimeout(() => {
- $block.find('.js-sidebar-dropdown-toggle').trigger('click');
- });
- };
-
- Sidebar.prototype.setCollapseAfterUpdate = function($block) {
- $block.addClass('collapse-after-update');
- return $('.page-with-sidebar').addClass('with-overlay');
- };
-
- Sidebar.prototype.onSidebarDropdownHidden = function(e) {
- var $block, sidebar;
- sidebar = e.data;
- e.preventDefault();
- $block = $(e.target).closest('.block');
- return sidebar.sidebarDropdownHidden($block);
- };
-
- Sidebar.prototype.sidebarDropdownHidden = function($block) {
- if ($block.hasClass('collapse-after-update')) {
- $block.removeClass('collapse-after-update');
- $('.page-with-sidebar').removeClass('with-overlay');
- return this.toggleSidebar('hide');
- }
- };
-
- Sidebar.prototype.triggerOpenSidebar = function() {
- return this.sidebar.find('.js-sidebar-toggle').trigger('click');
- };
-
- Sidebar.prototype.toggleSidebar = function(action) {
- if (action == null) {
- action = 'toggle';
- }
- if (action === 'toggle') {
- this.triggerOpenSidebar();
- }
- if (action === 'open') {
- if (!this.isOpen()) {
- this.triggerOpenSidebar();
- }
- }
- if (action === 'hide') {
- if (this.isOpen()) {
- return this.triggerOpenSidebar();
- }
- }
- };
+ if ($el.data(`${attrPrefix}-icon`)) {
+ $elText.html($el.data(`${attrPrefix}-icon`));
+ } else {
+ $elText.text($el.data(`${attrPrefix}-text`));
+ }
+ });
+};
+
+Sidebar.prototype.sidebarDropdownLoading = function(e) {
+ var $loading, $sidebarCollapsedIcon, i, img;
+ $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ img = $sidebarCollapsedIcon.find('img');
+ i = $sidebarCollapsedIcon.find('i');
+ $loading = $('<i class="fa fa-spinner fa-spin"></i>');
+ if (img.length) {
+ img.before($loading);
+ return img.hide();
+ } else if (i.length) {
+ i.before($loading);
+ return i.hide();
+ }
+};
+
+Sidebar.prototype.sidebarDropdownLoaded = function(e) {
+ var $sidebarCollapsedIcon, i, img;
+ $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ img = $sidebarCollapsedIcon.find('img');
+ $sidebarCollapsedIcon.find('i.fa-spin').remove();
+ i = $sidebarCollapsedIcon.find('i');
+ if (img.length) {
+ return img.show();
+ } else {
+ return i.show();
+ }
+};
+
+Sidebar.prototype.sidebarCollapseClicked = function(e) {
+ var $block, sidebar;
+ if ($(e.currentTarget).hasClass('dont-change-state')) {
+ return;
+ }
+ sidebar = e.data;
+ e.preventDefault();
+ $block = $(this).closest('.block');
+ return sidebar.openDropdown($block);
+};
+
+Sidebar.prototype.openDropdown = function(blockOrName) {
+ var $block;
+ $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
+ if (!this.isOpen()) {
+ this.setCollapseAfterUpdate($block);
+ this.toggleSidebar('open');
+ }
+
+ // Wait for the sidebar to trigger('click') open
+ // so it doesn't cause our dropdown to close preemptively
+ setTimeout(() => {
+ $block.find('.js-sidebar-dropdown-toggle').trigger('click');
+ });
+};
+
+Sidebar.prototype.setCollapseAfterUpdate = function($block) {
+ $block.addClass('collapse-after-update');
+ return $('.layout-page').addClass('with-overlay');
+};
+
+Sidebar.prototype.onSidebarDropdownHidden = function(e) {
+ var $block, sidebar;
+ sidebar = e.data;
+ e.preventDefault();
+ $block = $(e.target).closest('.block');
+ return sidebar.sidebarDropdownHidden($block);
+};
+
+Sidebar.prototype.sidebarDropdownHidden = function($block) {
+ if ($block.hasClass('collapse-after-update')) {
+ $block.removeClass('collapse-after-update');
+ $('.layout-page').removeClass('with-overlay');
+ return this.toggleSidebar('hide');
+ }
+};
+
+Sidebar.prototype.triggerOpenSidebar = function() {
+ return this.sidebar.find('.js-sidebar-toggle').trigger('click');
+};
+
+Sidebar.prototype.toggleSidebar = function(action) {
+ if (action == null) {
+ action = 'toggle';
+ }
+ if (action === 'toggle') {
+ this.triggerOpenSidebar();
+ }
+ if (action === 'open') {
+ if (!this.isOpen()) {
+ this.triggerOpenSidebar();
+ }
+ }
+ if (action === 'hide') {
+ if (this.isOpen()) {
+ return this.triggerOpenSidebar();
+ }
+ }
+};
- Sidebar.prototype.isOpen = function() {
- return this.sidebar.is('.right-sidebar-expanded');
- };
+Sidebar.prototype.isOpen = function() {
+ return this.sidebar.is('.right-sidebar-expanded');
+};
- Sidebar.prototype.getBlock = function(name) {
- return this.sidebar.find(".block." + name);
- };
+Sidebar.prototype.getBlock = function(name) {
+ return this.sidebar.find(".block." + name);
+};
- return Sidebar;
- })();
-}).call(window);
+export default Sidebar;
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 07fee53d814..363322af47a 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,118 +1,113 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
import Flash from './flash';
import Api from './api';
-(function() {
- this.Search = (function() {
- function Search() {
- var $groupDropdown, $projectDropdown;
- $groupDropdown = $('.js-search-group-dropdown');
- $projectDropdown = $('.js-search-project-dropdown');
- this.groupId = $groupDropdown.data('group-id');
- this.eventListeners();
- $groupDropdown.glDropdown({
- selectable: true,
- filterable: true,
- fieldName: 'group_id',
- search: {
- fields: ['full_name']
- },
- data: function(term, callback) {
- return Api.groups(term, {}, function(data) {
+export default class Search {
+ constructor() {
+ const $groupDropdown = $('.js-search-group-dropdown');
+ const $projectDropdown = $('.js-search-project-dropdown');
+
+ this.searchInput = '.js-search-input';
+ this.searchClear = '.js-search-clear';
+
+ this.groupId = $groupDropdown.data('group-id');
+ this.eventListeners();
+
+ $groupDropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ fieldName: 'group_id',
+ search: {
+ fields: ['full_name'],
+ },
+ data(term, callback) {
+ return Api.groups(term, {}, (data) => {
+ data.unshift({
+ full_name: 'Any',
+ });
+ data.splice(1, 0, 'divider');
+ return callback(data);
+ });
+ },
+ id(obj) {
+ return obj.id;
+ },
+ text(obj) {
+ return obj.full_name;
+ },
+ toggleLabel(obj) {
+ return `${($groupDropdown.data('default-label'))} ${obj.full_name}`;
+ },
+ clicked: () => Search.submitSearch(),
+ });
+
+ $projectDropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ fieldName: 'project_id',
+ search: {
+ fields: ['name'],
+ },
+ data: (term, callback) => {
+ this.getProjectsData(term)
+ .then((data) => {
data.unshift({
- full_name: 'Any'
+ name_with_namespace: 'Any',
});
data.splice(1, 0, 'divider');
- return callback(data);
- });
- },
- id: function(obj) {
- return obj.id;
- },
- text: function(obj) {
- return obj.full_name;
- },
- toggleLabel: function(obj) {
- return ($groupDropdown.data('default-label')) + " " + obj.full_name;
- },
- clicked: (function(_this) {
- return function() {
- return _this.submitSearch();
- };
- })(this)
- });
- $projectDropdown.glDropdown({
- selectable: true,
- filterable: true,
- fieldName: 'project_id',
- search: {
- fields: ['name']
- },
- data: (term, callback) => {
- this.getProjectsData(term)
- .then((data) => {
- data.unshift({
- name_with_namespace: 'Any'
- });
- data.splice(1, 0, 'divider');
- return data;
- })
- .then(data => callback(data))
- .catch(() => new Flash('Error fetching projects'));
- },
- id: function(obj) {
- return obj.id;
- },
- text: function(obj) {
- return obj.name_with_namespace;
- },
- toggleLabel: function(obj) {
- return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace;
- },
- clicked: (function(_this) {
- return function() {
- return _this.submitSearch();
- };
- })(this)
- });
- }
+ return data;
+ })
+ .then(data => callback(data))
+ .catch(() => new Flash('Error fetching projects'));
+ },
+ id(obj) {
+ return obj.id;
+ },
+ text(obj) {
+ return obj.name_with_namespace;
+ },
+ toggleLabel(obj) {
+ return `${($projectDropdown.data('default-label'))} ${obj.name_with_namespace}`;
+ },
+ clicked: () => Search.submitSearch(),
+ });
+ }
- Search.prototype.eventListeners = function() {
- $(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp);
- return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField);
- };
+ eventListeners() {
+ $(document)
+ .off('keyup', this.searchInput)
+ .on('keyup', this.searchInput, this.searchKeyUp);
+ $(document)
+ .off('click', this.searchClear)
+ .on('click', this.searchClear, this.clearSearchField.bind(this));
+ }
- Search.prototype.submitSearch = function() {
- return $('.js-search-form').submit();
- };
+ static submitSearch() {
+ return $('.js-search-form').submit();
+ }
- Search.prototype.searchKeyUp = function() {
- var $input;
- $input = $(this);
- if ($input.val() === '') {
- return $('.js-search-clear').addClass('hidden');
- } else {
- return $('.js-search-clear').removeClass('hidden');
- }
- };
-
- Search.prototype.clearSearchField = function() {
- return $('.js-search-input').val('').trigger('keyup').focus();
- };
+ searchKeyUp() {
+ const $input = $(this);
+ if ($input.val() === '') {
+ $('.js-search-clear').addClass('hidden');
+ } else {
+ $('.js-search-clear').removeClass('hidden');
+ }
+ }
- Search.prototype.getProjectsData = function(term) {
- return new Promise((resolve) => {
- if (this.groupId) {
- Api.groupProjects(this.groupId, term, resolve);
- } else {
- Api.projects(term, {
- order_by: 'id',
- }, resolve);
- }
- });
- };
+ clearSearchField() {
+ return $(this.searchInput).val('').trigger('keyup').focus();
+ }
- return Search;
- })();
-}).call(window);
+ getProjectsData(term) {
+ return new Promise((resolve) => {
+ if (this.groupId) {
+ Api.groupProjects(this.groupId, term, resolve);
+ } else {
+ Api.projects(term, {
+ order_by: 'id',
+ }, resolve);
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index e40a3596200..98b524f7e3f 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -8,448 +8,445 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
* When the user clicks `x` button it cleans the input and closes the dropdown.
*/
-((global) => {
- const KEYCODE = {
- ESCAPE: 27,
- BACKSPACE: 8,
- ENTER: 13,
- UP: 38,
- DOWN: 40,
- };
-
- class SearchAutocomplete {
- constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
- this.bindEventContext();
- this.wrap = wrap || $('.search');
- this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
- this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
- this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
- this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
- this.dropdown = this.wrap.find('.dropdown');
- this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
- this.dropdownContent = this.dropdown.find('.dropdown-content');
- this.locationBadgeEl = this.getElement('.location-badge');
- this.scopeInputEl = this.getElement('#scope');
- this.searchInput = this.getElement('.search-input');
- this.projectInputEl = this.getElement('#search_project_id');
- this.groupInputEl = this.getElement('#group_id');
- this.searchCodeInputEl = this.getElement('#search_code');
- this.repositoryInputEl = this.getElement('#repository_ref');
- this.clearInput = this.getElement('.js-clear-input');
- this.saveOriginalState();
-
- // Only when user is logged in
- if (gon.current_user_id) {
- this.createAutocomplete();
- }
+const KEYCODE = {
+ ESCAPE: 27,
+ BACKSPACE: 8,
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40,
+};
+
+function setSearchOptions() {
+ var $projectOptionsDataEl = $('.js-search-project-options');
+ var $groupOptionsDataEl = $('.js-search-group-options');
+ var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
+
+ if ($projectOptionsDataEl.length) {
+ gl.projectOptions = gl.projectOptions || {};
+
+ var projectPath = $projectOptionsDataEl.data('project-path');
+
+ gl.projectOptions[projectPath] = {
+ name: $projectOptionsDataEl.data('name'),
+ issuesPath: $projectOptionsDataEl.data('issues-path'),
+ issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
+ mrPath: $projectOptionsDataEl.data('mr-path'),
+ };
+ }
- this.searchInput.addClass('disabled');
- this.saveTextLength();
- this.bindEvents();
- this.dropdownToggle.dropdown();
- }
+ if ($groupOptionsDataEl.length) {
+ gl.groupOptions = gl.groupOptions || {};
- // Finds an element inside wrapper element
- bindEventContext() {
- this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
- this.onClearInputClick = this.onClearInputClick.bind(this);
- this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
- this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
- this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
- }
- getElement(selector) {
- return this.wrap.find(selector);
- }
+ var groupPath = $groupOptionsDataEl.data('group-path');
- saveOriginalState() {
- return this.originalState = this.serializeState();
- }
+ gl.groupOptions[groupPath] = {
+ name: $groupOptionsDataEl.data('name'),
+ issuesPath: $groupOptionsDataEl.data('issues-path'),
+ mrPath: $groupOptionsDataEl.data('mr-path'),
+ };
+ }
- saveTextLength() {
- return this.lastTextLength = this.searchInput.val().length;
+ if ($dashboardOptionsDataEl.length) {
+ gl.dashboardOptions = {
+ issuesPath: $dashboardOptionsDataEl.data('issues-path'),
+ mrPath: $dashboardOptionsDataEl.data('mr-path'),
+ };
+ }
+}
+
+export default class SearchAutocomplete {
+ constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
+ setSearchOptions();
+ this.bindEventContext();
+ this.wrap = wrap || $('.search');
+ this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
+ this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
+ this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
+ this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
+ this.dropdown = this.wrap.find('.dropdown');
+ this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
+ this.dropdownContent = this.dropdown.find('.dropdown-content');
+ this.locationBadgeEl = this.getElement('.location-badge');
+ this.scopeInputEl = this.getElement('#scope');
+ this.searchInput = this.getElement('.search-input');
+ this.projectInputEl = this.getElement('#search_project_id');
+ this.groupInputEl = this.getElement('#group_id');
+ this.searchCodeInputEl = this.getElement('#search_code');
+ this.repositoryInputEl = this.getElement('#repository_ref');
+ this.clearInput = this.getElement('.js-clear-input');
+ this.saveOriginalState();
+
+ // Only when user is logged in
+ if (gon.current_user_id) {
+ this.createAutocomplete();
}
- createAutocomplete() {
- return this.searchInput.glDropdown({
- filterInputBlur: false,
- filterable: true,
- filterRemote: true,
- highlight: true,
- enterCallback: false,
- filterInput: 'input#search',
- search: {
- fields: ['text'],
- },
- id: this.getSearchText,
- data: this.getData.bind(this),
- selectable: true,
- clicked: this.onClick.bind(this),
- });
+ this.searchInput.addClass('disabled');
+ this.saveTextLength();
+ this.bindEvents();
+ this.dropdownToggle.dropdown();
+ }
+
+ // Finds an element inside wrapper element
+ bindEventContext() {
+ this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
+ this.onClearInputClick = this.onClearInputClick.bind(this);
+ this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
+ this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
+ this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+ }
+ getElement(selector) {
+ return this.wrap.find(selector);
+ }
+
+ saveOriginalState() {
+ return this.originalState = this.serializeState();
+ }
+
+ saveTextLength() {
+ return this.lastTextLength = this.searchInput.val().length;
+ }
+
+ createAutocomplete() {
+ return this.searchInput.glDropdown({
+ filterInputBlur: false,
+ filterable: true,
+ filterRemote: true,
+ highlight: true,
+ enterCallback: false,
+ filterInput: 'input#search',
+ search: {
+ fields: ['text'],
+ },
+ id: this.getSearchText,
+ data: this.getData.bind(this),
+ selectable: true,
+ clicked: this.onClick.bind(this),
+ });
+ }
+
+ getSearchText(selectedObject, el) {
+ return selectedObject.id ? selectedObject.text : '';
+ }
+
+ getData(term, callback) {
+ if (!term) {
+ const contents = this.getCategoryContents();
+ if (contents) {
+ this.searchInput.data('glDropdown').filter.options.callback(contents);
+ this.enableAutocomplete();
+ }
+ return;
}
- getSearchText(selectedObject, el) {
- return selectedObject.id ? selectedObject.text : '';
+ // Prevent multiple ajax calls
+ if (this.loadingSuggestions) {
+ return;
}
- getData(term, callback) {
- if (!term) {
- const contents = this.getCategoryContents();
- if (contents) {
- this.searchInput.data('glDropdown').filter.options.callback(contents);
- this.enableAutocomplete();
- }
- return;
- }
+ this.loadingSuggestions = true;
- // Prevent multiple ajax calls
- if (this.loadingSuggestions) {
+ return $.get(this.autocompletePath, {
+ project_id: this.projectId,
+ project_ref: this.projectRef,
+ term: term,
+ }, (response) => {
+ var firstCategory, i, lastCategory, len, suggestion;
+ // Hide dropdown menu if no suggestions returns
+ if (!response.length) {
+ this.disableAutocomplete();
return;
}
- this.loadingSuggestions = true;
-
- return $.get(this.autocompletePath, {
- project_id: this.projectId,
- project_ref: this.projectRef,
- term: term,
- }, (response) => {
- var firstCategory, i, lastCategory, len, suggestion;
- // Hide dropdown menu if no suggestions returns
- if (!response.length) {
- this.disableAutocomplete();
- return;
- }
-
- const data = [];
- // List results
- firstCategory = true;
- for (i = 0, len = response.length; i < len; i += 1) {
- suggestion = response[i];
- // Add group header before list each group
- if (lastCategory !== suggestion.category) {
- if (!firstCategory) {
- data.push('separator');
- }
- if (firstCategory) {
- firstCategory = false;
- }
- data.push({
- header: suggestion.category,
- });
- lastCategory = suggestion.category;
+ const data = [];
+ // List results
+ firstCategory = true;
+ for (i = 0, len = response.length; i < len; i += 1) {
+ suggestion = response[i];
+ // Add group header before list each group
+ if (lastCategory !== suggestion.category) {
+ if (!firstCategory) {
+ data.push('separator');
+ }
+ if (firstCategory) {
+ firstCategory = false;
}
data.push({
- id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
- category: suggestion.category,
- text: suggestion.label,
- url: suggestion.url,
- });
- }
- // Add option to proceed with the search
- if (data.length) {
- data.push('separator');
- data.push({
- text: "Result name contains \"" + term + "\"",
- url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()),
+ header: suggestion.category,
});
+ lastCategory = suggestion.category;
}
- return callback(data);
- })
- .always(() => { this.loadingSuggestions = false; });
- }
-
- getCategoryContents() {
- const userId = gon.current_user_id;
- const userName = gon.current_username;
- const { projectOptions, groupOptions, dashboardOptions } = gl;
-
- // Get options
- let options;
- if (isInGroupsPage() && groupOptions) {
- options = groupOptions[getGroupSlug()];
- } else if (isInProjectPage() && projectOptions) {
- options = projectOptions[getProjectSlug()];
- } else if (dashboardOptions) {
- options = dashboardOptions;
+ data.push({
+ id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
+ category: suggestion.category,
+ text: suggestion.label,
+ url: suggestion.url,
+ });
}
-
- const { issuesPath, mrPath, name, issuesDisabled } = options;
- const baseItems = [];
-
- if (name) {
- baseItems.push({
- header: `${name}`,
+ // Add option to proceed with the search
+ if (data.length) {
+ data.push('separator');
+ data.push({
+ text: "Result name contains \"" + term + "\"",
+ url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()),
});
}
+ return callback(data);
+ })
+ .always(() => { this.loadingSuggestions = false; });
+ }
- const issueItems = [
- {
- text: 'Issues assigned to me',
- url: `${issuesPath}/?assignee_username=${userName}`,
- },
- {
- text: "Issues I've created",
- url: `${issuesPath}/?author_username=${userName}`,
- },
- ];
- const mergeRequestItems = [
- {
- text: 'Merge requests assigned to me',
- url: `${mrPath}/?assignee_username=${userName}`,
- },
- {
- text: "Merge requests I've created",
- url: `${mrPath}/?author_username=${userName}`,
- },
- ];
-
- let items;
- if (issuesDisabled) {
- items = baseItems.concat(mergeRequestItems);
- } else {
- items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
- }
- return items;
+ getCategoryContents() {
+ const userId = gon.current_user_id;
+ const userName = gon.current_username;
+ const { projectOptions, groupOptions, dashboardOptions } = gl;
+
+ // Get options
+ let options;
+ if (isInGroupsPage() && groupOptions) {
+ options = groupOptions[getGroupSlug()];
+ } else if (isInProjectPage() && projectOptions) {
+ options = projectOptions[getProjectSlug()];
+ } else if (dashboardOptions) {
+ options = dashboardOptions;
}
- serializeState() {
- return {
- // Search Criteria
- search_project_id: this.projectInputEl.val(),
- group_id: this.groupInputEl.val(),
- search_code: this.searchCodeInputEl.val(),
- repository_ref: this.repositoryInputEl.val(),
- scope: this.scopeInputEl.val(),
- // Location badge
- _location: this.locationBadgeEl.text(),
- };
+ const { issuesPath, mrPath, name, issuesDisabled } = options;
+ const baseItems = [];
+
+ if (name) {
+ baseItems.push({
+ header: `${name}`,
+ });
}
- bindEvents() {
- this.searchInput.on('keydown', this.onSearchInputKeyDown);
- this.searchInput.on('keyup', this.onSearchInputKeyUp);
- this.searchInput.on('focus', this.onSearchInputFocus);
- this.searchInput.on('blur', this.onSearchInputBlur);
- this.clearInput.on('click', this.onClearInputClick);
- this.locationBadgeEl.on('click', () => this.searchInput.focus());
+ const issueItems = [
+ {
+ text: 'Issues assigned to me',
+ url: `${issuesPath}/?assignee_username=${userName}`,
+ },
+ {
+ text: "Issues I've created",
+ url: `${issuesPath}/?author_username=${userName}`,
+ },
+ ];
+ const mergeRequestItems = [
+ {
+ text: 'Merge requests assigned to me',
+ url: `${mrPath}/?assignee_username=${userName}`,
+ },
+ {
+ text: "Merge requests I've created",
+ url: `${mrPath}/?author_username=${userName}`,
+ },
+ ];
+
+ let items;
+ if (issuesDisabled) {
+ items = baseItems.concat(mergeRequestItems);
+ } else {
+ items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
}
+ return items;
+ }
- enableAutocomplete() {
- // No need to enable anything if user is not logged in
- if (!gon.current_user_id) {
- return;
- }
+ serializeState() {
+ return {
+ // Search Criteria
+ search_project_id: this.projectInputEl.val(),
+ group_id: this.groupInputEl.val(),
+ search_code: this.searchCodeInputEl.val(),
+ repository_ref: this.repositoryInputEl.val(),
+ scope: this.scopeInputEl.val(),
+ // Location badge
+ _location: this.locationBadgeEl.text(),
+ };
+ }
- // If the dropdown is closed, we'll open it
- if (!this.dropdown.hasClass('open')) {
- this.loadingSuggestions = false;
- this.dropdownToggle.dropdown('toggle');
- return this.searchInput.removeClass('disabled');
- }
+ bindEvents() {
+ this.searchInput.on('keydown', this.onSearchInputKeyDown);
+ this.searchInput.on('keyup', this.onSearchInputKeyUp);
+ this.searchInput.on('focus', this.onSearchInputFocus);
+ this.searchInput.on('blur', this.onSearchInputBlur);
+ this.clearInput.on('click', this.onClearInputClick);
+ this.locationBadgeEl.on('click', () => this.searchInput.focus());
+ }
+
+ enableAutocomplete() {
+ // No need to enable anything if user is not logged in
+ if (!gon.current_user_id) {
+ return;
}
- // Saves last length of the entered text
- onSearchInputKeyDown() {
- return this.saveTextLength();
+ // If the dropdown is closed, we'll open it
+ if (!this.dropdown.hasClass('open')) {
+ this.loadingSuggestions = false;
+ this.dropdownToggle.dropdown('toggle');
+ return this.searchInput.removeClass('disabled');
}
+ }
- onSearchInputKeyUp(e) {
- switch (e.keyCode) {
- case KEYCODE.BACKSPACE:
- // when trying to remove the location badge
- if (this.lastTextLength === 0 && this.badgePresent()) {
- this.removeLocationBadge();
- }
- // When removing the last character and no badge is present
- if (this.lastTextLength === 1) {
- this.disableAutocomplete();
- }
- // When removing any character from existin value
- if (this.lastTextLength > 1) {
- this.enableAutocomplete();
- }
- break;
- case KEYCODE.ESCAPE:
- this.restoreOriginalState();
- break;
- case KEYCODE.ENTER:
+ // Saves last length of the entered text
+ onSearchInputKeyDown() {
+ return this.saveTextLength();
+ }
+
+ onSearchInputKeyUp(e) {
+ switch (e.keyCode) {
+ case KEYCODE.BACKSPACE:
+ // when trying to remove the location badge
+ if (this.lastTextLength === 0 && this.badgePresent()) {
+ this.removeLocationBadge();
+ }
+ // When removing the last character and no badge is present
+ if (this.lastTextLength === 1) {
+ this.disableAutocomplete();
+ }
+ // When removing any character from existin value
+ if (this.lastTextLength > 1) {
+ this.enableAutocomplete();
+ }
+ break;
+ case KEYCODE.ESCAPE:
+ this.restoreOriginalState();
+ break;
+ case KEYCODE.ENTER:
+ this.disableAutocomplete();
+ break;
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ return;
+ default:
+ // Handle the case when deleting the input value other than backspace
+ // e.g. Pressing ctrl + backspace or ctrl + x
+ if (this.searchInput.val() === '') {
this.disableAutocomplete();
- break;
- case KEYCODE.UP:
- case KEYCODE.DOWN:
- return;
- default:
- // Handle the case when deleting the input value other than backspace
- // e.g. Pressing ctrl + backspace or ctrl + x
- if (this.searchInput.val() === '') {
- this.disableAutocomplete();
- } else {
- // We should display the menu only when input is not empty
- if (e.keyCode !== KEYCODE.ENTER) {
- this.enableAutocomplete();
- }
+ } else {
+ // We should display the menu only when input is not empty
+ if (e.keyCode !== KEYCODE.ENTER) {
+ this.enableAutocomplete();
}
- }
- this.wrap.toggleClass('has-value', !!e.target.value);
+ }
}
+ this.wrap.toggleClass('has-value', !!e.target.value);
+ }
- onSearchInputFocus() {
- this.isFocused = true;
- this.wrap.addClass('search-active');
- if (this.getValue() === '') {
- return this.getData();
- }
+ onSearchInputFocus() {
+ this.isFocused = true;
+ this.wrap.addClass('search-active');
+ if (this.getValue() === '') {
+ return this.getData();
}
+ }
- getValue() {
- return this.searchInput.val();
- }
+ getValue() {
+ return this.searchInput.val();
+ }
- onClearInputClick(e) {
- e.preventDefault();
- this.wrap.toggleClass('has-value', !!e.target.value);
- return this.searchInput.val('').focus();
- }
+ onClearInputClick(e) {
+ e.preventDefault();
+ this.wrap.toggleClass('has-value', !!e.target.value);
+ return this.searchInput.val('').focus();
+ }
- onSearchInputBlur(e) {
- this.isFocused = false;
- this.wrap.removeClass('search-active');
- // If input is blank then restore state
- if (this.searchInput.val() === '') {
- return this.restoreOriginalState();
- }
+ onSearchInputBlur(e) {
+ this.isFocused = false;
+ this.wrap.removeClass('search-active');
+ // If input is blank then restore state
+ if (this.searchInput.val() === '') {
+ return this.restoreOriginalState();
}
+ }
- addLocationBadge(item) {
- var badgeText, category, value;
- category = item.category != null ? item.category + ": " : '';
- value = item.value != null ? item.value : '';
- badgeText = "" + category + value;
- this.locationBadgeEl.text(badgeText).show();
- return this.wrap.addClass('has-location-badge');
- }
+ addLocationBadge(item) {
+ var badgeText, category, value;
+ category = item.category != null ? item.category + ": " : '';
+ value = item.value != null ? item.value : '';
+ badgeText = "" + category + value;
+ this.locationBadgeEl.text(badgeText).show();
+ return this.wrap.addClass('has-location-badge');
+ }
- hasLocationBadge() {
- return this.wrap.is('.has-location-badge');
- }
+ hasLocationBadge() {
+ return this.wrap.is('.has-location-badge');
+ }
- restoreOriginalState() {
- var i, input, inputs, len;
- inputs = Object.keys(this.originalState);
- for (i = 0, len = inputs.length; i < len; i += 1) {
- input = inputs[i];
- this.getElement("#" + input).val(this.originalState[input]);
- }
- if (this.originalState._location === '') {
- return this.locationBadgeEl.hide();
- } else {
- return this.addLocationBadge({
- value: this.originalState._location,
- });
- }
+ restoreOriginalState() {
+ var i, input, inputs, len;
+ inputs = Object.keys(this.originalState);
+ for (i = 0, len = inputs.length; i < len; i += 1) {
+ input = inputs[i];
+ this.getElement("#" + input).val(this.originalState[input]);
}
-
- badgePresent() {
- return this.locationBadgeEl.length;
+ if (this.originalState._location === '') {
+ return this.locationBadgeEl.hide();
+ } else {
+ return this.addLocationBadge({
+ value: this.originalState._location,
+ });
}
+ }
- resetSearchState() {
- var i, input, inputs, len, results;
- inputs = Object.keys(this.originalState);
- results = [];
- for (i = 0, len = inputs.length; i < len; i += 1) {
- input = inputs[i];
- // _location isnt a input
- if (input === '_location') {
- break;
- }
- results.push(this.getElement("#" + input).val(''));
+ badgePresent() {
+ return this.locationBadgeEl.length;
+ }
+
+ resetSearchState() {
+ var i, input, inputs, len, results;
+ inputs = Object.keys(this.originalState);
+ results = [];
+ for (i = 0, len = inputs.length; i < len; i += 1) {
+ input = inputs[i];
+ // _location isnt a input
+ if (input === '_location') {
+ break;
}
- return results;
+ results.push(this.getElement("#" + input).val(''));
}
+ return results;
+ }
- removeLocationBadge() {
- this.locationBadgeEl.hide();
- this.resetSearchState();
- this.wrap.removeClass('has-location-badge');
- return this.disableAutocomplete();
- }
+ removeLocationBadge() {
+ this.locationBadgeEl.hide();
+ this.resetSearchState();
+ this.wrap.removeClass('has-location-badge');
+ return this.disableAutocomplete();
+ }
- disableAutocomplete() {
- if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
- this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
- this.restoreMenu();
- }
+ disableAutocomplete() {
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ this.searchInput.addClass('disabled');
+ this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.restoreMenu();
}
+ }
- restoreMenu() {
- var html;
- html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>';
- return this.dropdownContent.html(html);
- }
+ restoreMenu() {
+ var html;
+ html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>';
+ return this.dropdownContent.html(html);
+ }
- onClick(item, $el, e) {
- if (location.pathname.indexOf(item.url) !== -1) {
- if (!e.metaKey) e.preventDefault();
- if (!this.badgePresent) {
- if (item.category === 'Projects') {
- this.projectInputEl.val(item.id);
- this.addLocationBadge({
- value: 'This project',
- });
- }
- if (item.category === 'Groups') {
- this.groupInputEl.val(item.id);
- this.addLocationBadge({
- value: 'This group',
- });
- }
+ onClick(item, $el, e) {
+ if (location.pathname.indexOf(item.url) !== -1) {
+ if (!e.metaKey) e.preventDefault();
+ if (!this.badgePresent) {
+ if (item.category === 'Projects') {
+ this.projectInputEl.val(item.id);
+ this.addLocationBadge({
+ value: 'This project',
+ });
+ }
+ if (item.category === 'Groups') {
+ this.groupInputEl.val(item.id);
+ this.addLocationBadge({
+ value: 'This group',
+ });
}
- $el.removeClass('is-active');
- this.disableAutocomplete();
- return this.searchInput.val('').focus();
}
+ $el.removeClass('is-active');
+ this.disableAutocomplete();
+ return this.searchInput.val('').focus();
}
}
-
- global.SearchAutocomplete = SearchAutocomplete;
-
- $(function() {
- var $projectOptionsDataEl = $('.js-search-project-options');
- var $groupOptionsDataEl = $('.js-search-group-options');
- var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
-
- if ($projectOptionsDataEl.length) {
- gl.projectOptions = gl.projectOptions || {};
-
- var projectPath = $projectOptionsDataEl.data('project-path');
-
- gl.projectOptions[projectPath] = {
- name: $projectOptionsDataEl.data('name'),
- issuesPath: $projectOptionsDataEl.data('issues-path'),
- issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
- mrPath: $projectOptionsDataEl.data('mr-path'),
- };
- }
-
- if ($groupOptionsDataEl.length) {
- gl.groupOptions = gl.groupOptions || {};
-
- var groupPath = $groupOptionsDataEl.data('group-path');
-
- gl.groupOptions[groupPath] = {
- name: $groupOptionsDataEl.data('name'),
- issuesPath: $groupOptionsDataEl.data('issues-path'),
- mrPath: $groupOptionsDataEl.data('mr-path'),
- };
- }
-
- if ($dashboardOptionsDataEl.length) {
- gl.dashboardOptions = {
- issuesPath: $dashboardOptionsDataEl.data('issues-path'),
- mrPath: $dashboardOptionsDataEl.data('mr-path'),
- };
- }
- });
-})(window.gl || (window.gl = {}));
+}
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index ebe7a99ffae..d2f0d7410da 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,5 +1,6 @@
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
+import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility';
import findAndFollowLink from './shortcuts_dashboard_navigation';
const defaultStopCallback = Mousetrap.stopCallback;
@@ -38,7 +39,7 @@ export default class Shortcuts {
if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
Mousetrap.bind('t', () => {
- gl.utils.visitUrl(findFileURL);
+ visitUrl(findFileURL);
});
}
@@ -50,7 +51,10 @@ export default class Shortcuts {
}
onToggleHelp(e) {
- e.preventDefault();
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
Shortcuts.toggleHelp(this.enabledHelp);
}
@@ -62,7 +66,7 @@ export default class Shortcuts {
} else {
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
}
- gl.utils.refreshCurrentPage();
+ refreshCurrentPage();
}
static toggleMarkdownPreview(e) {
@@ -111,6 +115,9 @@ export default class Shortcuts {
static focusSearch(e) {
$('#search').focus();
- e.preventDefault();
+
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
}
}
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index fbc57bb4304..cf309be4f6f 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,5 +1,5 @@
/* global Mousetrap */
-
+import { getLocationHash, visitUrl } from './lib/utils/url_utility';
import Shortcuts from './shortcuts';
const defaults = {
@@ -18,9 +18,9 @@ export default class ShortcutsBlob extends Shortcuts {
moveToFilePermalink() {
if (this.options.fileBlobPermalinkUrl) {
- const hash = gl.utils.getLocationHash();
+ const hash = getLocationHash();
const hashUrlString = hash ? `#${hash}` : '';
- gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
+ visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
}
}
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 4f4f606d293..292e3d6a657 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,8 +1,8 @@
/* global Mousetrap */
-/* global sidebar */
import _ from 'underscore';
import 'mousetrap';
+import Sidebar from './right_sidebar';
import ShortcutsNavigation from './shortcuts_navigation';
import { CopyAsGFM } from './behaviors/copy_as_gfm';
@@ -11,7 +11,7 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
super();
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
- this.editBtn = document.querySelector('.issuable-edit');
+ this.editBtn = document.querySelector('.js-issuable-edit');
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
@@ -69,7 +69,7 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
}
static openSidebarDropdown(name) {
- sidebar.openDropdown(name);
+ Sidebar.instance.openDropdown(name);
return false;
}
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
index 74c17bc14a2..9e47039d920 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
@@ -1,22 +1,32 @@
import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
import Assignees from './assignees';
-
import Store from '../../stores/sidebar_store';
-import Mediator from '../../sidebar_mediator';
-
import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
data() {
return {
- mediator: new Mediator(),
store: new Store(),
loading: false,
- field: '',
};
},
+ props: {
+ mediator: {
+ type: Object,
+ required: true,
+ },
+ field: {
+ type: String,
+ required: true,
+ },
+ signedIn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
components: {
'assignee-title': AssigneeTitle,
assignees: Assignees,
@@ -61,10 +71,6 @@ export default {
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
- beforeMount() {
- this.field = this.$el.dataset.field;
- this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined';
- },
template: `
<div>
<assignee-title
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
index c1296b28db7..6fcd2f95309 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
@@ -1,15 +1,19 @@
<script>
import Store from '../../stores/sidebar_store';
-import Mediator from '../../sidebar_mediator';
import participants from './participants.vue';
export default {
data() {
return {
- mediator: new Mediator(),
store: new Store(),
};
},
+ props: {
+ mediator: {
+ type: Object,
+ required: true,
+ },
+ },
components: {
participants,
},
@@ -21,6 +25,7 @@ export default {
<participants
:loading="store.isFetching.participants"
:participants="store.participants"
- :number-of-less-participants="7" />
+ :number-of-less-participants="7"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 25acc099699..f4bae1d3dd5 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -1,6 +1,5 @@
<script>
import Store from '../../stores/sidebar_store';
-import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
@@ -9,11 +8,15 @@ import subscriptions from './subscriptions.vue';
export default {
data() {
return {
- mediator: new Mediator(),
store: new Store(),
};
},
-
+ props: {
+ mediator: {
+ type: Object,
+ required: true,
+ },
+ },
components: {
subscriptions,
},
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 4032f156b15..56cc78ca0ca 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -10,6 +10,27 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
+function mountAssigneesComponent(mediator) {
+ const el = document.getElementById('js-vue-sidebar-assignees');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ SidebarAssignees,
+ },
+ render: createElement => createElement('sidebar-assignees', {
+ props: {
+ mediator,
+ field: el.dataset.field,
+ signedIn: el.hasAttribute('data-signed-in'),
+ },
+ }),
+ });
+}
+
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
@@ -49,9 +70,10 @@ function mountLockComponent(mediator) {
}).$mount(el);
}
-function mountParticipantsComponent() {
+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
@@ -60,11 +82,15 @@ function mountParticipantsComponent() {
components: {
sidebarParticipants,
},
- render: createElement => createElement('sidebar-participants', {}),
+ render: createElement => createElement('sidebar-participants', {
+ props: {
+ mediator,
+ },
+ }),
});
}
-function mountSubscriptionsComponent() {
+function mountSubscriptionsComponent(mediator) {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
@@ -75,22 +101,35 @@ function mountSubscriptionsComponent() {
components: {
sidebarSubscriptions,
},
- render: createElement => createElement('sidebar-subscriptions', {}),
+ render: createElement => createElement('sidebar-subscriptions', {
+ props: {
+ mediator,
+ },
+ }),
});
}
-function mount(mediator) {
- const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
- // Only create the sidebarAssignees vue app if it is found in the DOM
- // We currently do not use sidebarAssignees for the MR page
- if (sidebarAssigneesEl) {
- new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
- }
+function mountTimeTrackingComponent() {
+ const el = document.getElementById('issuable-time-tracker');
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ SidebarTimeTracking,
+ },
+ render: createElement => createElement('sidebar-time-tracking', {}),
+ });
+}
+
+export function mountSidebar(mediator) {
+ mountAssigneesComponent(mediator);
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
- mountParticipantsComponent();
- mountSubscriptionsComponent();
+ mountParticipantsComponent(mediator);
+ mountSubscriptionsComponent(mediator);
new SidebarMoveIssue(
mediator,
@@ -98,7 +137,9 @@ function mount(mediator) {
$('.js-move-issue-confirmation-button'),
).init();
- new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
+ mountTimeTrackingComponent();
}
-export default mount;
+export function getSidebarOptions() {
+ return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
+}
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index f78287e504b..04c39d7b6b5 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,9 +1,8 @@
import Mediator from './sidebar_mediator';
-import mountSidebar from './mount_sidebar';
+import { mountSidebar, getSidebarOptions } from './mount_sidebar';
function domContentLoaded() {
- const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
- const mediator = new Mediator(sidebarOptions);
+ const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
mountSidebar(mediator);
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d4c07a188b3..d86557e870a 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,3 +1,4 @@
+import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import Service from './services/sidebar_service';
import Store from './stores/sidebar_store';
@@ -7,7 +8,6 @@ export default class SidebarMediator {
if (!SidebarMediator.singleton) {
this.initSingleton(options);
}
-
return SidebarMediator.singleton;
}
@@ -81,7 +81,7 @@ export default class SidebarMediator {
.then(response => response.json())
.then((data) => {
if (location.pathname !== data.web_url) {
- gl.utils.visitUrl(data.web_url);
+ visitUrl(data.web_url);
}
});
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 73eb25e2333..f20cc6d8cca 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -1,33 +1,37 @@
export default class SidebarStore {
- constructor(store) {
+ constructor(options) {
if (!SidebarStore.singleton) {
- const { currentUser, rootPath, editable } = store;
- this.currentUser = currentUser;
- this.rootPath = rootPath;
- this.editable = editable;
- this.timeEstimate = 0;
- this.totalTimeSpent = 0;
- this.humanTimeEstimate = '';
- this.humanTimeSpent = '';
- this.assignees = [];
- this.isFetching = {
- assignees: true,
- participants: true,
- subscriptions: true,
- };
- this.isLoading = {};
- this.autocompleteProjects = [];
- this.moveToProjectId = 0;
- this.isLockDialogOpen = false;
- this.participants = [];
- this.subscribed = null;
-
- SidebarStore.singleton = this;
+ this.initSingleton(options);
}
return SidebarStore.singleton;
}
+ initSingleton(options) {
+ const { currentUser, rootPath, editable } = options;
+ this.currentUser = currentUser;
+ this.rootPath = rootPath;
+ this.editable = editable;
+ this.timeEstimate = 0;
+ this.totalTimeSpent = 0;
+ this.humanTimeEstimate = '';
+ this.humanTimeSpent = '';
+ this.assignees = [];
+ this.isFetching = {
+ assignees: true,
+ participants: true,
+ subscriptions: true,
+ };
+ this.isLoading = {};
+ this.autocompleteProjects = [];
+ this.moveToProjectId = 0;
+ this.isLockDialogOpen = false;
+ this.participants = [];
+ this.subscribed = null;
+
+ SidebarStore.singleton = this;
+ }
+
setAssigneeData(data) {
this.isFetching.assignees = false;
if (data.assignees) {
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 3f811c59cb9..95e51bc4e7a 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -2,6 +2,7 @@
import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index';
+import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
@@ -64,7 +65,7 @@ export default class SingleFileDiff {
_this.loadingContent.hide();
if (data.html) {
_this.content = $(data.html);
- _this.content.syntaxHighlight();
+ syntaxHighlight(_this.content);
} else {
_this.hasError = true;
_this.content = $(ERROR_HTML);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 662d6b36c16..62bdef76c55 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -10,17 +10,15 @@
// <div class="js-syntax-highlight"></div>
//
-$.fn.syntaxHighlight = function() {
- var $children;
-
- if ($(this).hasClass('js-syntax-highlight')) {
+export default function syntaxHighlight(el) {
+ if ($(el).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
- return $(this).addClass(gon.user_color_scheme);
+ return $(el).addClass(gon.user_color_scheme);
} else {
// Given a parent element, recurse to any of its applicable children
- $children = $(this).find('.js-syntax-highlight');
+ const $children = $(el).find('.js-syntax-highlight');
if ($children.length) {
- return $children.syntaxHighlight();
+ return syntaxHighlight($children);
}
}
-};
+}
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index 2fffe09c74e..748caecf153 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -1,5 +1,5 @@
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
-
+import { visitUrl } from './lib/utils/url_utility';
import UsersSelect from './users_select';
import { isMetaClick } from './lib/utils/common_utils';
@@ -150,7 +150,7 @@ export default class Todos {
window.open(todoLink, windowTarget);
} else {
- gl.utils.visitUrl(todoLink);
+ visitUrl(todoLink);
}
}
}
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 7777ed1c3dc..1a0b2c0415b 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,5 @@
/* 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 */
+import { visitUrl } from './lib/utils/url_utility';
export default class TreeView {
constructor() {
@@ -14,7 +15,7 @@ export default class TreeView {
e.preventDefault();
return window.open(path, '_blank');
} else {
- return gl.utils.visitUrl(path);
+ return visitUrl(path);
}
}
});
@@ -56,7 +57,7 @@ export default class TreeView {
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
- return gl.utils.visitUrl(path);
+ return visitUrl(path);
}
}
});
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js
index 5e947769f8a..0581239d5a5 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/users/activity_calendar.js
@@ -1,5 +1,9 @@
import _ from 'underscore';
-import d3 from 'd3';
+import { scaleLinear, scaleThreshold } from 'd3-scale';
+import { select } from 'd3-selection';
+import { getDayName, getDayDifference } from '../lib/utils/datetime_utility';
+
+const d3 = { select, scaleLinear, scaleThreshold };
const LOADING_HTML = `
<div class="text-center">
@@ -17,7 +21,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
- const dateDayName = gl.utils.getDayName(dateObject);
+ const dateDayName = getDayName(dateObject);
const dateText = dateObject.format('mmm d, yyyy');
let contribText = 'No contributions';
@@ -27,7 +31,7 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
-const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
@@ -51,7 +55,7 @@ export default class ActivityCalendar {
const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1);
- const days = gl.utils.getDayDifference(oneYearAgo, today);
+ const days = getDayDifference(oneYearAgo, today);
for (let i = 0; i <= days; i += 1) {
const date = new Date(oneYearAgo);
@@ -204,7 +208,7 @@ export default class ActivityCalendar {
initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
- return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange);
+ return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
}
clickDay(stamp) {
diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js
index 1215b265e28..992baa9a1ef 100644
--- a/app/assets/javascripts/users/user_tabs.js
+++ b/app/assets/javascripts/users/user_tabs.js
@@ -1,4 +1,6 @@
+import Activities from '../activities';
import ActivityCalendar from './activity_calendar';
+import { localTimeAgo } from '../lib/utils/datetime_utility';
/**
* UserTabs
@@ -138,7 +140,7 @@ export default class UserTabs {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
- gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ localTimeAgo($('.js-timeago', tabSelector));
},
});
}
@@ -169,7 +171,7 @@ export default class UserTabs {
});
// eslint-disable-next-line no-new
- new gl.Activities();
+ new Activities();
this.loaded.activity = true;
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 759cc9925f4..f249bd036d6 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -541,7 +541,6 @@ function UsersSelect(currentUser, els, options = {}) {
options.projectId = $(select).data('project-id');
options.groupId = $(select).data('group-id');
options.showCurrentUser = $(select).data('current-user');
- options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches');
options.authorId = $(select).data('author-id');
options.skipUsers = $(select).data('skip-users');
showNullUser = $(select).data('null-user');
@@ -688,7 +687,6 @@ UsersSelect.prototype.users = function(query, options, callback) {
todo_filter: options.todoFilter || null,
todo_state_filter: options.todoStateFilter || null,
current_user: options.showCurrentUser || null,
- push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
author_id: options.authorId || null,
skip_users: options.skipUsers || null
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index e86a0f7e749..d48f3a01420 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,4 +1,5 @@
-import '~/lib/utils/datetime_utility';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { visitUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon';
@@ -16,7 +17,7 @@ export default {
},
methods: {
formatDate(date) {
- return gl.utils.getTimeago().format(date);
+ return getTimeago().format(date);
},
hasExternalUrls(deployment = {}) {
return deployment.external_url && deployment.external_url_formatted;
@@ -33,10 +34,10 @@ export default {
if (isConfirmed) {
MRWidgetService.stopEnvironment(deployment.stop_url)
- .then(res => res.json())
- .then((res) => {
- if (res.redirect_url) {
- gl.utils.visitUrl(res.redirect_url);
+ .then(res => res.data)
+ .then((data) => {
+ if (data.redirect_url) {
+ visitUrl(data.redirect_url);
}
})
.catch(() => {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index 13e4cb5717e..85bfd03a3cf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -1,5 +1,6 @@
import tooltip from '../../vue_shared/directives/tooltip';
import { pluralize } from '../../lib/utils/text_utility';
+import icon from '../../vue_shared/components/icon.vue';
export default {
name: 'MRWidgetHeader',
@@ -9,6 +10,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ icon,
+ },
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
@@ -81,10 +85,9 @@ export default {
data-toggle="dropdown"
aria-label="Download as"
role="button">
- <i
- class="fa fa-download"
- aria-hidden="true">
- </i>
+ <icon
+ name="download">
+ </icon>
<i
class="fa fa-caret-down"
aria-hidden="true">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index a8c686e5065..69e70ba1dd6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -102,11 +102,11 @@ export default {
return res;
}
- return res.json();
+ return res.data;
})
- .then((res) => {
- this.computeGraphData(res.metrics, res.deployment_time);
- return res;
+ .then((data) => {
+ this.computeGraphData(data.metrics, data.deployment_time);
+ return data;
})
.catch(() => {
this.loadFailed = true;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
index b25cc3443ef..dd8b2665b1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -16,9 +16,9 @@ export default {
<div class="media-body">
<mr-widget-author-and-time
actionText="Closed by"
- :author="mr.closedEvent.author"
- :dateTitle="mr.closedEvent.updatedAt"
- :dateReadable="mr.closedEvent.formattedUpdatedAt"
+ :author="mr.metrics.closedBy"
+ :dateTitle="mr.metrics.closedAt"
+ :dateReadable="mr.metrics.readableClosedAt"
/>
<section class="mr-info-list">
<p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index 05c4a28be88..bd349111bbd 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -31,9 +31,9 @@ export default {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
- .then(res => res.json())
- .then((res) => {
- eventHub.$emit('UpdateWidgetData', res);
+ .then(res => res.data)
+ .then((data) => {
+ eventHub.$emit('UpdateWidgetData', data);
})
.catch(() => {
this.isCancellingAutoMerge = false;
@@ -49,9 +49,9 @@ export default {
this.isRemovingSourceBranch = true;
this.service.mergeResource.save(options)
- .then(res => res.json())
- .then((res) => {
- if (res.status === 'merge_when_pipeline_succeeds') {
+ .then(res => res.data)
+ .then((data) => {
+ if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
@@ -65,10 +65,12 @@ export default {
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
- <h4>
- Set by
- <mr-widget-author :author="mr.setToMWPSBy" />
- to be merged automatically when the pipeline succeeds
+ <h4 class="flex-container-block">
+ <span class="append-right-10">
+ Set by
+ <mr-widget-author :author="mr.setToMWPSBy" />
+ to be merged automatically when the pipeline succeeds
+ </span>
<a
v-if="mr.canCancelAutomaticMerge"
@click.prevent="cancelAutomaticMerge"
@@ -94,8 +96,13 @@ export default {
<p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed
</p>
- <p v-else>
- The source branch will not be removed
+ <p
+ v-else
+ class="flex-container-block"
+ >
+ <span class="append-right-10">
+ The source branch will not be removed
+ </span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index 2dfd87ed904..ba9681680ef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -47,9 +47,9 @@ export default {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
- .then(res => res.json())
- .then((res) => {
- if (res.message === 'Branch was removed') {
+ .then(res => res.data)
+ .then((data) => {
+ if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
@@ -68,9 +68,9 @@ export default {
<div class="space-children">
<mr-widget-author-and-time
actionText="Merged by"
- :author="mr.mergedEvent.author"
- :date-title="mr.mergedEvent.updatedAt"
- :date-readable="mr.mergedEvent.formattedUpdatedAt" />
+ :author="mr.metrics.mergedBy"
+ :date-title="mr.metrics.mergedAt"
+ :date-readable="mr.metrics.readableMergedAt" />
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index be37dd87de9..e82fb979162 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -135,16 +135,16 @@ export default {
this.isMakingRequest = true;
this.service.merge(options)
- .then(res => res.json())
- .then((res) => {
- const hasError = res.status === 'failed' || res.status === 'hook_validation_error';
+ .then(res => res.data)
+ .then((data) => {
+ const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
- if (res.status === 'merge_when_pipeline_succeeds') {
+ if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
- } else if (res.status === 'success') {
+ } else if (data.status === 'success') {
this.initiateMergePolling();
} else if (hasError) {
- eventHub.$emit('FailedToMerge', res.merge_error);
+ eventHub.$emit('FailedToMerge', data.merge_error);
}
})
.catch(() => {
@@ -159,9 +159,9 @@ export default {
},
handleMergePolling(continuePolling, stopPolling) {
this.service.poll()
- .then(res => res.json())
- .then((res) => {
- if (res.state === 'merged') {
+ .then(res => res.data)
+ .then((data) => {
+ if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('FetchActionsContent');
@@ -174,11 +174,11 @@ export default {
// If user checked remove source branch and we didn't remove the branch yet
// we should start another polling for source branch remove process
- if (this.removeSourceBranch && res.source_branch_exists) {
+ if (this.removeSourceBranch && data.source_branch_exists) {
this.initiateRemoveSourceBranchPolling();
}
- } else if (res.merge_error) {
- eventHub.$emit('FailedToMerge', res.merge_error);
+ } else if (data.merge_error) {
+ eventHub.$emit('FailedToMerge', data.merge_error);
stopPolling();
} else {
// MR is not merged yet, continue polling until the state becomes 'merged'
@@ -199,11 +199,11 @@ export default {
},
handleRemoveBranchPolling(continuePolling, stopPolling) {
this.service.poll()
- .then(res => res.json())
- .then((res) => {
+ .then(res => res.data)
+ .then((data) => {
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
- if (res.source_branch_exists) {
+ if (data.source_branch_exists) {
continuePolling();
} else {
// Branch is removed. Update widget, stop polling and hide the spinner
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
new file mode 100644
index 00000000000..09276ba2769
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -0,0 +1,133 @@
+<script>
+ import simplePoll from '../../../lib/utils/simple_poll';
+ import eventHub from '../../event_hub';
+ import statusIcon from '../mr_widget_status_icon';
+ import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+ import Flash from '../../../flash';
+
+ export default {
+ name: 'MRWidgetRebase',
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ statusIcon,
+ loadingIcon,
+ },
+ data() {
+ return {
+ isMakingRequest: false,
+ rebasingError: null,
+ };
+ },
+ computed: {
+ status() {
+ if (this.mr.rebaseInProgress || this.isMakingRequest) {
+ return 'loading';
+ }
+ if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) {
+ return 'warning';
+ }
+ return 'success';
+ },
+ showDisabledButton() {
+ return ['failed', 'loading'].includes(this.status);
+ },
+ },
+ methods: {
+ rebase() {
+ this.isMakingRequest = true;
+ this.rebasingError = null;
+
+ this.service.rebase()
+ .then(() => {
+ simplePoll(this.checkRebaseStatus);
+ })
+ .catch((error) => {
+ this.rebasingError = error.merge_error;
+ this.isMakingRequest = false;
+ Flash('Something went wrong. Please try again.');
+ });
+ },
+ checkRebaseStatus(continuePolling, stopPolling) {
+ this.service.poll()
+ .then(res => res.data)
+ .then((res) => {
+ if (res.rebase_in_progress) {
+ continuePolling();
+ } else {
+ this.isMakingRequest = false;
+
+ if (res.merge_error && res.merge_error.length) {
+ this.rebasingError = res.merge_error;
+ Flash('Something went wrong. Please try again.');
+ }
+
+ eventHub.$emit('MRWidgetUpdateRequested');
+ stopPolling();
+ }
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ Flash('Something went wrong. Please try again.');
+ stopPolling();
+ });
+ },
+ },
+ };
+</script>
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ :status="status"
+ :show-disabled-button="showDisabledButton"
+ />
+
+ <div class="rebase-state-find-class-convention media media-body space-children">
+ <template v-if="mr.rebaseInProgress || isMakingRequest">
+ <span class="bold">
+ Rebase in progress
+ </span>
+ </template>
+ <template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch">
+ <span class="bold">
+ Fast-forward merge is not possible.
+ Rebase the source branch onto
+ <span class="label-branch">{{mr.targetBranch}}</span>
+ to allow this merge request to be merged.
+ </span>
+ </template>
+ <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest">
+ <div class="accept-merge-holder clearfix js-toggle-container accept-action media space-children">
+ <button
+ type="button"
+ class="btn btn-sm btn-reopen btn-success"
+ :disabled="isMakingRequest"
+ @click="rebase">
+ <loading-icon v-if="isMakingRequest" />
+ Rebase
+ </button>
+ <span
+ v-if="!rebasingError"
+ class="bold">
+ Fast-forward merge is not possible.
+ Rebase the source branch onto the target branch or merge target
+ branch into source branch to allow this merge request to be merged.
+ </span>
+ <span
+ v-else
+ class="bold danger">
+ {{rebasingError}}
+ </span>
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index 4f83350e07c..13461440ef2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -23,9 +23,9 @@ export default {
removeWIP() {
this.isMakingRequest = true;
this.service.removeWIP()
- .then(res => res.json())
- .then((res) => {
- eventHub.$emit('UpdateWidgetData', res);
+ .then(res => res.data)
+ .then((data) => {
+ eventHub.$emit('UpdateWidgetData', data);
new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 5bd8b99420a..940f3d9b2d0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -32,6 +32,7 @@ export { default as UnresolvedDiscussionsState } from './components/states/mr_wi
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds';
+export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed';
export { default as CheckingState } from './components/states/mr_widget_checking';
export { default as MRWidgetStore } from './stores/mr_widget_store';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 9cb3edead86..2075f8e4fec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -10,6 +10,7 @@ import {
MergedState,
ClosedState,
MergingState,
+ RebaseState,
WipState,
ArchivedState,
ConflictsState,
@@ -62,7 +63,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return !!this.mr.relatedLinks;
+ return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
@@ -79,19 +80,20 @@ export default {
ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
statusPath: store.statusPath,
mergeActionsContentPath: store.mergeActionsContentPath,
+ rebasePath: store.rebasePath,
};
return new MRWidgetService(endpoints);
},
checkStatus(cb) {
return this.service.checkStatus()
- .then(res => res.json())
- .then((res) => {
- this.handleNotification(res);
- this.mr.setData(res);
+ .then(res => res.data)
+ .then((data) => {
+ this.handleNotification(data);
+ this.mr.setData(data);
this.setFaviconHelper();
if (cb) {
- cb.call(null, res);
+ cb.call(null, data);
}
})
.catch(() => {
@@ -124,10 +126,10 @@ export default {
},
fetchDeployments() {
return this.service.fetchDeployments()
- .then(res => res.json())
- .then((res) => {
- if (res.length) {
- this.mr.deployments = res;
+ .then(res => res.data)
+ .then((data) => {
+ if (data.length) {
+ this.mr.deployments = data;
}
})
.catch(() => {
@@ -137,9 +139,9 @@ export default {
fetchActionsContent() {
this.service.fetchMergeActionsContent()
.then((res) => {
- if (res.body) {
+ if (res.data) {
const el = document.createElement('div');
- el.innerHTML = res.body;
+ el.innerHTML = res.data;
document.body.appendChild(el);
Project.initRefSwitcher();
}
@@ -232,6 +234,7 @@ export default {
'mr-widget-pipeline-failed': PipelineFailedState,
'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
'mr-widget-auto-merge-failed': AutoMergeFailed,
+ 'mr-widget-rebase': RebaseState,
},
template: `
<div class="mr-state-widget prepend-top-default">
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 99f5c305df5..fecbfec2214 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -1,57 +1,51 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class MRWidgetService {
constructor(endpoints) {
- this.mergeResource = Vue.resource(endpoints.mergePath);
- this.mergeCheckResource = Vue.resource(endpoints.statusPath);
- this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath);
- this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
- this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
- this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
- this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
- this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
+ this.endpoints = endpoints;
}
merge(data) {
- return this.mergeResource.save(data);
+ return axios.post(this.endpoints.mergePath, data);
}
cancelAutomaticMerge() {
- return this.cancelAutoMergeResource.save();
+ return axios.post(this.endpoints.cancelAutoMergePath);
}
removeWIP() {
- return this.removeWIPResource.save();
+ return axios.post(this.endpoints.removeWIPPath);
}
removeSourceBranch() {
- return this.removeSourceBranchResource.delete();
+ return axios.delete(this.endpoints.sourceBranchPath);
}
fetchDeployments() {
- return this.deploymentsResource.get();
+ return axios.get(this.endpoints.ciEnvironmentsStatusPath);
}
poll() {
- return this.pollResource.get();
+ return axios.get(`${this.endpoints.statusPath}?serializer=basic`);
}
checkStatus() {
- return this.mergeCheckResource.get();
+ return axios.get(`${this.endpoints.statusPath}?serializer=widget`);
}
fetchMergeActionsContent() {
- return this.mergeActionsContentResource.get();
+ return axios.get(this.endpoints.mergeActionsContentPath);
+ }
+
+ rebase() {
+ return axios.post(this.endpoints.rebasePath);
}
static stopEnvironment(url) {
- return Vue.http.post(url);
+ return axios.post(url);
}
static fetchMetrics(metricsUrl) {
- return Vue.http.get(`${metricsUrl}.json`);
+ return axios.get(`${metricsUrl}.json`);
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 7c15abfff10..f7f0c1b6cb7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -1,30 +1,34 @@
+import { stateKey } from './state_maps';
+
export default function deviseState(data) {
if (data.project_archived) {
- return 'archived';
+ return stateKey.archived;
} else if (data.branch_missing) {
- return 'missingBranch';
+ return stateKey.missingBranch;
} else if (!data.commits_count) {
- return 'nothingToMerge';
+ return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked') {
- return 'checking';
+ return stateKey.checking;
} else if (data.has_conflicts) {
- return 'conflicts';
+ return stateKey.conflicts;
} else if (data.work_in_progress) {
- return 'workInProgress';
+ return stateKey.workInProgress;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
- return 'pipelineFailed';
+ return stateKey.pipelineFailed;
} else if (this.hasMergeableDiscussionsState) {
- return 'unresolvedDiscussions';
+ return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
- return 'pipelineBlocked';
+ return stateKey.pipelineBlocked;
} else if (this.hasSHAChanged) {
- return 'shaMismatch';
+ return stateKey.shaMismatch;
} else if (this.mergeWhenPipelineSucceeds) {
- return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
+ return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) {
- return 'notAllowedToMerge';
+ return stateKey.notAllowedToMerge;
+ } else if (this.shouldBeRebased) {
+ return stateKey.rebase;
} else if (this.canBeMerged) {
- return 'readyToMerge';
+ return stateKey.readyToMerge;
}
return null;
}
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 c1f7e64f580..ed004b3bb08 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
@@ -1,5 +1,7 @@
import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies';
+import { stateKey } from './state_maps';
+import { formatDate } from '../../lib/utils/datetime_utility';
export default class MergeRequestStore {
@@ -24,6 +26,7 @@ export default class MergeRequestStore {
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.deployments = this.deployments || data.deployments || [];
+ this.initRebase(data);
if (data.issues_links) {
const links = data.issues_links;
@@ -37,9 +40,8 @@ export default class MergeRequestStore {
}
this.updatedAt = data.updated_at;
- this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
- this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
- this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
+ this.metrics = MergeRequestStore.buildMetrics(data.metrics);
+ this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
this.sourceBranchPath = data.source_branch_path;
@@ -119,43 +121,53 @@ export default class MergeRequestStore {
}
}
- static getEventObject(event) {
- return {
- author: MergeRequestStore.getAuthorObject(event),
- updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
- formattedUpdatedAt: MergeRequestStore.getEventDate(event),
- };
+ get isNothingToMergeState() {
+ return this.state === stateKey.nothingToMerge;
+ }
+
+ initRebase(data) {
+ this.canPushToSourceBranch = data.can_push_to_source_branch;
+ this.rebaseInProgress = data.rebase_in_progress;
+ this.approvalsLeft = !data.approved;
+ this.rebasePath = data.rebase_path;
}
- static getAuthorObject(event) {
- if (!event) {
+ static buildMetrics(metrics) {
+ if (!metrics) {
return {};
}
return {
- name: event.author.name || '',
- username: event.author.username || '',
- webUrl: event.author.web_url || '',
- avatarUrl: event.author.avatar_url || '',
+ mergedBy: MergeRequestStore.formatUserObject(metrics.merged_by),
+ closedBy: MergeRequestStore.formatUserObject(metrics.closed_by),
+ mergedAt: formatDate(metrics.merged_at),
+ closedAt: formatDate(metrics.closed_at),
+ readableMergedAt: MergeRequestStore.getReadableDate(metrics.merged_at),
+ readableClosedAt: MergeRequestStore.getReadableDate(metrics.closed_at),
};
}
- static getEventUpdatedAtDate(event) {
- if (!event) {
- return '';
+ static formatUserObject(user) {
+ if (!user) {
+ return {};
}
- return event.updated_at;
+ return {
+ name: user.name || '',
+ username: user.username || '',
+ webUrl: user.web_url || '',
+ avatarUrl: user.avatar_url || '',
+ };
}
- static getEventDate(event) {
- const timeagoInstance = new Timeago();
-
- if (!event) {
+ static getReadableDate(date) {
+ if (!date) {
return '';
}
- return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
+ const timeagoInstance = new Timeago();
+
+ return timeagoInstance.format(date);
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 9074a064a6d..29d5bd4a1da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -17,6 +17,7 @@ const stateToComponentMap = {
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'mr-widget-sha-mismatch',
+ rebase: 'mr-widget-rebase',
};
const statesToShowHelpWidget = [
@@ -29,8 +30,27 @@ const statesToShowHelpWidget = [
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
+ 'rebase',
];
+export const stateKey = {
+ archived: 'archived',
+ missingBranch: 'missingBranch',
+ nothingToMerge: 'nothingToMerge',
+ checking: 'checking',
+ conflicts: 'conflicts',
+ workInProgress: 'workInProgress',
+ pipelineFailed: 'pipelineFailed',
+ unresolvedDiscussions: 'unresolvedDiscussions',
+ pipelineBlocked: 'pipelineBlocked',
+ shaMismatch: 'shaMismatch',
+ autoMergeFailed: 'autoMergeFailed',
+ mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
+ notAllowedToMerge: 'notAllowedToMerge',
+ readyToMerge: 'readyToMerge',
+ rebase: 'rebase',
+};
+
export default {
stateToComponentMap,
statesToShowHelpWidget,
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 52814de8b2d..59ca9a0a6d4 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -2,13 +2,14 @@
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
+ import Icon from '../../vue_shared/components/icon.vue';
export default {
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render `fa-code-fork` icon.
+ * if false will render a svg sprite fork icon
*/
tag: {
type: Boolean,
@@ -107,6 +108,7 @@
},
components: {
userAvatarLink,
+ Icon,
},
created() {
this.commitIconSvg = commitIconSvg;
@@ -123,11 +125,10 @@
class="fa fa-tag"
aria-hidden="true">
</i>
- <i
+ <icon
v-if="!tag"
- class="fa fa-code-fork"
- aria-hidden="true">
- </i>
+ name="fork">
+ </icon>
</div>
<a
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
new file mode 100644
index 00000000000..05e48ed297f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -0,0 +1,46 @@
+<script>
+ import { __ } from '~/locale';
+ /**
+ * Port of detail_behavior expand button.
+ *
+ * @example
+ * <expand-button>
+ * <template slot="expanded">
+ * Text goes here.
+ * </template>
+ * </expand-button>
+ */
+ export default {
+ name: 'expandButton',
+ data() {
+ return {
+ isCollapsed: true,
+ };
+ },
+ computed: {
+ ariaLabel() {
+ return __('Click to expand text');
+ },
+ },
+ methods: {
+ onClick() {
+ this.isCollapsed = !this.isCollapsed;
+ },
+ },
+ };
+</script>
+<template>
+ <span>
+ <button
+ type="button"
+ v-show="isCollapsed"
+ class="text-expander btn-blank"
+ :aria-label="ariaLabel"
+ @click="onClick">
+ ...
+ </button>
+ <span v-show="!isCollapsed">
+ <slot name="expanded"></slot>
+ </span>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
new file mode 100644
index 00000000000..65c64967fdc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -0,0 +1,92 @@
+<script>
+ import getIconForFile from './file_icon/file_icon_map';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import icon from '../../vue_shared/components/icon.vue';
+
+ /* This is a re-usable vue component for rendering a svg sprite
+ icon
+
+ Sample configuration:
+
+ <file-icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />
+
+ */
+ export default {
+ props: {
+ fileName: {
+ type: String,
+ required: true,
+ },
+
+ folder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ opened: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ },
+
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ components: {
+ loadingIcon,
+ icon,
+ },
+ computed: {
+ spriteHref() {
+ const iconName = getIconForFile(this.fileName) || 'file';
+ return `${gon.sprite_file_icons}#${iconName}`;
+ },
+ folderIconName() {
+ // We don't have a open folder icon yet
+ return this.opened ? 'folder' : 'folder';
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
+ },
+ },
+ };
+</script>
+<template>
+ <span>
+ <svg
+ :class="[iconSizeClass, cssClasses]"
+ v-if="!loading && !folder">
+ <use
+ v-bind="{'xlink:href':spriteHref}"/>
+ </svg>
+ <icon
+ v-if="!loading && folder"
+ :name="folderIconName"
+ :size="size"
+ />
+ <loading-icon
+ v-if="loading"
+ :inline="true"
+ />
+ </span>
+</template>
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
new file mode 100644
index 00000000000..9ffbaae3ea5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -0,0 +1,589 @@
+const fileExtensionIcons = {
+ html: 'html',
+ htm: 'html',
+ html_vm: 'html',
+ asp: 'html',
+ jade: 'pug',
+ pug: 'pug',
+ md: 'markdown',
+ 'md.rendered': 'markdown',
+ markdown: 'markdown',
+ 'markdown.rendered': 'markdown',
+ rst: 'markdown',
+ blink: 'blink',
+ css: 'css',
+ scss: 'sass',
+ sass: 'sass',
+ less: 'less',
+ json: 'json',
+ yaml: 'yaml',
+ 'YAML-tmLanguage': 'yaml',
+ yml: 'yaml',
+ xml: 'xml',
+ plist: 'xml',
+ xsd: 'xml',
+ dtd: 'xml',
+ xsl: 'xml',
+ xslt: 'xml',
+ resx: 'xml',
+ iml: 'xml',
+ xquery: 'xml',
+ tmLanguage: 'xml',
+ manifest: 'xml',
+ project: 'xml',
+ png: 'image',
+ jpeg: 'image',
+ jpg: 'image',
+ gif: 'image',
+ svg: 'image',
+ ico: 'image',
+ tif: 'image',
+ tiff: 'image',
+ psd: 'image',
+ psb: 'image',
+ ami: 'image',
+ apx: 'image',
+ bmp: 'image',
+ bpg: 'image',
+ brk: 'image',
+ cur: 'image',
+ dds: 'image',
+ dng: 'image',
+ exr: 'image',
+ fpx: 'image',
+ gbr: 'image',
+ img: 'image',
+ jbig2: 'image',
+ jb2: 'image',
+ jng: 'image',
+ jxr: 'image',
+ pbm: 'image',
+ pgf: 'image',
+ pic: 'image',
+ raw: 'image',
+ webp: 'image',
+ js: 'javascript',
+ ejs: 'javascript',
+ esx: 'javascript',
+ jsx: 'react',
+ tsx: 'react',
+ ini: 'settings',
+ dlc: 'settings',
+ dll: 'settings',
+ config: 'settings',
+ conf: 'settings',
+ properties: 'settings',
+ prop: 'settings',
+ settings: 'settings',
+ option: 'settings',
+ props: 'settings',
+ toml: 'settings',
+ prefs: 'settings',
+ 'sln.dotsettings': 'settings',
+ 'sln.dotsettings.user': 'settings',
+ ts: 'typescript',
+ 'd.ts': 'typescript-def',
+ marko: 'markojs',
+ pdf: 'pdf',
+ xlsx: 'table',
+ xls: 'table',
+ csv: 'table',
+ tsv: 'table',
+ vscodeignore: 'vscode',
+ vsixmanifest: 'vscode',
+ vsix: 'vscode',
+ 'code-workplace': 'vscode',
+ suo: 'visualstudio',
+ sln: 'visualstudio',
+ csproj: 'visualstudio',
+ vb: 'visualstudio',
+ pdb: 'database',
+ sql: 'database',
+ pks: 'database',
+ pkb: 'database',
+ accdb: 'database',
+ mdb: 'database',
+ sqlite: 'database',
+ cs: 'csharp',
+ zip: 'zip',
+ tar: 'zip',
+ gz: 'zip',
+ xz: 'zip',
+ bzip2: 'zip',
+ gzip: 'zip',
+ '7z': 'zip',
+ rar: 'zip',
+ tgz: 'zip',
+ exe: 'exe',
+ msi: 'exe',
+ java: 'java',
+ jar: 'java',
+ jsp: 'java',
+ c: 'c',
+ m: 'c',
+ h: 'h',
+ cc: 'cpp',
+ cpp: 'cpp',
+ mm: 'cpp',
+ cxx: 'cpp',
+ hpp: 'hpp',
+ go: 'go',
+ py: 'python',
+ url: 'url',
+ sh: 'console',
+ ksh: 'console',
+ csh: 'console',
+ tcsh: 'console',
+ zsh: 'console',
+ bash: 'console',
+ bat: 'console',
+ cmd: 'console',
+ ps1: 'powershell',
+ psm1: 'powershell',
+ psd1: 'powershell',
+ ps1xml: 'powershell',
+ psc1: 'powershell',
+ pssc: 'powershell',
+ gradle: 'gradle',
+ doc: 'word',
+ docx: 'word',
+ rtf: 'word',
+ cer: 'certificate',
+ cert: 'certificate',
+ crt: 'certificate',
+ pub: 'key',
+ key: 'key',
+ pem: 'key',
+ asc: 'key',
+ gpg: 'key',
+ woff: 'font',
+ woff2: 'font',
+ ttf: 'font',
+ eot: 'font',
+ suit: 'font',
+ otf: 'font',
+ bmap: 'font',
+ fnt: 'font',
+ odttf: 'font',
+ ttc: 'font',
+ font: 'font',
+ fonts: 'font',
+ sui: 'font',
+ ntf: 'font',
+ mrf: 'font',
+ lib: 'lib',
+ bib: 'lib',
+ rb: 'ruby',
+ erb: 'ruby',
+ fs: 'fsharp',
+ fsx: 'fsharp',
+ fsi: 'fsharp',
+ fsproj: 'fsharp',
+ swift: 'swift',
+ ino: 'arduino',
+ dockerignore: 'docker',
+ dockerfile: 'docker',
+ tex: 'tex',
+ cls: 'tex',
+ sty: 'tex',
+ pptx: 'powerpoint',
+ ppt: 'powerpoint',
+ pptm: 'powerpoint',
+ potx: 'powerpoint',
+ pot: 'powerpoint',
+ potm: 'powerpoint',
+ ppsx: 'powerpoint',
+ ppsm: 'powerpoint',
+ pps: 'powerpoint',
+ ppam: 'powerpoint',
+ ppa: 'powerpoint',
+ webm: 'movie',
+ mkv: 'movie',
+ flv: 'movie',
+ vob: 'movie',
+ ogv: 'movie',
+ ogg: 'movie',
+ gifv: 'movie',
+ avi: 'movie',
+ mov: 'movie',
+ qt: 'movie',
+ wmv: 'movie',
+ yuv: 'movie',
+ rm: 'movie',
+ rmvb: 'movie',
+ mp4: 'movie',
+ m4v: 'movie',
+ mpg: 'movie',
+ mp2: 'movie',
+ mpeg: 'movie',
+ mpe: 'movie',
+ mpv: 'movie',
+ m2v: 'movie',
+ vdi: 'virtual',
+ vbox: 'virtual',
+ 'vbox-prev': 'virtual',
+ ics: 'email',
+ mp3: 'music',
+ flac: 'music',
+ m4a: 'music',
+ wma: 'music',
+ aiff: 'music',
+ coffee: 'coffee',
+ txt: 'document',
+ graphql: 'graphql',
+ rs: 'rust',
+ raml: 'raml',
+ xaml: 'xaml',
+ hs: 'haskell',
+ kt: 'kotlin',
+ kts: 'kotlin',
+ patch: 'git',
+ lua: 'lua',
+ clj: 'clojure',
+ cljs: 'clojure',
+ groovy: 'groovy',
+ r: 'r',
+ rmd: 'r',
+ dart: 'dart',
+ as: 'actionscript',
+ mxml: 'mxml',
+ ahk: 'autohotkey',
+ swf: 'flash',
+ swc: 'swc',
+ cmake: 'cmake',
+ asm: 'assembly',
+ a51: 'assembly',
+ inc: 'assembly',
+ nasm: 'assembly',
+ s: 'assembly',
+ ms: 'assembly',
+ agc: 'assembly',
+ ags: 'assembly',
+ aea: 'assembly',
+ argus: 'assembly',
+ mitigus: 'assembly',
+ binsource: 'assembly',
+ vue: 'vue',
+ ml: 'ocaml',
+ mli: 'ocaml',
+ cmx: 'ocaml',
+ 'js.map': 'javascript-map',
+ 'css.map': 'css-map',
+ lock: 'lock',
+ hbs: 'handlebars',
+ mustache: 'handlebars',
+ pl: 'perl',
+ pm: 'perl',
+ hx: 'haxe',
+ 'spec.ts': 'test-ts',
+ 'test.ts': 'test-ts',
+ 'ts.snap': 'test-ts',
+ 'spec.tsx': 'test-jsx',
+ 'test.tsx': 'test-jsx',
+ 'tsx.snap': 'test-jsx',
+ 'spec.jsx': 'test-jsx',
+ 'test.jsx': 'test-jsx',
+ 'jsx.snap': 'test-jsx',
+ 'spec.js': 'test-js',
+ 'test.js': 'test-js',
+ 'js.snap': 'test-js',
+ 'routing.ts': 'angular-routing',
+ 'routing.js': 'angular-routing',
+ 'module.ts': 'angular',
+ 'module.js': 'angular',
+ 'ng-template': 'angular',
+ 'component.ts': 'angular-component',
+ 'component.js': 'angular-component',
+ 'guard.ts': 'angular-guard',
+ 'guard.js': 'angular-guard',
+ 'service.ts': 'angular-service',
+ 'service.js': 'angular-service',
+ 'pipe.ts': 'angular-pipe',
+ 'pipe.js': 'angular-pipe',
+ 'filter.js': 'angular-pipe',
+ 'directive.ts': 'angular-directive',
+ 'directive.js': 'angular-directive',
+ 'resolver.ts': 'angular-resolver',
+ 'resolver.js': 'angular-resolver',
+ pp: 'puppet',
+ ex: 'elixir',
+ exs: 'elixir',
+ ls: 'livescript',
+ erl: 'erlang',
+ twig: 'twig',
+ jl: 'julia',
+ elm: 'elm',
+ pure: 'purescript',
+ tpl: 'smarty',
+ styl: 'stylus',
+ re: 'reason',
+ rei: 'reason',
+ cmj: 'bucklescript',
+ merlin: 'merlin',
+ v: 'verilog',
+ vhd: 'verilog',
+ sv: 'verilog',
+ svh: 'verilog',
+ nb: 'mathematica',
+ wl: 'wolframlanguage',
+ wls: 'wolframlanguage',
+ njk: 'nunjucks',
+ nunjucks: 'nunjucks',
+ robot: 'robot',
+ sol: 'solidity',
+ au3: 'autoit',
+ haml: 'haml',
+ yang: 'yang',
+ tf: 'terraform',
+ 'tf.json': 'terraform',
+ tfvars: 'terraform',
+ tfstate: 'terraform',
+ 'blade.php': 'laravel',
+ 'inky.php': 'laravel',
+ applescript: 'applescript',
+ cake: 'cake',
+ feature: 'cucumber',
+ nim: 'nim',
+ nimble: 'nim',
+ apib: 'apiblueprint',
+ apiblueprint: 'apiblueprint',
+ tag: 'riot',
+ vfl: 'vfl',
+ kl: 'kl',
+ pcss: 'postcss',
+ sss: 'postcss',
+ todo: 'todo',
+ cfml: 'coldfusion',
+ cfc: 'coldfusion',
+ lucee: 'coldfusion',
+ cabal: 'cabal',
+ nix: 'nix',
+ slim: 'slim',
+ http: 'http',
+ rest: 'http',
+ rql: 'restql',
+ restql: 'restql',
+ kv: 'kivy',
+ graphcool: 'graphcool',
+ sbt: 'sbt',
+ 'reducer.ts': 'ngrx-reducer',
+ 'rootReducer.ts': 'ngrx-reducer',
+ 'state.ts': 'ngrx-state',
+ 'actions.ts': 'ngrx-actions',
+ 'effects.ts': 'ngrx-effects',
+ cr: 'crystal',
+ 'drone.yml': 'drone',
+ cu: 'cuda',
+ cuh: 'cuda',
+ log: 'log',
+};
+
+const fileNameIcons = {
+ '.jscsrc': 'json',
+ '.jshintrc': 'json',
+ 'tsconfig.json': 'json',
+ 'tslint.json': 'json',
+ 'composer.lock': 'json',
+ '.jsbeautifyrc': 'json',
+ '.esformatter': 'json',
+ 'cdp.pid': 'json',
+ '.htaccess': 'xml',
+ '.jshintignore': 'settings',
+ '.buildignore': 'settings',
+ makefile: 'settings',
+ '.mrconfig': 'settings',
+ '.yardopts': 'settings',
+ 'gradle.properties': 'gradle',
+ gradlew: 'gradle',
+ 'gradle-wrapper.properties': 'gradle',
+ license: 'certificate',
+ 'license.md': 'certificate',
+ 'license.md.rendered': 'certificate',
+ 'license.txt': 'certificate',
+ licence: 'certificate',
+ 'licence.md': 'certificate',
+ 'licence.md.rendered': 'certificate',
+ 'licence.txt': 'certificate',
+ dockerfile: 'docker',
+ 'docker-compose.yml': 'docker',
+ '.mailmap': 'email',
+ '.gitignore': 'git',
+ '.gitconfig': 'git',
+ '.gitattributes': 'git',
+ '.gitmodules': 'git',
+ '.gitkeep': 'git',
+ 'git-history': 'git',
+ '.Rhistory': 'r',
+ 'cmakelists.txt': 'cmake',
+ 'cmakecache.txt': 'cmake',
+ 'angular-cli.json': 'angular',
+ '.angular-cli.json': 'angular',
+ '.vfl': 'vfl',
+ '.kl': 'kl',
+ 'postcss.config.js': 'postcss',
+ '.postcssrc.js': 'postcss',
+ 'project.graphcool': 'graphcool',
+ 'webpack.js': 'webpack',
+ 'webpack.ts': 'webpack',
+ 'webpack.base.js': 'webpack',
+ 'webpack.base.ts': 'webpack',
+ 'webpack.config.js': 'webpack',
+ 'webpack.config.ts': 'webpack',
+ 'webpack.common.js': 'webpack',
+ 'webpack.common.ts': 'webpack',
+ 'webpack.config.common.js': 'webpack',
+ 'webpack.config.common.ts': 'webpack',
+ 'webpack.config.common.babel.js': 'webpack',
+ 'webpack.config.common.babel.ts': 'webpack',
+ 'webpack.dev.js': 'webpack',
+ 'webpack.dev.ts': 'webpack',
+ 'webpack.config.dev.js': 'webpack',
+ 'webpack.config.dev.ts': 'webpack',
+ 'webpack.config.dev.babel.js': 'webpack',
+ 'webpack.config.dev.babel.ts': 'webpack',
+ 'webpack.prod.js': 'webpack',
+ 'webpack.prod.ts': 'webpack',
+ 'webpack.server.js': 'webpack',
+ 'webpack.server.ts': 'webpack',
+ 'webpack.client.js': 'webpack',
+ 'webpack.client.ts': 'webpack',
+ 'webpack.config.server.js': 'webpack',
+ 'webpack.config.server.ts': 'webpack',
+ 'webpack.config.client.js': 'webpack',
+ 'webpack.config.client.ts': 'webpack',
+ 'webpack.config.production.babel.js': 'webpack',
+ 'webpack.config.production.babel.ts': 'webpack',
+ 'webpack.config.prod.babel.js': 'webpack',
+ 'webpack.config.prod.babel.ts': 'webpack',
+ 'webpack.config.prod.js': 'webpack',
+ 'webpack.config.prod.ts': 'webpack',
+ 'webpack.config.production.js': 'webpack',
+ 'webpack.config.production.ts': 'webpack',
+ 'webpack.config.staging.js': 'webpack',
+ 'webpack.config.staging.ts': 'webpack',
+ 'webpack.config.babel.js': 'webpack',
+ 'webpack.config.babel.ts': 'webpack',
+ 'webpack.config.base.babel.js': 'webpack',
+ 'webpack.config.base.babel.ts': 'webpack',
+ 'webpack.config.base.js': 'webpack',
+ 'webpack.config.base.ts': 'webpack',
+ 'webpack.config.staging.babel.js': 'webpack',
+ 'webpack.config.staging.babel.ts': 'webpack',
+ 'webpack.config.coffee': 'webpack',
+ 'webpack.config.test.js': 'webpack',
+ 'webpack.config.test.ts': 'webpack',
+ 'webpack.config.vendor.js': 'webpack',
+ 'webpack.config.vendor.ts': 'webpack',
+ 'webpack.config.vendor.production.js': 'webpack',
+ 'webpack.config.vendor.production.ts': 'webpack',
+ 'webpack.test.js': 'webpack',
+ 'webpack.test.ts': 'webpack',
+ 'webpack.dist.js': 'webpack',
+ 'webpack.dist.ts': 'webpack',
+ 'webpackfile.js': 'webpack',
+ 'webpackfile.ts': 'webpack',
+ 'ionic.config.json': 'ionic',
+ '.io-config.json': 'ionic',
+ 'gulpfile.js': 'gulp',
+ 'gulpfile.ts': 'gulp',
+ 'gulpfile.babel.js': 'gulp',
+ 'package.json': 'nodejs',
+ 'package-lock.json': 'nodejs',
+ '.nvmrc': 'nodejs',
+ '.npmignore': 'npm',
+ '.npmrc': 'npm',
+ '.yarnrc': 'yarn',
+ 'yarn.lock': 'yarn',
+ '.yarnclean': 'yarn',
+ '.yarn-integrity': 'yarn',
+ 'yarn-error.log': 'yarn',
+ 'androidmanifest.xml': 'android',
+ '.env': 'tune',
+ '.env.example': 'tune',
+ '.babelrc': 'babel',
+ 'contributing.md': 'contributing',
+ 'contributing.md.rendered': 'contributing',
+ 'readme.md': 'readme',
+ 'readme.md.rendered': 'readme',
+ changelog: 'changelog',
+ 'changelog.md': 'changelog',
+ 'changelog.md.rendered': 'changelog',
+ CREDITS: 'credits',
+ 'credits.txt': 'credits',
+ 'credits.md': 'credits',
+ 'credits.md.rendered': 'credits',
+ '.flowconfig': 'flow',
+ 'favicon.ico': 'favicon',
+ 'karma.conf.js': 'karma',
+ 'karma.conf.ts': 'karma',
+ 'karma.conf.coffee': 'karma',
+ 'karma.config.js': 'karma',
+ 'karma.config.ts': 'karma',
+ 'karma-main.js': 'karma',
+ 'karma-main.ts': 'karma',
+ '.bithoundrc': 'bithound',
+ 'appveyor.yml': 'appveyor',
+ '.travis.yml': 'travis',
+ 'protractor.conf.js': 'protractor',
+ 'protractor.conf.ts': 'protractor',
+ 'protractor.conf.coffee': 'protractor',
+ 'protractor.config.js': 'protractor',
+ 'protractor.config.ts': 'protractor',
+ 'fuse.js': 'fusebox',
+ procfile: 'heroku',
+ '.editorconfig': 'editorconfig',
+ '.gitlab-ci.yml': 'gitlab',
+ '.bowerrc': 'bower',
+ 'bower.json': 'bower',
+ '.eslintrc.js': 'eslint',
+ '.eslintrc.yaml': 'eslint',
+ '.eslintrc.yml': 'eslint',
+ '.eslintrc.json': 'eslint',
+ '.eslintrc': 'eslint',
+ '.eslintignore': 'eslint',
+ 'code_of_conduct.md': 'conduct',
+ 'code_of_conduct.md.rendered': 'conduct',
+ '.watchmanconfig': 'watchman',
+ 'aurelia.json': 'aurelia',
+ 'mocha.opts': 'mocha',
+ jenkinsfile: 'jenkins',
+ 'firebase.json': 'firebase',
+ '.firebaserc': 'firebase',
+ 'rollup.config.js': 'rollup',
+ 'rollup.config.ts': 'rollup',
+ 'rollup-config.js': 'rollup',
+ 'rollup-config.ts': 'rollup',
+ 'rollup.config.prod.js': 'rollup',
+ 'rollup.config.prod.ts': 'rollup',
+ 'rollup.config.dev.js': 'rollup',
+ 'rollup.config.dev.ts': 'rollup',
+ 'rollup.config.prod.vendor.js': 'rollup',
+ 'rollup.config.prod.vendor.ts': 'rollup',
+ '.hhconfig': 'hack',
+ '.stylelintrc': 'stylelint',
+ 'stylelint.config.js': 'stylelint',
+ '.stylelintrc.json': 'stylelint',
+ '.stylelintrc.yaml': 'stylelint',
+ '.stylelintrc.yml': 'stylelint',
+ '.stylelintrc.js': 'stylelint',
+ '.stylelintignore': 'stylelint',
+ '.codeclimate.yml': 'code-climate',
+ '.prettierrc': 'prettier',
+ 'prettier.config.js': 'prettier',
+ '.prettierrc.js': 'prettier',
+ '.prettierrc.json': 'prettier',
+ '.prettierrc.yaml': 'prettier',
+ '.prettierrc.yml': 'prettier',
+ 'nodemon.json': 'nodemon',
+ '.sonarrc': 'sonar',
+ browserslist: 'browserlist',
+ '.browserslistrc': 'browserlist',
+ '.snyk': 'snyk',
+ '.drone.yml': 'drone',
+};
+
+export default function getIconForFile(name) {
+ return fileNameIcons[name] ||
+ fileExtensionIcons[name ? name.split('.').pop() : ''] ||
+ '';
+}
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 d305bd6acdc..2209bc0f9cf 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -45,6 +45,11 @@ export default {
required: false,
default: false,
},
+ shouldRenderTriggeredLabel: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
directives: {
@@ -82,7 +87,12 @@ export default {
{{itemName}} #{{itemId}}
</strong>
- triggered
+ <template v-if="shouldRenderTriggeredLabel">
+ triggered
+ </template>
+ <template v-else>
+ created
+ </template>
<timeago-tooltip :time="time" />
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 4216660da8c..365229ea274 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -36,6 +36,30 @@
required: false,
default: '',
},
+
+ width: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+
+ height: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+
+ y: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+
+ x: {
+ type: Number,
+ required: false,
+ default: null,
+ },
},
computed: {
@@ -51,7 +75,11 @@
<template>
<svg
- :class="[iconSizeClass, cssClasses]">
+ :class="[iconSizeClass, cssClasses]"
+ :width="width"
+ :height="height"
+ :x="x"
+ :y="y">
<use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 6c575d8eb49..36d2d1dc164 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -72,7 +72,9 @@
Preview
</a>
</li>
- <li class="md-header-toolbar">
+ <li
+ class="md-header-toolbar"
+ :class="{ active: !previewMarkdown }">
<toolbar-button
tag="**"
button-title="Add bold text"
diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.js
index 643b77e04c7..f37ef1a5ca3 100644
--- a/app/assets/javascripts/vue_shared/components/memory_graph.js
+++ b/app/assets/javascripts/vue_shared/components/memory_graph.js
@@ -1,3 +1,5 @@
+import { getTimeago } from '../../lib/utils/datetime_utility';
+
export default {
name: 'MemoryGraph',
props: {
@@ -16,7 +18,7 @@ export default {
},
computed: {
getFormattedMedian() {
- const deployedSince = gl.utils.getTimeago().format(this.deploymentTime * 1000);
+ const deployedSince = getTimeago().format(this.deploymentTime * 1000);
return `Deployed ${deployedSince}`;
},
},
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/modal.vue
index 47efee64c6e..00089dfef38 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/modal.vue
@@ -1,8 +1,12 @@
<script>
export default {
- name: 'popup-dialog',
+ name: 'modal',
props: {
+ id: {
+ type: String,
+ required: false,
+ },
title: {
type: String,
required: false,
@@ -38,7 +42,8 @@ export default {
},
primaryButtonLabel: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
submitDisabled: {
type: Boolean,
@@ -61,11 +66,11 @@ export default {
},
methods: {
- close() {
- this.$emit('toggle', false);
+ emitCancel(event) {
+ this.$emit('cancel', event);
},
- emitSubmit(status) {
- this.$emit('submit', status);
+ emitSubmit(event) {
+ this.$emit('submit', event);
},
},
};
@@ -74,7 +79,9 @@ export default {
<template>
<div class="modal-open">
<div
- class="modal popup-dialog"
+ :id="id"
+ class="modal"
+ :class="id ? '' : 'show'"
role="dialog"
tabindex="-1"
>
@@ -92,7 +99,8 @@ export default {
<button
type="button"
class="close pull-right"
- @click="close"
+ @click="emitCancel($event)"
+ data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
@@ -109,21 +117,27 @@ export default {
type="button"
class="btn pull-left"
:class="btnCancelKindClass"
- @click="close">
+ @click="emitCancel($event)"
+ data-dismiss="modal">
{{ closeButtonLabel }}
</button>
<button
+ v-if="primaryButtonLabel"
type="button"
- class="btn pull-right"
+ class="btn pull-right js-primary-button"
:disabled="submitDisabled"
:class="btnKindClass"
- @click="emitSubmit(true)">
+ @click="emitSubmit($event)"
+ data-dismiss="modal">
{{ primaryButtonLabel }}
</button>
</div>
</div>
</div>
</div>
- <div class="modal-backdrop fade in" />
+ <div
+ v-if="!id"
+ class="modal-backdrop fade in">
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
new file mode 100644
index 00000000000..4371534d345
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
@@ -0,0 +1,91 @@
+<script>
+export default {
+ props: {
+ startSize: {
+ type: Number,
+ required: true,
+ },
+ side: {
+ type: String,
+ required: true,
+ },
+ minSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ maxSize: {
+ type: Number,
+ required: false,
+ default: Number.MAX_VALUE,
+ },
+ enabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ size: this.startSize,
+ };
+ },
+ computed: {
+ className() {
+ return `drag${this.side}`;
+ },
+ cursorStyle() {
+ if (this.enabled) {
+ return { cursor: 'ew-resize' };
+ }
+ return {};
+ },
+ },
+ methods: {
+ resetSize(e) {
+ e.preventDefault();
+ this.size = this.startSize;
+ this.$emit('update:size', this.size);
+ },
+ startDrag(e) {
+ if (this.enabled) {
+ e.preventDefault();
+ this.startPos = e.clientX;
+ this.currentStartSize = this.size;
+ document.addEventListener('mousemove', this.drag);
+ document.addEventListener('mouseup', this.endDrag, { once: true });
+ this.$emit('resize-start', this.size);
+ }
+ },
+ drag(e) {
+ e.preventDefault();
+ let moved = e.clientX - this.startPos;
+ if (this.side === 'left') moved = -moved;
+ let newSize = this.currentStartSize + moved;
+ if (newSize < this.minSize) {
+ newSize = this.minSize;
+ } else if (newSize > this.maxSize) {
+ newSize = this.maxSize;
+ }
+ this.size = newSize;
+
+ this.$emit('update:size', newSize);
+ },
+ endDrag(e) {
+ e.preventDefault();
+ document.removeEventListener('mousemove', this.drag);
+ this.$emit('resize-end', this.size);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="dragHandle"
+ :class="className"
+ :style="cursorStyle"
+ @mousedown="startDrag"
+ @dblclick="resetSize"
+ ></div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
new file mode 100644
index 00000000000..dce23bd65f6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
@@ -0,0 +1,103 @@
+<script>
+
+/* This is a re-usable vue component for rendering a project avatar that
+ does not need to link to the project's profile. The image and an optional
+ tooltip can be configured by props passed to this component.
+
+ Sample configuration:
+
+ <project-avatar-image
+ :lazy="true"
+ :img-src="projectAvatarSrc"
+ :img-alt="tooltipText"
+ :tooltip-text="tooltipText"
+ tooltip-placement="top"
+ />
+
+*/
+
+import defaultAvatarUrl from 'images/no_avatar.png';
+import { placeholderImage } from '../../../lazy_loader';
+import tooltip from '../../directives/tooltip';
+
+export default {
+ name: 'ProjectAvatarImage',
+ props: {
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ imgSrc: {
+ type: String,
+ required: false,
+ default: defaultAvatarUrl,
+ },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgAlt: {
+ type: String,
+ required: false,
+ default: 'project avatar',
+ },
+ size: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside project avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ sanitizedSource() {
+ return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ },
+ resultantSrcAttribute() {
+ return this.lazy ? placeholderImage : this.sanitizedSource;
+ },
+ tooltipContainer() {
+ return this.tooltipText ? 'body' : null;
+ },
+ avatarSizeClass() {
+ return `s${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <img
+ v-tooltip
+ class="avatar"
+ :class="{
+ lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-src="sanitizedSource"
+ :data-container="tooltipContainer"
+ :data-placement="tooltipPlacement"
+ :title="tooltipText"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
new file mode 100644
index 00000000000..16d60bb2876
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -0,0 +1,85 @@
+<script>
+import modal from './modal.vue';
+
+export default {
+ name: 'recaptcha-modal',
+
+ props: {
+ html: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ script: {},
+ scriptSrc: 'https://www.google.com/recaptcha/api.js',
+ };
+ },
+
+ components: {
+ modal,
+ },
+
+ methods: {
+ appendRecaptchaScript() {
+ this.removeRecaptchaScript();
+
+ const script = document.createElement('script');
+ script.src = this.scriptSrc;
+ script.classList.add('js-recaptcha-script');
+ script.async = true;
+ script.defer = true;
+
+ this.script = script;
+
+ document.body.appendChild(script);
+ },
+
+ removeRecaptchaScript() {
+ if (this.script instanceof Element) this.script.remove();
+ },
+
+ close() {
+ this.removeRecaptchaScript();
+ this.$emit('close');
+ },
+
+ submit() {
+ this.$el.querySelector('form').submit();
+ },
+ },
+
+ watch: {
+ html() {
+ this.appendRecaptchaScript();
+ },
+ },
+
+ mounted() {
+ window.recaptchaDialogCallback = this.submit.bind(this);
+ },
+};
+</script>
+
+<template>
+<modal
+ kind="warning"
+ class="recaptcha-modal js-recaptcha-modal"
+ :hide-footer="true"
+ :title="__('Please solve the reCAPTCHA')"
+ @cancel="close"
+>
+ <div slot="body">
+ <p>
+ {{__('We want to be sure it is you, please confirm you are not a robot.')}}
+ </p>
+ <div
+ ref="recaptcha"
+ v-html="html"
+ ></div>
+ </div>
+</modal>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 710452bb3d3..33096b53cf8 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -122,11 +122,17 @@ export default {
return items;
},
+ showPagination() {
+ return this.pageInfo.totalPages > 1;
+ },
},
};
</script>
<template>
- <div class="gl-pagination">
+ <div
+ v-if="showPagination"
+ class="gl-pagination"
+ >
<ul class="pagination clearfix">
<li
v-for="item in getItems"
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index ddc9ddbc3a3..4277d9281a0 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -1,6 +1,13 @@
<script>
+ import { s__ } from '../../locale';
+ import icon from './icon.vue';
import loadingIcon from './loading_icon.vue';
+ const ICON_ON = 'status_success_borderless';
+ const ICON_OFF = 'status_failed_borderless';
+ const LABEL_ON = s__('ToggleButton|Toggle Status: ON');
+ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
+
export default {
props: {
name: {
@@ -22,19 +29,10 @@
required: false,
default: false,
},
- enabledText: {
- type: String,
- required: false,
- default: 'Enabled',
- },
- disabledText: {
- type: String,
- required: false,
- default: 'Disabled',
- },
},
components: {
+ icon,
loadingIcon,
},
@@ -43,6 +41,15 @@
event: 'change',
},
+ computed: {
+ toggleIcon() {
+ return this.value ? ICON_ON : ICON_OFF;
+ },
+ ariaLabel() {
+ return this.value ? LABEL_ON : LABEL_OFF;
+ },
+ },
+
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
@@ -60,10 +67,8 @@
/>
<button
type="button"
- aria-label="Toggle"
class="project-feature-toggle"
- :data-enabled-text="enabledText"
- :data-disabled-text="disabledText"
+ :aria-label="ariaLabel"
:class="{
'is-checked': value,
'is-disabled': disabledInput,
@@ -72,6 +77,11 @@
@click="toggleFeature"
>
<loadingIcon class="loading-icon" />
+ <span class="toggle-icon">
+ <icon
+ css-classes="toggle-icon-svg"
+ :name="toggleIcon"/>
+ </span>
</button>
</label>
</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js
new file mode 100644
index 00000000000..ff1f565e79a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js
@@ -0,0 +1,36 @@
+import recaptchaModal from '../components/recaptcha_modal.vue';
+
+export default {
+ data() {
+ return {
+ showRecaptcha: false,
+ recaptchaHTML: '',
+ };
+ },
+
+ components: {
+ recaptchaModal,
+ },
+
+ methods: {
+ openRecaptcha() {
+ this.showRecaptcha = true;
+ },
+
+ closeRecaptcha() {
+ this.showRecaptcha = false;
+ },
+
+ checkForSpam(data) {
+ if (!data.recaptcha_html) return data;
+
+ this.recaptchaHTML = data.recaptcha_html;
+
+ const spamError = new Error(data.error_message);
+ spamError.name = 'SpamError';
+ spamError.message = 'SpamError';
+
+ throw spamError;
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
index 20f63ab663c..4e3b9d7b767 100644
--- a/app/assets/javascripts/vue_shared/mixins/timeago.js
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -1,4 +1,4 @@
-import '../../lib/utils/datetime_utility';
+import { formatDate, getTimeago } from '../../lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
@@ -6,13 +6,13 @@ import '../../lib/utils/datetime_utility';
export default {
methods: {
timeFormated(time) {
- const timeago = gl.utils.getTimeago();
+ const timeago = getTimeago();
return timeago.format(time);
},
tooltipTitle(time) {
- return gl.utils.formatDate(time);
+ return formatDate(time);
},
},
};
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 26db2386879..077d0424093 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -71,7 +71,7 @@
vertical-align: top;
&.s16 { font-size: 12px; line-height: 1.33; }
- &.s24 { font-size: 14px; line-height: 1.8; }
+ &.s24 { font-size: 13px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; }
&.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 91976ca1f56..c5c7afe25be 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -10,7 +10,6 @@
color: $gl-text-color;
font-weight: $gl-font-weight-normal;
font-size: 14px;
- line-height: 36px;
&.diff-collapsed {
padding: 5px;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 3f630f82e29..fcc420923f9 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -4,8 +4,8 @@
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
- width: 23px;
- height: 23px;
+ width: 24px;
+ height: 24px;
border: 1px solid $blue-500;
&:hover,
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index b73932eb7e1..1acde98c3ae 100644
--- a/app/assets/stylesheets/framework/contextual-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -1,4 +1,6 @@
.page-with-contextual-sidebar {
+ transition: padding-left $sidebar-transition-duration;
+
@media (min-width: $screen-md-min) {
padding-left: $contextual-sidebar-collapsed-width;
}
@@ -7,12 +9,6 @@
padding-left: $contextual-sidebar-width;
}
- // Override position: absolute
- .right-sidebar {
- position: fixed;
- height: calc(100% - #{$header-height});
- }
-
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
padding: 10px 0 15px;
}
@@ -27,8 +23,10 @@
.context-header {
position: relative;
margin-right: 2px;
+ width: $contextual-sidebar-width;
a {
+ transition: padding $sidebar-transition-duration;
font-weight: $gl-font-weight-bold;
display: flex;
align-items: center;
@@ -63,10 +61,10 @@
}
.nav-sidebar {
+ transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
z-index: 400;
width: $contextual-sidebar-width;
- transition: left $sidebar-transition-duration;
top: $header-height;
bottom: 0;
left: 0;
@@ -74,16 +72,15 @@
box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
- &:not(.sidebar-icons-only) {
+ &:not(.sidebar-collapsed-desktop) {
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
- &.sidebar-icons-only {
- width: auto;
- min-width: $contextual-sidebar-collapsed-width;
+ &.sidebar-collapsed-desktop {
+ width: $contextual-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -108,12 +105,11 @@
}
}
- &.nav-sidebar-expanded {
+ &.sidebar-expanded-mobile {
left: 0;
}
a {
- transition: none;
text-decoration: none;
}
@@ -126,9 +122,10 @@
white-space: nowrap;
a {
+ transition: padding $sidebar-transition-duration;
display: flex;
align-items: center;
- padding: 12px 16px;
+ padding: 12px 15px;
color: $gl-text-color-secondary;
}
@@ -288,7 +285,8 @@
> a {
margin-left: 4px;
- padding-left: 12px;
+ // Subtract width of left border on active element
+ padding-left: 11px;
}
.badge {
@@ -313,15 +311,17 @@
.toggle-sidebar-button,
.close-nav-button {
width: $contextual-sidebar-width - 2px;
+ transition: width $sidebar-transition-duration;
position: fixed;
bottom: 0;
- padding: 16px;
+ padding: $gl-padding;
background-color: $gray-light;
border: 0;
border-top: 2px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
+ line-height: 1;
svg {
margin-right: 8px;
@@ -343,25 +343,22 @@
}
}
+.collapse-text {
+ white-space: nowrap;
+ overflow: hidden;
+}
-.sidebar-icons-only {
+.sidebar-collapsed-desktop {
.context-header {
- height: 61px;
+ height: 60px;
+ width: $contextual-sidebar-collapsed-width;
a {
padding: 10px 4px;
}
}
- li a {
- padding: 12px 15px;
- }
-
.sidebar-top-level-items > li {
- &.active a {
- padding-left: 12px;
- }
-
.sidebar-sub-level-items {
&:not(.flyout-list) {
display: none;
@@ -374,8 +371,8 @@
}
.toggle-sidebar-button {
- width: $contextual-sidebar-collapsed-width - 2px;
padding: 16px;
+ width: $contextual-sidebar-collapsed-width - 2px;
.collapse-text,
.icon-angle-double-left {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 8d83554d813..bc907a390d8 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -16,27 +16,18 @@
@mixin set-visible {
transform: translateY(0);
- visibility: visible;
- opacity: 1;
- transition-duration: 100ms, 150ms, 25ms;
- transition-delay: 35ms, 50ms, 25ms;
+ display: block;
}
@mixin set-invisible {
transform: translateY(-10px);
- visibility: hidden;
- opacity: 0;
- transition-property: opacity, transform, visibility;
- transition-duration: 70ms, 250ms, 250ms;
- transition-timing-function: linear, $dropdown-animation-timing;
- transition-delay: 25ms, 50ms, 0ms;
+ display: none;
}
.open {
.dropdown-menu,
.dropdown-menu-nav {
@include set-visible;
- display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) {
@@ -55,6 +46,11 @@
}
}
+// Get search dropdown to line up with other nav dropdowns
+.search-input-container .dropdown-menu {
+ margin-top: 11px;
+}
+
.dropdown-toggle {
padding: 6px 8px 6px 10px;
background-color: $white-light;
@@ -143,20 +139,48 @@
}
}
+@mixin dropdown-item-hover {
+ background-color: $dropdown-item-hover-bg;
+ color: $gl-text-color;
+ outline: 0;
+
+ // make sure the text color is not overriden
+ &.text-danger {
+ color: $brand-danger;
+ }
+
+ .avatar {
+ border-color: $white-light;
+ }
+}
+
@mixin dropdown-link {
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
display: block;
+ font-weight: $gl-font-weight-normal;
position: relative;
- padding: 5px 8px;
+ padding: 8px 16px;
color: $gl-text-color;
- line-height: initial;
- border-radius: 2px;
- white-space: nowrap;
+ line-height: normal;
+ white-space: normal;
overflow: hidden;
+ text-align: left;
+ width: 100%;
+
+ // make sure the text color is not overriden
+ &.text-danger {
+ color: $brand-danger;
+ }
&:hover,
+ &:active,
&:focus,
&.is-focused {
- background-color: $dropdown-link-hover-bg;
+ @include dropdown-item-hover;
+
text-decoration: none;
.badge {
@@ -166,6 +190,13 @@
&.dropdown-menu-user-link {
line-height: 16px;
+ padding-top: 10px;
+ padding-bottom: 7px;
+ white-space: nowrap;
+
+ .dropdown-menu-user-username {
+ display: block;
+ }
}
.icon-play {
@@ -179,7 +210,6 @@
.dropdown-menu,
.dropdown-menu-nav {
@include set-invisible;
- display: block;
position: absolute;
width: auto;
top: 100%;
@@ -187,8 +217,8 @@
z-index: 300;
min-width: 240px;
max-width: 500px;
- margin-top: 2px;
- margin-bottom: 2px;
+ margin-top: $dropdown-vertical-offset;
+ margin-bottom: 24px;
font-size: 14px;
font-weight: $gl-font-weight-normal;
padding: 8px 0;
@@ -197,6 +227,10 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ &.dropdown-open-top {
+ margin-bottom: $dropdown-vertical-offset;
+ }
+
&.dropdown-open-left {
right: 0;
left: auto;
@@ -227,16 +261,27 @@
}
li {
+ display: block;
text-align: left;
list-style: none;
- padding: 0 10px;
+ padding: 0 1px;
+
+ a,
+ button,
+ .menu-item {
+ @include dropdown-link;
+ }
}
.divider {
height: 1px;
- margin: 6px 10px;
+ margin: 6px 0;
padding: 0;
background-color: $dropdown-divider-color;
+
+ &:hover {
+ background-color: $dropdown-divider-color;
+ }
}
.separator {
@@ -247,10 +292,6 @@
background-color: $dropdown-divider-color;
}
- a {
- @include dropdown-link;
- }
-
.dropdown-menu-empty-item a {
&:hover,
&:focus {
@@ -262,7 +303,7 @@
color: $gl-text-color-secondary;
font-size: 13px;
line-height: 22px;
- padding: 0 16px;
+ padding: 8px 16px;
}
&.capitalize-header .dropdown-header {
@@ -277,7 +318,7 @@
.separator + .dropdown-header,
.separator + .dropdown-bold-header {
- padding-top: 2px;
+ padding-top: 10px;
}
.unclickable {
@@ -298,48 +339,28 @@
}
.dropdown-menu li {
- padding: $gl-btn-padding;
cursor: pointer;
+ &.droplab-item-active button {
+ @include dropdown-item-hover;
+ }
+
> a,
> button {
display: flex;
margin: 0;
- padding: 0;
- border-radius: 0;
text-overflow: inherit;
- background-color: inherit;
- color: inherit;
- border: inherit;
text-align: left;
- &:hover,
- &:focus {
- background-color: inherit;
- color: inherit;
- }
-
&.btn .fa:not(:last-child) {
margin-left: 5px;
}
}
- &:hover,
- &:focus {
- background-color: $dropdown-hover-color;
- color: $white-light;
- }
-
&.droplab-item-selected i {
visibility: visible;
}
- &.divider {
- margin: 0 8px;
- padding: 0;
- border-top: $gray-darkest;
- }
-
.icon {
visibility: hidden;
}
@@ -431,11 +452,6 @@
}
}
-.dropdown-menu-user-link {
- padding-top: 10px;
- padding-bottom: 7px;
-}
-
.dropdown-menu-user-full-name {
display: block;
font-weight: $gl-font-weight-normal;
@@ -464,41 +480,44 @@
.dropdown-menu-align-right {
left: auto;
right: 0;
- margin-top: -5px;
}
.dropdown-menu-selectable {
- a {
- padding-left: 26px;
- position: relative;
+ li {
+ a {
+ padding: 8px 40px;
+ position: relative;
+
+ &.is-indeterminate,
+ &.is-active {
+ color: $gl-text-color;
+
+ &::before {
+ position: absolute;
+ left: 16px;
+ top: 16px;
+ transform: translateY(-50%);
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
- &.is-indeterminate,
- &.is-active {
- font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
-
- &::before {
- position: absolute;
- left: 6px;
- top: 50%;
- transform: translateY(-50%);
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+ &.dropdown-menu-user-link {
+ &::before {
+ top: 50%;
+ }
+ }
}
- }
- &.is-indeterminate::before {
- content: "\f068";
- }
+ &.is-indeterminate::before {
+ content: "\f068";
+ }
- &.is-active::before {
- content: "\f00c";
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
+ &.is-active::before {
+ content: "\f00c";
+ }
}
}
}
@@ -735,136 +754,6 @@
}
}
-@mixin dropdown-item-hover {
- background-color: $dropdown-item-hover-bg;
- color: $gl-text-color;
-}
-
-// TODO: change global style and remove mixin
-@mixin new-style-dropdown($selector: '') {
- #{$selector}.dropdown-menu,
- #{$selector}.dropdown-menu-nav {
- margin-bottom: 24px;
-
- &.dropdown-open-top {
- margin-bottom: $dropdown-vertical-offset;
- }
-
- li {
- display: block;
- padding: 0 1px;
-
- &:hover {
- background-color: transparent;
- }
-
- &.divider {
- margin: 6px 0;
-
- &:hover {
- background-color: $dropdown-divider-color;
- }
- }
-
- &.dropdown-header {
- padding: 8px 16px;
- }
-
- &.droplab-item-active button {
- @include dropdown-item-hover;
- }
-
- a,
- button,
- .menu-item {
- margin-bottom: 0;
- border-radius: 0;
- box-shadow: none;
- padding: 8px 16px;
- text-align: left;
- white-space: normal;
- width: 100%;
- font-weight: $gl-font-weight-normal;
- line-height: normal;
-
- &.dropdown-menu-user-link {
- white-space: nowrap;
-
- .dropdown-menu-user-username {
- display: block;
- }
- }
-
- // make sure the text color is not overriden
- &.text-danger {
- color: $brand-danger;
- }
-
- &.is-focused,
- &:hover,
- &:active,
- &:focus {
- @include dropdown-item-hover;
-
- background-color: $dropdown-item-hover-bg;
- color: $gl-text-color;
-
- // make sure the text color is not overriden
- &.text-danger {
- color: $brand-danger;
- }
- }
-
- &.is-active {
- font-weight: inherit;
-
- &::before {
- top: 16px;
- }
-
- &.dropdown-menu-user-link::before {
- top: 50%;
- transform: translateY(-50%);
- }
- }
- }
-
- &.dropdown-menu-empty-item a {
- &:hover,
- &:focus {
- background-color: transparent;
- }
- }
- }
-
- &.dropdown-menu-selectable {
- li {
- a {
- padding: 8px 40px;
-
- &.is-indeterminate::before,
- &.is-active::before {
- left: 16px;
- }
- }
- }
- }
- }
-
- #{$selector}.dropdown-menu-align-right {
- margin-top: 2px;
- }
-
- .open {
- #{$selector}.dropdown-menu,
- #{$selector}.dropdown-menu-nav {
- @media (max-width: $screen-xs-max) {
- max-width: 100%;
- }
- }
- }
-}
-
@media (max-width: $screen-xs-max) {
.navbar-gitlab {
li.header-projects,
@@ -891,9 +780,6 @@
}
}
-@include new-style-dropdown('.breadcrumbs-list .dropdown ');
-@include new-style-dropdown('.js-namespace-select + ');
-
header.header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 609f33582e1..1588036aeae 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -396,3 +396,8 @@ span.idiff {
.file-fork-suggestion-note {
margin-right: 1.5em;
}
+
+.label-lfs {
+ color: $common-gray-light;
+ border: 1px solid $common-gray-light;
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index cec38eea464..2d7465401f1 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -50,8 +50,6 @@
}
.filtered-search-wrapper {
- @include new-style-dropdown;
-
display: -webkit-flex;
display: flex;
@@ -165,16 +163,6 @@
}
}
-.droplab-dropdown li.filtered-search-token {
- padding: 0;
-
- &:hover,
- &:focus {
- background-color: inherit;
- color: inherit;
- }
-}
-
.filtered-search-term {
.name {
background-color: inherit;
@@ -336,21 +324,12 @@
.filtered-search-history-dropdown-content {
max-height: none;
-}
-
-.filtered-search-history-dropdown-item,
-.filtered-search-history-clear-button {
- @include dropdown-link;
-
- overflow: hidden;
- width: 100%;
- margin: 0.5em 0;
- background-color: transparent;
- border: 0;
- text-align: left;
- white-space: nowrap;
- text-overflow: ellipsis;
+ .filtered-search-history-dropdown-item,
+ .filtered-search-history-clear-button {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
}
.filtered-search-history-dropdown-token {
@@ -402,24 +381,9 @@
}
}
-%filter-dropdown-item-btn-hover {
- text-decoration: none;
- outline: 0;
-
- .avatar {
- border-color: $white-light;
- }
-}
-
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn {
- border: 0;
- width: 100%;
- text-align: left;
- padding: 8px 16px;
text-overflow: ellipsis;
- overflow: hidden;
- border-radius: 0;
.fa {
width: 15px;
@@ -434,11 +398,6 @@
height: 17px;
top: 0;
}
-
- &:hover,
- &:focus {
- @extend %filter-dropdown-item-btn-hover;
- }
}
.dropdown-light-content {
@@ -459,17 +418,9 @@
word-break: break-all;
}
}
-
- &.droplab-item-active .btn {
- @extend %filter-dropdown-item-btn-hover;
- }
}
.filter-dropdown-loading {
padding: 8px 16px;
text-align: center;
}
-
-.issues-details-filters {
- @include new-style-dropdown;
-}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index f985a3aea5c..ad160f37641 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,10 +1,4 @@
-.content-wrapper.page-with-new-nav {
- margin-top: $header-height;
-}
-
.navbar-gitlab {
- @include new-style-dropdown;
-
&.navbar-gitlab {
padding: 0 16px;
z-index: 1000;
@@ -522,7 +516,7 @@
.header-user {
.dropdown-menu-nav {
width: auto;
- min-width: 140px;
+ min-width: 160px;
margin-top: 4px;
color: $gl-text-color;
left: auto;
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 78a8e57ddbb..fd5c3c81a53 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -19,6 +19,16 @@
max-width: 425px;
width: 100%;
}
+
+ $image-widths: 250 306 394;
+ @each $width in $image-widths {
+ &.svg-#{$width} {
+ img,
+ svg {
+ width: #{$width + 'px'};
+ }
+ }
+ }
}
@mixin svg-size($size) {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 1537b0744cc..1d8bd26cf1a 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -24,10 +24,14 @@
font-size: $gl-font-size;
line-height: 25px;
- &.status-box-closed {
+ &.status-box-mr-closed {
background-color: $gl-danger;
}
+ &.status-box-issue-closed {
+ background-color: $gl-primary;
+ }
+
&.status-box-merged {
background-color: $gl-primary;
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index cb324ccc440..3f0268541a4 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -24,6 +24,7 @@ body {
}
.content-wrapper {
+ margin-top: $header-height;
padding-bottom: 100px;
}
@@ -105,11 +106,11 @@ body {
}
}
-.page-with-sidebar > .content-wrapper {
+.layout-page > .content-wrapper {
min-height: calc(100vh - #{$header-height});
}
-.with-performance-bar .page-with-sidebar {
+.with-performance-bar .layout-page {
margin-top: $header-height + $performance-bar-height;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index e6e6c4c3963..dcd98cb522f 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -12,6 +12,7 @@
padding: 10px 15px;
min-height: 20px;
border-bottom: 1px solid $list-border;
+ word-wrap: break-word;
&::after {
content: " ";
@@ -125,15 +126,11 @@ ul.content-list {
}
.description {
- p {
- @include str-truncated;
- margin-bottom: 0;
- }
+ @include str-truncated;
+ color: $gl-text-color-secondary;
}
.controls {
- @include new-style-dropdown;
-
float: right;
> .control-text {
@@ -316,7 +313,7 @@ ul.indent-list {
border: 2px solid $white-normal;
&.identicon {
- line-height: 30px;
+ line-height: 15px;
}
}
}
@@ -350,14 +347,19 @@ ul.indent-list {
.folder-caret {
width: 15px;
+
+ svg {
+ margin-bottom: 2px;
+ }
}
.item-type-icon {
+ margin-top: 2px;
width: 20px;
}
> .group-row:not(.has-children) {
- .folder-caret .fa {
+ .folder-caret {
opacity: 0;
}
}
@@ -440,12 +442,61 @@ ul.indent-list {
.avatar-container > a {
width: 100%;
+ text-decoration: none;
}
&.has-more-items {
display: block;
padding: 20px 10px;
}
+
+ .stats {
+ position: relative;
+ line-height: 46px;
+
+ > span {
+ display: inline-flex;
+ align-items: center;
+ height: 16px;
+ min-width: 30px;
+ }
+
+ > span:last-child {
+ margin-right: 0;
+ }
+
+ .stat-value {
+ margin: 2px 0 0 5px;
+ }
+ }
+
+ .controls {
+ margin-left: 5px;
+
+ > .btn {
+ margin-right: $btn-xs-side-margin;
+ }
+ }
+ }
+
+ .project-row-contents .stats {
+ line-height: inherit;
+
+ > span:first-child {
+ margin-left: 25px;
+ }
+
+ .item-visibility {
+ margin-right: 0;
+ }
+
+ .last-updated {
+ position: absolute;
+ right: 12px;
+ min-width: 250px;
+ text-align: right;
+ color: $gl-text-color-secondary;
+ }
}
}
@@ -457,12 +508,12 @@ ul.indent-list {
ul.group-list-tree {
li.group-row {
- &.has-description .title {
- line-height: inherit;
+ > .group-row-contents .title {
+ line-height: $list-text-height;
}
- &:not(.has-description) .title {
- line-height: $list-text-height;
+ &.has-description > .group-row-contents .title {
+ line-height: inherit;
}
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 5389eb0a5f2..938f5f49c09 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -74,7 +74,7 @@
}
.md-header-tab {
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
flex: 1;
width: 100%;
border-bottom: 1px solid $border-color;
@@ -82,16 +82,23 @@
}
}
-.md-header-toolbar {
- margin-left: auto;
+.nav-links {
+ li.md-header-toolbar {
+ margin-left: auto;
+ display: none;
- @media(max-width: $screen-xs-max) {
- flex: none;
- display: flex;
- justify-content: center;
- width: 100%;
- padding-top: $gl-padding-top;
- padding-bottom: $gl-padding-top;
+ &.active {
+ display: block;
+
+ @media (max-width: $screen-xs-max) {
+ flex: none;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding-top: $gl-padding-top;
+ padding-bottom: $gl-padding-top;
+ }
+ }
}
}
@@ -175,7 +182,7 @@
margin-left: $gl-padding;
margin-right: -5px;
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
margin-left: 0;
margin-right: 0;
}
@@ -185,6 +192,17 @@
overflow-y: auto;
overflow-x: hidden;
+ .name,
+ small.aliases,
+ small.params {
+ float: left;
+ }
+
+ small.aliases,
+ small.params {
+ padding: 2px 5px;
+ }
+
small.description {
float: right;
padding: 3px 5px;
@@ -202,6 +220,7 @@
}
ul > li {
+ @include clearfix;
white-space: nowrap;
}
@@ -239,7 +258,7 @@
}
}
-@media(max-width: $screen-xs-max) {
+@media (max-width: $screen-xs-max) {
.atwho-view-ul {
width: 350px;
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 600a1f53b58..a12f28efce6 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -111,21 +111,4 @@
aside:not(.right-sidebar) {
display: none;
}
-
- .show-aside {
- display: block !important;
- }
-}
-
-.show-aside {
- display: none;
- position: fixed;
- right: 0;
- top: 30%;
- padding: 5px 15px;
- background: $show-aside-bg;
- font-size: 20px;
- color: $show-aside-color;
- z-index: 100;
- box-shadow: 0 1px 2px $show-aside-shadow;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 5c9838c1029..1be66d0ab21 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -44,7 +44,21 @@ body.modal-open {
}
}
-.modal.popup-dialog {
- display: block;
+.modal {
+ background-color: $black-transparent;
+ z-index: 2100;
+
+ @media (min-width: $screen-md-min) {
+ .modal-dialog {
+ margin: 30px auto;
+ }
+ }
}
+.recaptcha-modal .recaptcha-form {
+ display: inline-block;
+
+ .recaptcha {
+ margin: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
index 8498b37abe4..5f67126bafa 100644
--- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -86,8 +86,6 @@
}
.nav-controls {
- @include new-style-dropdown;
-
display: inline-block;
float: right;
text-align: right;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 792981fdc48..d61809cb0a4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -50,6 +50,11 @@
&:not(.disabled) {
cursor: pointer;
}
+
+ svg {
+ width: $gl-padding;
+ height: $gl-padding;
+ }
}
}
@@ -85,11 +90,6 @@
.right-sidebar {
border-left: 1px solid $border-color;
height: calc(100% - #{$header-height});
-
- &.affix {
- position: fixed;
- top: $header-height;
- }
}
.with-performance-bar .right-sidebar.affix {
@@ -139,10 +139,6 @@
}
}
-.issuable-sidebar {
- @include new-style-dropdown;
-}
-
.pikaday-container {
.pika-single {
margin-top: 2px;
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 71765da3908..0cd83df218f 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -27,7 +27,7 @@
border: 0;
outline: 0;
display: block;
- width: 100px;
+ width: 50px;
height: 24px;
cursor: pointer;
user-select: none;
@@ -42,31 +42,31 @@
background: none;
}
- &::before {
- color: $feature-toggle-text-color;
- font-size: 12px;
- line-height: 24px;
- position: absolute;
- top: 0;
- left: 25px;
- right: 5px;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- animation: animate-disabled .2s ease-in;
- content: attr(data-disabled-text);
- }
-
- &::after {
+ .toggle-icon {
position: relative;
display: block;
- content: "";
- width: 22px;
- height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
+
+ &,
+ .toggle-icon-svg {
+ width: 18px;
+ height: 18px;
+ }
+
+ .toggle-icon-svg {
+ fill: $feature-toggle-color-disabled;
+ }
+
+ .toggle-status-checked {
+ display: none;
+ }
+
+ .toggle-status-unchecked {
+ display: inline;
+ }
}
.loading-icon {
@@ -77,11 +77,10 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-
}
&.is-loading {
- &::before {
+ .toggle-icon {
display: none;
}
@@ -100,15 +99,20 @@
&.is-checked {
background: $feature-toggle-color-enabled;
- &::before {
- left: 5px;
- right: 25px;
- animation: animate-enabled .2s ease-in;
- content: attr(data-enabled-text);
- }
+ .toggle-icon {
+ left: calc(100% - 18px);
- &::after {
- left: calc(100% - 22px);
+ .toggle-icon-svg {
+ fill: $feature-toggle-color-enabled;
+ }
+
+ .toggle-status-checked {
+ display: inline;
+ }
+
+ .toggle-status-unchecked {
+ display: none;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 0817cce114c..d0999e60e65 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -178,6 +178,10 @@
font-weight: inherit;
}
+ dd {
+ margin-left: $gl-padding;
+ }
+
ul,
ol {
padding: 0;
@@ -343,8 +347,6 @@ a > code {
@extend .ref-name;
}
-@include new-style-dropdown('.git-revision-dropdown');
-
/**
* Apply Markdown typography
*
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4f99c27eff1..f7853909f56 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -5,10 +5,9 @@ $grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
-$sidebar-transition-duration: .15s;
+$sidebar-transition-duration: .3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
-$right-sidebar-transition-duration: .3s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
@@ -220,6 +219,7 @@ $gl-input-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
+$gl-bar-padding: 3px;
/*
* Misc
@@ -246,9 +246,6 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
-$show-aside-bg: #eee;
-$show-aside-color: #777;
-$show-aside-shadow: #ddd;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
@@ -722,7 +719,7 @@ $issuable-warning-icon-margin: 4px;
Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
-$image-comment-cursor-top-offset: 30;
+$image-comment-cursor-top-offset: 12;
/*
Popup
@@ -730,3 +727,8 @@ Popup
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
+
+/*
+Multi file editor
+*/
+$border-color-settings: #e1e1e1;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 735fc4babd7..2f3a80daa90 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -16,6 +16,10 @@
.commit-sha,
.commit-info {
margin-left: 4px;
+
+ .fork-svg {
+ margin-right: 4px;
+ }
}
.ref-name {
@@ -79,7 +83,7 @@
}
.limit-icon {
- margin: 0 8px;
+ margin: 0 4px;
}
.limit-message {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 3683afa07de..2803144ef1d 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -57,7 +57,7 @@
position: relative;
@media (min-width: $screen-sm-min) {
- transition: width $right-sidebar-transition-duration;
+ transition: width $sidebar-transition-duration;
width: 100%;
&.is-compact {
@@ -415,7 +415,7 @@
margin: 5px;
}
-.page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar {
+.page-with-contextual-sidebar.layout-page .issue-boards-sidebar {
.issuable-sidebar-header {
position: relative;
}
@@ -453,8 +453,8 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
- transition: width $right-sidebar-transition-duration,
- padding $right-sidebar-transition-duration;
+ transition: width $sidebar-transition-duration,
+ padding $sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index f139f4ab650..98d460339cd 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -323,8 +323,6 @@
}
.build-dropdown {
- @include new-style-dropdown;
-
margin: $gl-padding 0;
padding: 0;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index c303f016ff9..7b8ee026357 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -6,15 +6,13 @@
.cluster-applications-table {
// Wait for the Vue to kick-in and render the applications block
- min-height: 302px;
+ min-height: 400px;
}
.clusters-dropdown-menu {
max-width: 100%;
}
-@include new-style-dropdown('.clusters-dropdown ');
-
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 292e0ad394b..3b35beb7695 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -1,6 +1,4 @@
#cycle-analytics {
- @include new-style-dropdown;
-
max-width: 1000px;
margin: 24px auto 0;
position: relative;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 52e4d904b9b..2f2c04206e2 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -32,8 +32,6 @@
}
.detail-page-header-actions {
- @include new-style-dropdown;
-
align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 848d7f144dc..60b07537799 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -581,8 +581,6 @@
}
.commit-stat-summary {
- @include new-style-dropdown;
-
@media (min-width: $screen-sm-min) {
margin-left: -$gl-padding;
padding-left: $gl-padding;
@@ -732,18 +730,18 @@
.frame.click-to-comment {
position: relative;
- cursor: image-url('icon_image_comment.svg')
+ cursor: image-url('illustrations/image_comment_light_cursor.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor
- cursor: -webkit-image-set(image-url('icon_image_comment.svg') 1x, image-url('icon_image_comment@2x.svg') 2x)
+ 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;
padding: 0;
width: (2px * $image-comment-cursor-left-offset);
- height: (1px * $image-comment-cursor-top-offset);
+ height: (2px * $image-comment-cursor-top-offset);
// center the indicator to match the top left click region
margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
@@ -778,15 +776,20 @@
.frame .badge,
.frame .image-comment-badge {
// Center align badges on the frame
- transform: translate3d(-50%, -50%, 0);
+ transform: translate(-50%, -50%);
}
.image-comment-badge {
- @include btn-comment-icon;
position: absolute;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ background: none;
+ border: 0;
- &.inverted {
- border-color: $white-light;
+ > svg {
+ width: 100%;
+ height: 100%;
}
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index c586dab4cf2..8ecda50602d 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -204,8 +204,6 @@
.gitlab-ci-yml-selector,
.dockerfile-selector,
.template-type-selector {
- @include new-style-dropdown;
-
display: inline-block;
vertical-align: top;
font-family: $regular_font;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index b0795353ec1..794bc668562 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -12,8 +12,6 @@
.environments-container {
.ci-table {
- @include new-style-dropdown;
-
.deployment-column {
> span {
word-break: break-all;
@@ -201,8 +199,9 @@
stroke-width: 1;
}
-.deploy-info-text {
- dominant-baseline: text-before-edge;
+.divider-line {
+ stroke-width: 1;
+ stroke: $gray-darkest;
}
.prometheus-state {
@@ -249,6 +248,73 @@
}
}
+.prometheus-graph-cursor {
+ position: absolute;
+ background: $theme-gray-600;
+ width: 1px;
+}
+
+.prometheus-graph-flag {
+ display: block;
+ min-width: 160px;
+
+ h5 {
+ padding: 0;
+ margin: 0;
+ font-size: 14px;
+ line-height: 1.2;
+ }
+
+ table {
+ border-collapse: collapse;
+ padding: 0;
+ margin: 0;
+ }
+
+ td {
+ vertical-align: middle;
+
+ + td {
+ padding-left: 5px;
+ vertical-align: top;
+ }
+ }
+
+ .deploy-meta-content {
+ border-bottom: 1px solid $white-dark;
+
+ svg {
+ height: 15px;
+ vertical-align: bottom;
+ }
+ }
+
+ &.popover {
+ &.left {
+ left: auto;
+ right: 0;
+ margin-right: 10px;
+ }
+
+ &.right {
+ left: 0;
+ right: auto;
+ margin-left: 10px;
+ }
+
+ > .arrow {
+ top: 40px;
+ }
+
+ > .popover-title,
+ > .popover-content {
+ padding: 5px 8px;
+ font-size: 12px;
+ white-space: nowrap;
+ }
+ }
+}
+
.prometheus-svg-container {
position: relative;
height: 0;
@@ -312,6 +378,20 @@
stroke: $gray-darker;
}
+ .deploy-info-text {
+ dominant-baseline: text-before-edge;
+ font-size: 12px;
+ }
+
+ .deploy-info-text-link {
+ font-family: $monospace_font;
+ fill: $gl-link-color;
+
+ &:hover {
+ fill: $gl-link-hover-color;
+ }
+ }
+
@media (max-width: $screen-sm-max) {
.label-axis-text,
.text-metric-usage,
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index eea8b7dd193..da096354b5a 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -159,7 +159,6 @@
}
}
-
/*
* Last push widget
*/
@@ -182,6 +181,12 @@
.event-item {
padding-left: 0;
+ &.event-inline {
+ .event-title {
+ line-height: 20px;
+ }
+ }
+
.event-title {
white-space: normal;
overflow: visible;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 11ee1232bfe..e1637618ab2 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -122,11 +122,11 @@
}
.right-sidebar {
- position: absolute;
+ position: fixed;
top: $header-height;
bottom: 0;
right: 0;
- transition: width $right-sidebar-transition-duration;
+ transition: width $sidebar-transition-duration;
background: $gray-light;
z-index: 200;
overflow: hidden;
@@ -470,7 +470,8 @@
}
}
- .milestone-title span {
+ .milestone-title span,
+ .collapse-truncated-title {
@include str-truncated(100%);
display: block;
margin: 0 4px;
@@ -487,12 +488,6 @@
}
}
- .dropdown-content {
- a:hover {
- color: inherit;
- }
- }
-
.dropdown-menu-toggle {
width: 100%;
padding-top: 6px;
@@ -507,14 +502,10 @@
top: $header-height + $performance-bar-height;
.issuable-sidebar {
- height: calc(100% - #{$header-height} - #{$performance-bar-height});
+ height: calc(100% - #{$performance-bar-height});
}
}
-.sidebar-move-issue-dropdown {
- @include new-style-dropdown;
-}
-
.sidebar-move-issue-confirmation-button {
width: 100%;
@@ -619,11 +610,19 @@
}
.issuable-status-box {
- float: none;
- display: inline-block;
+ align-self: stretch;
+ display: flex;
+ justify-content: center;
+ align-items: center;
margin-top: 0;
- height: auto;
- align-self: center;
+ padding-left: 9px;
+ padding-right: 9px;
+
+ @media (min-width: $screen-sm-min) {
+ display: inline-block;
+ height: auto;
+ align-self: center;
+ }
}
.issuable-gutter-toggle {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index af1df8b8802..c48e58af691 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -142,8 +142,6 @@ ul.related-merge-requests > li {
}
.issue-form {
- @include new-style-dropdown;
-
.select2-container {
width: 250px !important;
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 92abe82df4c..e8cd8a4905c 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -116,8 +116,6 @@
}
.manage-labels-list {
- @include new-style-dropdown;
-
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
cursor: move;
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 18c48405ecd..3422829de58 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -58,8 +58,6 @@
}
.member-form-control {
- @include new-style-dropdown;
-
@media (max-width: $screen-xs-max) {
padding-bottom: 5px;
margin-left: 0;
@@ -73,8 +71,6 @@
}
.member-search-form {
- @include new-style-dropdown;
-
position: relative;
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 2afb17334e3..f887a11004f 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -485,8 +485,6 @@
}
.mr-source-target {
- @include new-style-dropdown;
-
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@@ -608,8 +606,6 @@
}
.mr-version-controls {
- @include new-style-dropdown;
-
position: relative;
background: $gray-light;
color: $gl-text-color;
@@ -728,6 +724,6 @@
}
}
-.merge-request-form {
- @include new-style-dropdown;
+.fork-sprite {
+ margin-right: -5px;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index ebb5d121433..6d4ccd53e12 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -23,8 +23,6 @@
.new-note,
.note-edit-form {
.note-form-actions {
- @include new-style-dropdown;
-
position: relative;
margin: $gl-padding 0 0;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 4d5613c292b..26e6e8688b6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -490,8 +490,6 @@ ul.notes {
}
.note-actions {
- @include new-style-dropdown;
-
align-self: flex-start;
flex-shrink: 0;
display: inline-flex;
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index c28b1e68008..bdf07a99daf 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -14,7 +14,3 @@
font-size: 18px;
}
}
-
-.notification-form {
- @include new-style-dropdown;
-}
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 7e2297c283f..b698a4f9afa 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -39,6 +39,10 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+
+ svg {
+ vertical-align: middle;
+ }
}
.next-run-cell {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index cb24274c612..05c1033c5f7 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -143,6 +143,12 @@
fill: $gl-text-color-secondary;
}
+ .sprite {
+ width: 12px;
+ height: 12px;
+ fill: $gl-text-color;
+ }
+
.fa {
font-size: 12px;
color: $gl-text-color;
@@ -286,8 +292,6 @@
// Pipeline visualization
.pipeline-actions {
- @include new-style-dropdown;
-
border-bottom: 0;
}
@@ -703,9 +707,6 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
-@include new-style-dropdown('.big-pipeline-graph-dropdown-menu');
-@include new-style-dropdown('.mini-pipeline-graph-dropdown-menu');
-
// dropdown content for big and mini pipeline
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
@@ -804,7 +805,6 @@ button.mini-pipeline-graph-dropdown-toggle {
font-weight: normal;
line-height: $line-height-base;
white-space: nowrap;
- border-radius: 3px;
.ci-job-name-component {
align-items: center;
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index c197494b152..68d40b56133 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -20,6 +20,22 @@
}
}
+.multi-file-editor-options {
+ label {
+ margin-right: 20px;
+ text-align: center;
+ }
+
+ .preview {
+ font-size: 0;
+
+ img {
+ border: 1px solid $border-color-settings;
+ border-radius: 4px;
+ }
+ }
+}
+
.application-theme {
label {
margin-right: 20px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 674588752d2..6f4c678c4b8 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -323,8 +323,6 @@
}
.project-repo-buttons {
- @include new-style-dropdown;
-
.project-action-button .dropdown-menu {
max-height: 250px;
overflow-y: auto;
@@ -898,8 +896,6 @@ pre.light-well {
.new-protected-branch,
.new-protected-tag {
- @include new-style-dropdown;
-
label {
margin-top: 6px;
font-weight: $gl-font-weight-normal;
@@ -919,8 +915,6 @@ pre.light-well {
.protected-branches-list,
.protected-tags-list {
- @include new-style-dropdown;
-
margin-bottom: 30px;
.settings-message {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 402412eae71..6cb32408a48 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1,16 +1,3 @@
-.modal.popup-dialog {
- display: block;
- background-color: $black-transparent;
- z-index: 2100;
-
- @media (min-width: $screen-md-min) {
- .modal-dialog {
- width: 600px;
- margin: 30px auto;
- }
- }
-}
-
.project-refs-form,
.project-refs-target-form {
display: inline-block;
@@ -35,9 +22,10 @@
}
}
-.multi-file {
+.ide-view {
display: flex;
- height: calc(100vh - 145px);
+ height: calc(100vh - #{$header-height});
+ color: $almost-black;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
@@ -50,10 +38,41 @@
.ide-file-list {
flex: 1;
- overflow: scroll;
.file {
cursor: pointer;
+
+ &.file-open {
+ background: $white-normal;
+ }
+
+ .repo-file-name {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .unsaved-icon {
+ color: $indigo-700;
+ float: right;
+ font-size: smaller;
+ line-height: 20px;
+ }
+
+ .repo-new-btn {
+ display: none;
+ margin-top: -4px;
+ margin-bottom: -4px;
+ }
+
+ &:hover {
+ .repo-new-btn {
+ display: block;
+ }
+
+ .unsaved-icon {
+ display: none;
+ }
+ }
}
a {
@@ -68,17 +87,36 @@
.multi-file-table-name,
.multi-file-table-col-commit-message {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ overflow: visible;
max-width: 0;
+ padding: 6px 12px;
}
-.multi-file-table-name {
+.multi-file-loading-container {
+ margin-top: 10px;
+ padding: 10px;
+
+ .animation-container {
+ background: $gray-light;
+
+ div {
+ background: $gray-light;
+ }
+ }
+}
+
+table.table tr td.multi-file-table-name {
width: 350px;
+ padding: 6px 12px;
+
+ svg {
+ vertical-align: middle;
+ margin-right: 2px;
+ }
}
.multi-file-table-col-commit-message {
+ white-space: nowrap;
width: 50%;
}
@@ -92,7 +130,7 @@
.multi-file-tabs {
display: flex;
- overflow: scroll;
+ overflow-x: auto;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
@@ -109,6 +147,10 @@
border-bottom: 1px solid $white-dark;
cursor: pointer;
+ svg {
+ vertical-align: middle;
+ }
+
&.active {
background-color: $white-light;
border-bottom-color: $white-light;
@@ -141,9 +183,38 @@
height: 0;
}
+.blob-editor-container {
+ flex: 1;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .vertical-center {
+ min-height: auto;
+ }
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+}
+
.multi-file-editor-btn-group {
- padding: $grid-size;
+ padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ background: $white-light;
+}
+
+.ide-status-bar {
+ padding: $gl-bar-padding $gl-padding;
+ background: $white-light;
+ display: flex;
+ justify-content: space-between;
+
+ svg {
+ vertical-align: middle;
+ }
}
// Not great, but this is to deal with our current output
@@ -151,10 +222,6 @@
height: 100%;
overflow: scroll;
- .blob-viewer {
- height: 100%;
- }
-
.file-content.code {
display: flex;
@@ -175,18 +242,102 @@
}
}
+.file-content.blob-no-preview {
+ a {
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
.multi-file-commit-panel {
display: flex;
+ position: relative;
flex-direction: column;
height: 100%;
width: 290px;
- padding: $gl-padding;
+ padding: 0;
background-color: $gray-light;
- border-left: 1px solid $white-dark;
+ padding-right: 3px;
+
+ .projects-sidebar {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner-scroll {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: auto;
+ }
&.is-collapsed {
width: 60px;
- padding: 0;
+
+ .multi-file-commit-list {
+ padding-top: $gl-padding;
+ overflow: hidden;
+ }
+
+ .multi-file-context-bar-icon {
+ align-items: center;
+
+ svg {
+ float: none;
+ margin: 0;
+ }
+ }
+ }
+
+ .branch-container {
+ border-left: 4px solid $indigo-700;
+ margin-bottom: $gl-bar-padding;
+ }
+
+ .branch-header {
+ background: $white-dark;
+ display: flex;
+ }
+
+ .branch-header-title {
+ flex: 1;
+ padding: $grid-size $gl-padding;
+ color: $indigo-700;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+
+ .branch-header-btns {
+ padding: $gl-vert-padding $gl-padding;
+ }
+
+ .left-collapse-btn {
+ display: none;
+ background: $gray-light;
+ text-align: left;
+ border-top: 1px solid $white-dark;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+}
+
+.multi-file-context-bar-icon {
+ padding: 10px;
+
+ svg {
+ margin-right: 10px;
+ float: left;
}
}
@@ -199,9 +350,9 @@
.multi-file-commit-panel-header {
display: flex;
align-items: center;
- padding: 0 0 12px;
margin-bottom: 12px;
border-bottom: 1px solid $white-dark;
+ padding: $gl-btn-padding 0;
&.is-collapsed {
border-bottom: 1px solid $white-dark;
@@ -210,23 +361,33 @@
margin-left: auto;
margin-right: auto;
}
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
}
}
-.multi-file-commit-panel-collapse-btn {
- padding-top: 0;
- padding-bottom: 0;
- margin-left: auto;
- font-size: 20px;
+.multi-file-commit-panel-header-title {
+ display: flex;
+ flex: 1;
+ padding: $gl-btn-padding;
- &.is-collapsed {
- margin-right: auto;
+ svg {
+ margin-right: $gl-btn-padding;
}
}
+.multi-file-commit-panel-collapse-btn {
+ border-left: 1px solid $white-dark;
+}
+
.multi-file-commit-list {
flex: 1;
- overflow: scroll;
+ overflow: auto;
+ padding: $gl-padding;
}
.multi-file-commit-list-item {
@@ -257,7 +418,7 @@
}
.multi-file-commit-form {
- padding-top: 12px;
+ padding: $gl-padding;
border-top: 1px solid $white-dark;
}
@@ -308,3 +469,67 @@
}
}
}
+
+.ide-loading {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-empty-state {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.repo-new-btn {
+ .dropdown-toggle svg {
+ margin-top: -2px;
+ margin-bottom: 2px;
+ }
+
+ .dropdown-menu {
+ left: auto;
+ right: 0;
+
+ label {
+ font-weight: $gl-font-weight-normal;
+ padding: 5px 8px;
+ margin-bottom: 0;
+ }
+ }
+}
+
+.ide-flash-container.flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+}
+
+.with-performance-bar {
+ .ide-flash-container.flash-container {
+ margin-top: $header-height + $performance-bar-height;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $performance-bar-height});
+ }
+}
+
+
+.dragHandle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background-color: $white-dark;
+
+ &.dragright {
+ right: 0;
+ }
+
+ &.dragleft {
+ left: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index fe455a04960..c9363188505 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -108,19 +108,7 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning
.dropdown-menu {
- transition-property: opacity, transform;
- transition-duration: 250ms, 250ms;
- transition-delay: 0ms, 25ms;
- transition-timing-function: $dropdown-animation-timing;
- transform: translateY(0);
- opacity: 0;
- display: block;
left: -5px;
- padding: 0;
-
- ul {
- padding: 10px 0;
- }
}
.dropdown-content {
@@ -157,13 +145,6 @@ input[type="checkbox"]:hover {
background-color: $nav-badge-bg;
border-color: $border-color;
}
-
- .dropdown-menu {
- transition-duration: 100ms, 75ms;
- transition-delay: 75ms, 100ms;
- transform: translateY(7px);
- opacity: 1;
- }
}
&.has-value {
@@ -185,8 +166,6 @@ input[type="checkbox"]:hover {
}
.search-holder {
- @include new-style-dropdown;
-
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 5d630c7d61e..6353482ede7 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -268,3 +268,7 @@
margin: 0 0 5px 17px;
}
}
+
+.deprecated-service {
+ cursor: default;
+}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index cede147d559..8e2c42c1bd3 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -10,7 +10,6 @@
}
.axis {
- fill: $stat-graph-axis-fill;
font-size: 10px;
}
@@ -54,9 +53,7 @@
}
.selection rect {
- fill: $stat-graph-selection-fill;
fill-opacity: 0.1;
- stroke: $stat-graph-selection-stroke;
stroke-width: 1px;
stroke-opacity: 0.4;
shape-rendering: crispedges;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 2139a029fc7..a79772ea37b 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -265,7 +265,3 @@
font-weight: $gl-font-weight-bold;
}
}
-
-.todos-filters {
- @include new-style-dropdown;
-}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 5d14323e4bc..e0ee7e9aa3d 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -1,6 +1,4 @@
.tree-holder {
- @include new-style-dropdown;
-
.nav-block {
margin: 10px 0;
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 2ce26de1768..a94726887d9 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -1,4 +1,6 @@
class Admin::GroupsController < Admin::ApplicationController
+ include MembersPresentation
+
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
@@ -10,8 +12,10 @@ class Admin::GroupsController < Admin::ApplicationController
def show
@group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id])
- @members = @group.members.order("access_level DESC").page(params[:members_page])
- @requesters = AccessRequestsFinder.new(@group).execute(current_user)
+ @members = present_members(
+ @group.members.order("access_level DESC").page(params[:members_page]))
+ @requesters = present_members(
+ AccessRequestsFinder.new(@group).execute(current_user))
@projects = @group.projects.with_statistics.page(params[:projects_page])
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 50cf2643390..3afe66c3566 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -1,4 +1,6 @@
class Admin::ProjectsController < Admin::ApplicationController
+ include MembersPresentation
+
before_action :project, only: [:show, :transfer, :repository_check]
before_action :group, only: [:show, :transfer]
@@ -19,11 +21,14 @@ class Admin::ProjectsController < Admin::ApplicationController
def show
if @group
- @group_members = @group.members.order("access_level DESC").page(params[:group_members_page])
+ @group_members = present_members(
+ @group.members.order("access_level DESC").page(params[:group_members_page]))
end
- @project_members = @project.members.page(params[:project_members_page])
- @requesters = AccessRequestsFinder.new(@project).execute(current_user)
+ @project_members = present_members(
+ @project.members.page(params[:project_members_page]))
+ @requesters = present_members(
+ AccessRequestsFinder.new(@project).execute(current_user))
end
def transfer
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index cde1e284d2d..86bade49ec9 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -8,12 +8,12 @@ class AutocompleteController < ApplicationController
def users
@users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
- render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
+ render json: UserSerializer.new.represent(@users)
end
def user
@user = User.find(params[:id])
- render json: @user, only: [:name, :username, :id], methods: [:avatar_url]
+ render json: UserSerializer.new.represent(@user)
end
def projects
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index 2c9c095a5d7..a145049dc7d 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -24,11 +24,11 @@ module BoardsResponses
end
def respond_with_boards
- respond_with(@boards)
+ respond_with(@boards) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def respond_with_board
- respond_with(@board)
+ respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def respond_with(resource)
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 782f0be9c4a..6f4fdcdaa4f 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -1,6 +1,8 @@
module CreatesCommit
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project)
@project_to_commit_into = @project
@@ -45,6 +47,7 @@ module CreatesCommit
end
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def authorize_edit_tree!
return if can_collaborate_with_project?
@@ -77,6 +80,7 @@ module CreatesCommit
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def new_merge_request_path
project_new_merge_request_path(
@project_to_commit_into,
@@ -88,20 +92,28 @@ module CreatesCommit
}
)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def existing_merge_request_path
- project_merge_request_path(@project, @merge_request)
+ project_merge_request_path(@project, @merge_request) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def merge_request_exists?
- return @merge_request if defined?(@merge_request)
-
- @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
- .find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch)
+ strong_memoize(:merge_request) do
+ MergeRequestsFinder.new(current_user, project_id: @project.id)
+ .execute
+ .opened
+ .find_by(
+ source_project_id: @project_to_commit_into,
+ source_branch: @branch_name,
+ target_branch: @start_branch)
+ end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def different_project?
- @project_to_commit_into != @project
+ @project_to_commit_into != @project # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def create_merge_request?
@@ -109,6 +121,6 @@ module CreatesCommit
# as the target branch in the same project,
# we don't want to create a merge request.
params[:create_merge_request].present? &&
- (different_project? || @start_branch != @branch_name)
+ (different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 9d4f97aa443..b10147835f3 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -1,4 +1,5 @@
module GroupTree
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_group_tree(groups)
@groups = if params[:filter].present?
Gitlab::GroupHierarchy.new(groups.search(params[:filter]))
@@ -20,5 +21,6 @@ module GroupTree
render json: serializer.represent(@groups)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 744e448e8df..74a4f437dc8 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -17,15 +17,15 @@ module IssuableActions
end
def update
- @issuable = update_service.execute(issuable)
+ @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
respond_to do |format|
format.html do
- recaptcha_check_with_fallback { render :edit }
+ recaptcha_check_if_spammable { render :edit }
end
format.json do
- render_entity_json
+ recaptcha_check_if_spammable(false) { render_entity_json }
end
end
@@ -55,7 +55,6 @@ module IssuableActions
def destroy
Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
- TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
@@ -80,10 +79,16 @@ module IssuableActions
private
+ def recaptcha_check_if_spammable(should_redirect = true, &block)
+ return yield unless issuable.is_a? Spammable
+
+ recaptcha_check_with_fallback(should_redirect, &block)
+ end
+
def render_conflict_response
respond_to do |format|
format.html do
- @conflict = true
+ @conflict = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
render :edit
end
@@ -98,7 +103,7 @@ module IssuableActions
end
def labels
- @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def authorize_destroy_issuable!
@@ -108,7 +113,7 @@ module IssuableActions
end
def authorize_admin_issuable!
- unless can?(current_user, :"admin_#{resource_name}", @project)
+ unless can?(current_user, :"admin_#{resource_name}", @project) # rubocop:disable Gitlab/ModuleWithInstanceVariables
return access_denied!
end
end
@@ -142,6 +147,7 @@ module IssuableActions
@resource_name ||= controller_name.singularize
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_entity_json
if @issuable.valid?
render json: serializer.represent(@issuable)
@@ -149,6 +155,7 @@ module IssuableActions
render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def serializer
raise NotImplementedError
@@ -159,6 +166,6 @@ module IssuableActions
end
def parent
- @project || @group
+ @project || @group # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index f3c9251225f..b25e753a5ad 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -2,6 +2,7 @@ module IssuableCollections
extend ActiveSupport::Concern
include SortingHelper
include Gitlab::IssuableMetadata
+ include Gitlab::Utils::StrongMemoize
included do
helper_method :finder
@@ -9,6 +10,7 @@ module IssuableCollections
private
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_issuables_index
@issuables = issuables_collection
@issuables = @issuables.page(params[:page])
@@ -33,6 +35,7 @@ module IssuableCollections
@users.push(author) if author
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def issuables_collection
finder.execute.preload(preload_for_collection)
@@ -41,7 +44,7 @@ module IssuableCollections
def redirect_out_of_range(total_pages)
return false if total_pages.zero?
- out_of_range = @issuables.current_page > total_pages
+ out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
@@ -51,7 +54,7 @@ module IssuableCollections
end
def issuable_page_count
- page_count_for_relation(@issuables, finder.row_count)
+ page_count_for_relation(@issuables, finder.row_count) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def page_count_for_relation(relation, row_count)
@@ -66,6 +69,7 @@ module IssuableCollections
finder_class.new(current_user, filter_params)
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def filter_params
set_sort_order_from_cookie
set_default_state
@@ -90,6 +94,7 @@ module IssuableCollections
@filter_params.permit(IssuableFinder::VALID_PARAMS)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def set_default_state
params[:state] = 'opened' if params[:state].blank?
@@ -129,9 +134,9 @@ module IssuableCollections
end
def finder
- return @finder if defined?(@finder)
-
- @finder = issuable_finder_for(@finder_type)
+ strong_memoize(:finder) do
+ issuable_finder_for(@finder_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
end
def collection_type
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index ad594903331..3ba1235cee0 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -2,10 +2,9 @@ module IssuesAction
extend ActiveSupport::Concern
include IssuableCollections
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues
@finder_type = IssuesFinder
- @label = finder.labels.first
-
@issues = issuables_collection
.non_archived
.page(params[:page])
@@ -17,4 +16,5 @@ module IssuesAction
format.atom { render layout: 'xml.atom' }
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb
new file mode 100644
index 00000000000..c0622516fd3
--- /dev/null
+++ b/app/controllers/concerns/members_presentation.rb
@@ -0,0 +1,11 @@
+module MembersPresentation
+ extend ActiveSupport::Concern
+
+ def present_members(members)
+ Gitlab::View::Presenter::Factory.new(
+ members,
+ current_user: current_user,
+ presenter_class: MembersPresenter
+ ).fabricate!
+ end
+end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 8b569a01afd..a9cc13038bf 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -2,14 +2,15 @@ module MergeRequestsAction
extend ActiveSupport::Concern
include IssuableCollections
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def merge_requests
@finder_type = MergeRequestsFinder
- @label = finder.labels.first
@merge_requests = issuables_collection.page(params[:page])
@issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 081f3336780..d92cf8b4894 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -6,7 +6,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
- merge_requests: @milestone.sorted_merge_requests,
+ merge_requests: @milestone.sorted_merge_requests, # rubocop:disable Gitlab/ModuleWithInstanceVariables
show_project_name: true
})
end
@@ -18,7 +18,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
- users: @milestone.participants
+ users: @milestone.participants # rubocop:disable Gitlab/ModuleWithInstanceVariables
})
end
end
@@ -29,7 +29,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: @milestone.labels
+ labels: @milestone.labels # rubocop:disable Gitlab/ModuleWithInstanceVariables
})
end
end
@@ -43,6 +43,7 @@ module MilestoneActions
}
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def milestone_redirect_path
if @project
project_milestone_path(@project, @milestone)
@@ -52,4 +53,5 @@ module MilestoneActions
dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index be2e1b47feb..e82a5650935 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -1,5 +1,6 @@
module NotesActions
include RendersNotes
+ include Gitlab::Utils::StrongMemoize
extend ActiveSupport::Concern
included do
@@ -30,6 +31,7 @@ module NotesActions
render json: notes_json
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
@@ -47,7 +49,9 @@ module NotesActions
format.html { redirect_back_or_default }
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
@@ -60,6 +64,7 @@ module NotesActions
format.html { redirect_back_or_default }
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def destroy
if note.editable?
@@ -138,7 +143,7 @@ module NotesActions
end
else
template = "discussions/_diff_discussion"
- @fresh_discussion = true
+ @fresh_discussion = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
locals = { discussions: [discussion], on_image: on_image }
end
@@ -191,7 +196,7 @@ module NotesActions
end
def noteable
- @noteable ||= notes_finder.target || @note&.noteable
+ @noteable ||= notes_finder.target || @note&.noteable # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def require_noteable!
@@ -211,20 +216,21 @@ module NotesActions
end
def note_project
- return @note_project if defined?(@note_project)
- return nil unless project
+ strong_memoize(:note_project) do
+ return nil unless project
- note_project_id = params[:note_project_id]
+ note_project_id = params[:note_project_id]
- @note_project =
- if note_project_id.present?
- Project.find(note_project_id)
- else
- project
- end
+ the_project =
+ if note_project_id.present?
+ Project.find(note_project_id)
+ else
+ project
+ end
- return access_denied! unless can?(current_user, :create_note, @note_project)
+ return access_denied! unless can?(current_user, :create_note, the_project)
- @note_project
+ the_project
+ end
end
end
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
index 9849aa93fa6..f0a68f23566 100644
--- a/app/controllers/concerns/oauth_applications.rb
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -14,6 +14,6 @@ module OauthApplications
end
def load_scopes
- @scopes = Doorkeeper.configuration.scopes
+ @scopes ||= Doorkeeper.configuration.scopes
end
end
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index e9b9e9b38bc..90bb7a87b45 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -1,6 +1,7 @@
module PreviewMarkdown
extend ActiveSupport::Concern
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def preview_markdown
result = PreviewMarkdownService.new(@project, current_user, params).execute
@@ -20,4 +21,5 @@ module PreviewMarkdown
}
}
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
index bb2c1dfa00a..fb41dc1e8a8 100644
--- a/app/controllers/concerns/renders_commits.rb
+++ b/app/controllers/concerns/renders_commits.rb
@@ -1,6 +1,6 @@
module RendersCommits
def prepare_commits_for_rendering(commits)
- Banzai::CommitRenderer.render(commits, @project, current_user)
+ Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
commits
end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index 824ad06465c..e7ef297879f 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -1,4 +1,5 @@
module RendersNotes
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def prepare_notes_for_rendering(notes, noteable = nil)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
@@ -7,6 +8,7 @@ module RendersNotes
notes
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index be2e6c7f193..3d61458c064 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -66,7 +66,7 @@ module ServiceParams
FILTER_BLANK_PARAMS = [:password].freeze
def service_params
- dynamic_params = @service.event_channel_names + @service.event_names
+ dynamic_params = @service.event_channel_names + @service.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables
service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params)
if service_params[:service].is_a?(Hash)
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index ffea712a833..9095cc7f783 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -4,6 +4,7 @@ module SnippetsActions
def edit
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
@@ -14,6 +15,7 @@ module SnippetsActions
filename: @snippet.sanitized_file_name
)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index ada0dde87fb..922aa58a00f 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -2,6 +2,7 @@ module SpammableActions
extend ActiveSupport::Concern
include Recaptcha::Verify
+ include Gitlab::Utils::StrongMemoize
included do
before_action :authorize_submit_spammable!, only: :mark_as_spam
@@ -18,13 +19,13 @@ module SpammableActions
private
def ensure_spam_config_loaded!
- return @spam_config_loaded if defined?(@spam_config_loaded)
-
- @spam_config_loaded = Gitlab::Recaptcha.load_configurations!
+ strong_memoize(:spam_config_loaded) do
+ Gitlab::Recaptcha.load_configurations!
+ end
end
- def recaptcha_check_with_fallback(&fallback)
- if spammable.valid?
+ def recaptcha_check_with_fallback(should_redirect = true, &fallback)
+ if should_redirect && spammable.valid?
redirect_to spammable_path
elsif render_recaptcha?
ensure_spam_config_loaded!
@@ -33,7 +34,18 @@ module SpammableActions
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
- render :verify
+ respond_to do |format|
+ format.html do
+ render :verify
+ end
+
+ format.json do
+ locals = { spammable: spammable, script: false, has_submit: false }
+ recaptcha_html = render_to_string(partial: 'shared/recaptcha_form', formats: :html, locals: locals)
+
+ render json: { recaptcha_html: recaptcha_html }
+ end
+ end
else
yield
end
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
index 92cb534343e..776583579e8 100644
--- a/app/controllers/concerns/toggle_subscription_action.rb
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -12,7 +12,7 @@ module ToggleSubscriptionAction
private
def subscribable_project
- @project || raise(NotImplementedError)
+ @project ||= raise(NotImplementedError)
end
def subscribable_resource
diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb
index ed253042701..230bbe4b1aa 100644
--- a/app/controllers/concerns/with_performance_bar.rb
+++ b/app/controllers/concerns/with_performance_bar.rb
@@ -6,6 +6,7 @@ module WithPerformanceBar
end
def peek_enabled?
+ return true if Rails.env.development?
return false unless Gitlab::PerformanceBar.enabled?(current_user)
if RequestStore.active?
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 8fc234a62b1..21e77431176 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,5 +1,6 @@
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
+ include MembersPresentation
include SortingHelper
# Authorize
@@ -14,15 +15,17 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50)
- @members.includes(:user)
+ @members = present_members(@members.includes(:user))
- @requesters = AccessRequestsFinder.new(@group).execute(current_user)
+ @requesters = present_members(
+ AccessRequestsFinder.new(@group).execute(current_user))
@group_member = @group.group_members.new
end
def update
- @group_member = @group.group_members.find(params[:id])
+ @group_member = @group.members_and_requesters.find(params[:id])
+ .present(current_user: current_user)
return render_403 unless can?(current_user, :update_group_member, @group_member)
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
new file mode 100644
index 00000000000..1ff25a45398
--- /dev/null
+++ b/app/controllers/ide_controller.rb
@@ -0,0 +1,6 @@
+class IdeController < ApplicationController
+ layout 'nav_only'
+
+ def index
+ end
+end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 68a52f40342..57761bfbe26 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,6 +1,8 @@
class PasswordsController < Devise::PasswordsController
include Gitlab::CurrentSettings
+ skip_before_action :require_no_authentication, only: [:edit, :update]
+
before_action :resource_from_email, only: [:create]
before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 9e79852e378..6025a40348b 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController
def require_pages_enabled!
not_found unless @project.pages_available?
end
+
+ def check_issues_available!
+ return render_404 unless @project.feature_available?(:issues, current_user)
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 770381472c5..d838b8dc29e 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -5,9 +5,6 @@ class Projects::BlobController < Projects::ApplicationController
include RendersBlob
include ActionView::Helpers::SanitizeHelper
- # Raised when given an invalid file path
- InvalidPathError = Class.new(StandardError)
-
prepend_before_action :authenticate_user!, only: [:edit]
before_action :require_non_empty_project, except: [:new, :create]
@@ -61,7 +58,6 @@ class Projects::BlobController < Projects::ApplicationController
create_commit(Files::UpdateService, success_path: -> { after_edit_path },
failure_view: :edit,
failure_path: project_blob_path(@project, @id))
-
rescue Files::UpdateService::FileChangedError
@conflict = true
render :edit
@@ -132,7 +128,6 @@ class Projects::BlobController < Projects::ApplicationController
def assign_blob_vars
@id = params[:id]
@ref, @path = extract_ref(@id)
-
rescue InvalidPathError
render_404
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index e36105ddc11..949e54ff819 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -2,6 +2,7 @@ class Projects::BoardsController < Projects::ApplicationController
include BoardsResponses
include IssuableCollections
+ before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 56df9991fda..cabafe26357 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -46,14 +46,16 @@ class Projects::BranchesController < Projects::ApplicationController
result = CreateBranchService.new(project, current_user)
.execute(branch_name, ref)
- if params[:issue_iid]
+ success = (result[:status] == :success)
+
+ if params[:issue_iid] && success
issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid])
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
respond_to do |format|
format.html do
- if result[:status] == :success
+ if success
if redirect_to_autodeploy
redirect_to url_to_autodeploy_setup(project, branch_name),
notice: view_context.autodeploy_flash_notice(branch_name)
@@ -67,7 +69,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
format.json do
- if result[:status] == :success
+ if success
render json: { name: branch_name, url: project_tree_url(@project, branch_name) }
else
render json: result[:messsage], status: :unprocessable_entity
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
index b64f7a2a6bd..25608df0b9c 100644
--- a/app/controllers/projects/clusters/gcp_controller.rb
+++ b/app/controllers/projects/clusters/gcp_controller.rb
@@ -1,6 +1,7 @@
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]
before_action :authorize_create_cluster!, only: [:new, :create]
def login
@@ -22,15 +23,20 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end
def create
- @cluster = ::Clusters::CreateService
- .new(project, current_user, create_params)
- .execute(token_in_session)
+ case google_project_billing_status
+ when 'true'
+ @cluster = ::Clusters::CreateService
+ .new(project, current_user, create_params)
+ .execute(token_in_session)
- if @cluster.persisted?
- redirect_to project_cluster_path(project, @cluster)
+ return redirect_to project_cluster_path(project, @cluster) if @cluster.persisted?
+ when 'false'
+ flash[:error] = _('Please enable billing for one of your projects to be able to create a cluster.')
else
- render :new
+ flash[:error] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
end
+
+ render :new
end
private
@@ -39,6 +45,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
@@ -57,6 +64,17 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
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
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(CheckGcpProjectBillingWorker.redis_shared_state_key_for(token_in_session))
+ end
+ end
+
def token_in_session
@token_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb
index d7678512073..d0db64b2fa9 100644
--- a/app/controllers/projects/clusters/user_controller.rb
+++ b/app/controllers/projects/clusters/user_controller.rb
@@ -26,6 +26,7 @@ class Projects::Clusters::UserController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 0907daacbc3..1dc7f1b3a7f 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -8,11 +8,8 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000
def index
- @scope = params[:scope] || 'all'
- @clusters = ClustersFinder.new(project, current_user, @scope).execute.page(params[:page])
- @active_count = ClustersFinder.new(project, current_user, :active).execute.count
- @inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count
- @all_count = @active_count + @inactive_count
+ clusters = ClustersFinder.new(project, current_user, :all).execute
+ @clusters = clusters.page(params[:page]).per(20)
end
def new
@@ -90,6 +87,7 @@ class Projects::ClustersController < Projects::ApplicationController
if cluster.managed?
params.require(:cluster).permit(
:enabled,
+ :environment_scope,
platform_kubernetes_attributes: [
:namespace
]
@@ -98,6 +96,7 @@ class Projects::ClustersController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
platform_kubernetes_attributes: [
:api_url,
:token,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d7a3441a245..384f18b316c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -194,10 +194,6 @@ class Projects::IssuesController < Projects::ApplicationController
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
- def check_issues_available!
- return render_404 unless @project.feature_available?(:issues, current_user)
- end
-
def render_issue_json
if @issue.valid?
render json: serializer.represent(@issue)
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 1c4c09c772f..4865ec3dfe5 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -110,7 +110,7 @@ class Projects::JobsController < Projects::ApplicationController
def erase
if @build.erase(erased_by: current_user)
redirect_to project_job_path(project, @build),
- notice: "Build has been successfully erased!"
+ notice: "Job has been successfully erased!"
else
respond_422
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 1511fc08c89..dc524b790a0 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -9,7 +9,10 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
before_action :build_merge_request, except: [:create]
def new
- define_new_vars
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40934
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ define_new_vars
+ end
end
def create
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index e7b3b73024b..2e8a738b6d9 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,6 +10,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
+ before_action :check_user_can_push_to_source_branch!, only: [:rebase]
def index
@merge_requests = @issuables
@@ -131,7 +132,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
.new(project, current_user, wip_event: 'unwip')
.execute(@merge_request)
- render json: serializer.represent(@merge_request)
+ render json: serialize_widget(@merge_request)
end
def commit_change_content
@@ -147,7 +148,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
.new(@project, current_user)
.cancel(@merge_request)
- render json: serializer.represent(@merge_request)
+ render json: serialize_widget(@merge_request)
end
def merge
@@ -223,6 +224,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render json: environments
end
+ def rebase
+ RebaseWorker.perform_async(@merge_request.id, current_user.id)
+
+ render nothing: true, status: 200
+ end
+
protected
alias_method :subscribable_resource, :merge_request
@@ -304,6 +311,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
+ def serialize_widget(merge_request)
+ serializer.represent(merge_request, serializer: 'widget')
+ end
+
def serializer
MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
end
@@ -318,4 +329,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@finder_type = MergeRequestsFinder
super
end
+
+ def check_user_can_push_to_source_branch!
+ return access_denied! unless @merge_request.source_branch_exists?
+
+ access_check = ::Gitlab::UserAccess
+ .new(current_user, project: @merge_request.source_project)
+ .can_push_to_branch?(@merge_request.source_branch)
+
+ access_denied! unless access_check
+ end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 627cb2bd93c..5940fae8dd0 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -11,7 +11,7 @@ class Projects::NotesController < Projects::ApplicationController
# Controller actions are returned from AbstractController::Base and methods of parent classes are
# excluded in order to return only specific controller related methods.
# That is ok for the app (no :create method in ancestors)
- # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
+ # but fails for tests because there is a :create method on FactoryBot (one of the ancestors)
#
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
#
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index ec7c645df5a..b478e7b5e05 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -1,9 +1,11 @@
class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, except: [:index, :new, :create]
+ before_action :play_rate_limit, only: [:play]
+ before_action :authorize_play_pipeline_schedule!, only: [:play]
before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
- before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create]
+ before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
def index
@@ -40,6 +42,18 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
end
end
+ def play
+ job_id = RunPipelineScheduleWorker.perform_async(schedule.id, current_user.id)
+
+ if job_id
+ flash[:notice] = "Successfully scheduled a pipeline to run. Go to the <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details.".html_safe
+ else
+ flash[:alert] = 'Unable to schedule a pipeline to run immediately'
+ end
+
+ redirect_to pipeline_schedules_path(@project)
+ end
+
def take_ownership
if schedule.update(owner: current_user)
redirect_to pipeline_schedules_path(@project)
@@ -60,6 +74,17 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
private
+ def play_rate_limit
+ return unless current_user
+
+ limiter = ::Gitlab::ActionRateLimiter.new(action: :play_pipeline_schedule)
+
+ return unless limiter.throttled?([current_user, schedule], 1)
+
+ flash[:alert] = 'You cannot play this scheduled pipeline at the moment. Please wait a minute.'
+ redirect_to pipeline_schedules_path(@project)
+ end
+
def schedule
@schedule ||= project.pipeline_schedules.find(params[:id])
end
@@ -70,6 +95,10 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
variables_attributes: [:id, :key, :value, :_destroy] )
end
+ def authorize_play_pipeline_schedule!
+ return access_denied! unless can?(current_user, :play_pipeline_schedule, schedule)
+ end
+
def authorize_update_pipeline_schedule!
return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule)
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 7ad7b3003af..e146d0d3cd5 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipelines_count = PipelinesFinder
.new(project).execute.count
+ @pipelines.map(&:commit) # List commits for batch loading
+
respond_to do |format|
format.html
format.json do
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d925dcd21ff..d7372beb9d3 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,5 +1,6 @@
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
+ include MembersPresentation
include SortingHelper
# Authorize
@@ -20,13 +21,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- @project_members = @project_members.sort(@sort).page(params[:page])
- @requesters = AccessRequestsFinder.new(@project).execute(current_user)
+ @project_members = present_members(@project_members.sort(@sort).page(params[:page]))
+ @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
@project_member = @project.project_members.new
end
def update
- @project_member = @project.project_members.find(params[:id])
+ @project_member = @project.members_and_requesters.find(params[:id])
+ .present(current_user: current_user)
return render_403 unless can?(current_user, :update_project_member, @project_member)
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 9f9773575a5..c950d0f7001 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -29,17 +29,17 @@ class Projects::RunnersController < Projects::ApplicationController
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
- redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ redirect_to runners_path(@project), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
- redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ redirect_to runners_path(@project), alert: 'Runner was not updated.'
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index b029b31f9af..86717bb7242 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -11,6 +11,16 @@ module Projects
define_auto_devops_variables
end
+ def reset_cache
+ if ResetProjectCacheService.new(@project, current_user).execute
+ flash[:notice] = _("Project cache successfully reset.")
+ else
+ flash[:error] = _("Unable to reset project cache.")
+ end
+
+ redirect_to project_pipelines_path(@project)
+ end
+
private
def define_runners_variables
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index f3719059f88..f752a46f828 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -26,6 +26,7 @@ class Projects::TreeController < Projects::ApplicationController
respond_to do |format|
format.html do
+ lfs_blob_ids
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3882fa4791d..6f229b08c0c 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -9,6 +9,7 @@ class ProjectsController < Projects::ApplicationController
before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
+ before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
# Authorize
@@ -272,7 +273,7 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo?
else
- if @project.wiki_enabled?
+ if can?(current_user, :read_wiki, @project)
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
@@ -352,7 +353,7 @@ class ProjectsController < Projects::ApplicationController
end
def repo_exists?
- project.repository_exists? && !project.empty_repo? && project.repo
+ project.repository_exists? && !project.empty_repo?
rescue Gitlab::Git::Repository::NoRepository
project.repository.expire_exists_cache
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b46ec5e5350..493e7985d75 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -374,19 +374,14 @@ class IssuableFinder
end
def by_label(items)
- if labels?
+ return items unless labels?
+
+ items =
if filter_by_no_label?
- items = items.without_label
+ items.without_label
else
- items = items.with_label(label_names, params[:sort])
- items_projects = projects(items)
-
- if items_projects
- label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
- items = items.where(labels: { id: label_ids })
- end
+ items.with_label(label_names, params[:sort])
end
- end
items
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index ce432ddbfe6..6de9eb89468 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -1,4 +1,6 @@
class LabelsFinder < UnionFinder
+ include Gitlab::Utils::StrongMemoize
+
def initialize(current_user, params = {})
@current_user = current_user
@params = params
@@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder
label_ids << project.labels
end
end
+ elsif only_group_labels?
+ label_ids << Label.where(group_id: group.id)
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
@@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
+ def group
+ strong_memoize(:group) do
+ group = Group.find(params[:group_id])
+ authorized_to_read_labels?(group) && group
+ end
+ end
+
def group?
params[:group_id].present?
end
@@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder
params[:project_ids].present?
end
+ def only_group_labels?
+ params[:only_group_labels]
+ end
+
def title
params[:title] || params[:name]
end
@@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder
@projects
end
- def authorized_to_read_labels?(project)
+ def authorized_to_read_labels?(label_parent)
return true if skip_authorization
- Ability.allowed?(current_user, :read_label, project)
+ Ability.allowed?(current_user, :read_label, label_parent)
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 4754a67450f..d13407a06c8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -306,7 +306,7 @@ module ApplicationHelper
cookies["sidebar_collapsed"] == "true"
end
- def show_new_repo?
+ def show_new_ide?
cookies["new_repo"] == "true" && body_data_page != 'projects:show'
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 18075ee8be7..f9dcb32f7c4 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -8,7 +8,7 @@ module BlobHelper
%w(credits changelog news copying copyright license authors)
end
- def edit_path(project = @project, ref = @ref, path = @path, options = {})
+ def edit_blob_path(project = @project, ref = @ref, path = @path, options = {})
project_edit_blob_path(project,
tree_join(ref, path),
options[:link_opts])
@@ -26,10 +26,10 @@ module BlobHelper
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
- link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
+ link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
continue_params = {
- to: edit_path(project, ref, path, options),
+ to: edit_blob_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
@@ -41,6 +41,43 @@ module BlobHelper
end
end
+ def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
+ "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
+ end
+
+ def ide_edit_text
+ "#{_('Multi Edit')} <span class='label label-primary'>#{_('Beta')}</span>".html_safe
+ end
+
+ def ide_blob_link(project = @project, ref = @ref, path = @path, options = {})
+ return unless show_new_ide?
+
+ blob = options.delete(:blob)
+ blob ||= project.repository.blob_at(ref, path) rescue nil
+
+ return unless blob && blob.readable_text?
+
+ common_classes = "btn js-edit-ide #{options[:extra_class]}"
+
+ if !on_top_of_branch?(project, ref)
+ button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }
+ # This condition applies to anonymous or users who can edit directly
+ elsif current_user && can_modify_blob?(blob, project, ref)
+ link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
+ elsif current_user && can?(current_user, :fork_project, project)
+ continue_params = {
+ to: ide_edit_path(project, ref, path, options),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
+
+ button_tag ide_edit_text,
+ class: common_classes,
+ data: { fork_path: fork_path }
+ end
+ end
+
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
@@ -118,20 +155,24 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
- def blob_raw_path
+ def blob_raw_url(only_path: false)
if @build && @entry
- raw_project_job_artifacts_path(@project, @build, path: @entry.path)
+ raw_project_job_artifacts_url(@project, @build, path: @entry.path, only_path: only_path)
elsif @snippet
if @snippet.project_id
- raw_project_snippet_path(@project, @snippet)
+ raw_project_snippet_url(@project, @snippet, only_path: only_path)
else
- raw_snippet_path(@snippet)
+ raw_snippet_url(@snippet, only_path: only_path)
end
elsif @blob
- project_raw_path(@project, @id)
+ project_raw_url(@project, @id, only_path: only_path)
end
end
+ def blob_raw_path
+ blob_raw_url(only_path: true)
+ end
+
# SVGs can contain malicious JavaScript; only include whitelisted
# elements and attributes. Note that this whitelist is by no means complete
# and may omit some elements.
@@ -228,7 +269,7 @@ module BlobHelper
return if blob.empty?
if blob.raw_binary? || blob.stored_externally?
- icon = icon('download')
+ icon = sprite_icon('download')
title = 'Download'
else
icon = icon('file-code-o')
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 686437fc99a..2641a98e29e 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -23,4 +23,12 @@ module BranchesHelper
def protected_branch?(project, branch)
ProtectedBranch.protected?(project, branch.name)
end
+
+ def diverging_count_label(count)
+ if count >= Repository::MAX_DIVERGING_COUNT
+ "#{Repository::MAX_DIVERGING_COUNT - 1}+"
+ else
+ count.to_s
+ end
+ end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
new file mode 100644
index 00000000000..7e4eb06b99d
--- /dev/null
+++ b/app/helpers/clusters_helper.rb
@@ -0,0 +1,5 @@
+module ClustersHelper
+ def has_multiple_clusters?(project)
+ false
+ end
+end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 2d304f7eb91..0333c29e2fd 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -63,7 +63,7 @@ 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
- icon('code-fork', class: 'append-right-5') + "#{text}"
+ sprite_icon('fork', size: 16, css_class: 'fork-svg') + "#{text}"
end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index e82136f0177..1ce487e6592 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -104,15 +104,23 @@ module DiffHelper
].join(' ').html_safe
end
- def diff_file_blob_raw_path(diff_file)
- project_raw_path(@project, tree_join(diff_file.content_sha, diff_file.file_path))
+ def diff_file_blob_raw_url(diff_file, only_path: false)
+ project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path)
end
- def diff_file_old_blob_raw_path(diff_file)
+ def diff_file_old_blob_raw_url(diff_file, only_path: false)
sha = diff_file.old_content_sha
return unless sha
- project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path))
+ project_raw_url(@project, tree_join(diff_file.old_content_sha, diff_file.old_path), only_path: only_path)
+ end
+
+ def diff_file_blob_raw_path(diff_file)
+ diff_file_blob_raw_url(diff_file, only_path: true)
+ end
+
+ def diff_file_old_blob_raw_path(diff_file)
+ diff_file_old_blob_raw_url(diff_file, only_path: true)
end
def diff_file_html_data(project, diff_file_path, diff_commit_id)
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index b5dece38de1..e26ce6da030 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -35,7 +35,7 @@ module FormHelper
multi_select: true,
'input-meta': 'name',
'always-show-selectbox': true,
- current_user_info: current_user.to_json(only: [:id, :name])
+ current_user_info: UserSerializer.new.represent(current_user)
}
}
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index a77aa0ad2cc..7f3c118c7ab 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -182,6 +182,11 @@ module GitlabRoutingHelper
edit_project_pipeline_schedule_path(project, schedule)
end
+ def play_pipeline_schedule_path(schedule, *args)
+ project = schedule.project
+ play_project_pipeline_schedule_path(project, schedule, *args)
+ end
+
def take_ownership_pipeline_schedule_path(schedule, *args)
project = schedule.project
take_ownership_project_pipeline_schedule_path(project, schedule, *args)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index c6a83f21ceb..c5522ff7a69 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -30,6 +30,13 @@ module IconsHelper
ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
end
+ def sprite_file_icons_path
+ # SVG Sprites currently don't work across domains, so in the case of a CDN
+ # we have to set the current path deliberately to prevent addition of asset_host
+ sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
+ ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
+ end
+
def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank?
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 4c60f4b0cd0..2668cf78afe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -32,7 +32,7 @@ module IssuablesHelper
end
end
- def serialize_issuable(issuable)
+ def serialize_issuable(issuable, serializer: nil)
serializer_klass = case issuable
when Issue
IssueSerializer
@@ -42,7 +42,7 @@ module IssuablesHelper
serializer_klass
.new(current_user: current_user, project: issuable.project)
- .represent(issuable)
+ .represent(issuable, serializer: serializer)
.to_json
end
@@ -362,7 +362,7 @@ module IssuablesHelper
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
- currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
+ currentUser: UserSerializer.new.represent(current_user),
rootPath: root_path,
fullPath: @project.full_path
}
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 212cdbb8157..0f110bd25c5 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -74,7 +74,7 @@ module IssuesHelper
elsif item.try(:merged?)
'status-box-merged'
elsif item.closed?
- 'status-box-closed'
+ 'status-box-mr-closed'
elsif item.try(:upcoming?)
'status-box-upcoming'
else
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e1ba7898ee6..c1c19062c91 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -1,6 +1,13 @@
module LabelsHelper
include ActionView::Helpers::TagHelper
+ def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil)
+ return true if label.is_a?(GroupLabel)
+ return true unless project
+
+ project.feature_available?(issuables_type, current_user)
+ end
+
# Link to a Label
#
# label - Label object to link to
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 41d471cc92f..a3129cac2b1 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -1,11 +1,4 @@
module MembersHelper
- # Returns a `<action>_<source>_member` association, e.g.:
- # - admin_project_member, update_project_member, destroy_project_member
- # - admin_group_member, update_group_member, destroy_group_member
- def action_member_permission(action, member)
- "#{action}_#{member.type.underscore}".to_sym
- end
-
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 8e822ed0ea2..aaee6eaeedd 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -58,7 +58,7 @@ module PreferencesHelper
user_view
elsif user_view == "activity"
"activity"
- elsif @project.wiki_enabled?
+ elsif can?(current_user, :read_wiki, @project)
"wiki"
elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues"
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 4a6b22b5ff6..f7bdcc6fd9c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -389,7 +389,7 @@ module ProjectsHelper
end
def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
- commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name.downcase }
+ commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
project.default_branch || 'master',
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 2f57660516d..0f9ac958f95 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -139,7 +139,7 @@ module SearchHelper
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
- 'username-params' => @users.to_json(only: [:id, :username])
+ 'username-params' => UserSerializer.new.represent(@users)
},
autocomplete: 'off'
}
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 1a4f1431bdc..6cefcde558a 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -73,7 +73,6 @@ module SelectsHelper
email_user: opts[:email_user] || false,
first_user: opts[:first_user] && current_user ? current_user.username : false,
current_user: opts[:current_user] || false,
- "push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
author_id: opts[:author_id] || '',
skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil
}
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 3707bb5ba36..240783bc7fd 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -27,5 +27,16 @@ module ServicesHelper
"#{event}_events"
end
+ def service_save_button(service)
+ button_tag(class: 'btn btn-save', type: 'submit', disabled: service.deprecated?) do
+ icon('spinner spin', class: 'hidden js-btn-spinner') +
+ content_tag(:span, 'Save changes', class: 'js-btn-label')
+ end
+ end
+
+ def disable_fields_service?(service)
+ !current_controller?("admin/services") && service.deprecated?
+ end
+
extend self
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index b05eb93b465..36a311dfa8a 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -43,14 +43,20 @@ module SortingHelper
end
def groups_sort_options_hash
- options = {
+ {
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated
}
+ end
- options
+ def admin_groups_sort_options_hash
+ groups_sort_options_hash.merge(
+ sort_value_largest_group => sort_title_largest_group
+ )
end
def member_sort_options_hash
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 77a82b895ce..50e17fe7717 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -5,7 +5,7 @@ module Emails
@commit = @note.noteable
@target_url = project_commit_url(*note_target_url_options)
- mail_answer_thread(@commit, note_thread_options(recipient_id))
+ mail_answer_note_thread(@commit, @note, note_thread_options(recipient_id))
end
def note_issue_email(recipient_id, note_id)
@@ -13,7 +13,7 @@ module Emails
@issue = @note.noteable
@target_url = project_issue_url(*note_target_url_options)
- mail_answer_thread(@issue, note_thread_options(recipient_id))
+ mail_answer_note_thread(@issue, @note, note_thread_options(recipient_id))
end
def note_merge_request_email(recipient_id, note_id)
@@ -21,7 +21,7 @@ module Emails
@merge_request = @note.noteable
@target_url = project_merge_request_url(*note_target_url_options)
- mail_answer_thread(@merge_request, note_thread_options(recipient_id))
+ mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id))
end
def note_snippet_email(recipient_id, note_id)
@@ -29,7 +29,7 @@ module Emails
@snippet = @note.noteable
@target_url = project_snippet_url(*note_target_url_options)
- mail_answer_thread(@snippet, note_thread_options(recipient_id))
+ mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id))
end
def note_personal_snippet_email(recipient_id, note_id)
@@ -37,7 +37,7 @@ module Emails
@snippet = @note.noteable
@target_url = snippet_url(@note.noteable)
- mail_answer_thread(@snippet, note_thread_options(recipient_id))
+ mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id))
end
private
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 9efabe3f44e..ec886e993c3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -119,8 +119,8 @@ class Notify < BaseMailer
headers['Reply-To'] = address
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
- headers['References'] ||= ''
- headers['References'] << ' ' << fallback_reply_message_id
+ headers['References'] ||= []
+ headers['References'] << fallback_reply_message_id
@reply_by_email = true
end
@@ -156,6 +156,18 @@ class Notify < BaseMailer
mail_thread(model, headers)
end
+ def mail_answer_note_thread(model, note, headers = {})
+ headers['Message-ID'] = message_id(note)
+ headers['In-Reply-To'] = message_id(note.references.last)
+ headers['References'] = note.references.map { |ref| message_id(ref) }
+
+ headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion?
+
+ headers[:subject]&.prepend('Re: ')
+
+ mail_thread(model, headers)
+ end
+
def reply_key
@reply_key ||= SentNotification.reply_key
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 29e762724e3..19ad110db58 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -77,9 +77,15 @@ class Blob < SimpleDelegator
end
def self.lazy(project, commit_id, path)
- BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader|
- project.repository.blobs_at(items.map(&:values)).each do |blob|
- loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob
+ BatchLoader.for({ project: project, commit_id: commit_id, path: path }).batch do |items, loader|
+ items_by_project = items.group_by { |i| i[:project] }
+
+ items_by_project.each do |project, items|
+ items = items.map { |i| i.values_at(:commit_id, :path) }
+
+ project.repository.blobs_at(items).each do |blob|
+ loader.call({ project: blob.project, commit_id: blob.commit_id, path: blob.path }, blob) if blob
+ end
end
end
end
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index a8d9be945dc..cc4950240af 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -27,10 +27,17 @@ module BlobViewer
private
- def package_name_from_json(key)
- prepare!
+ def json_data
+ @json_data ||= begin
+ prepare!
+ JSON.parse(blob.data)
+ rescue
+ {}
+ end
+ end
- JSON.parse(blob.data)[key] rescue nil
+ def package_name_from_json(key)
+ json_data[key]
end
def package_name_from_method_call(name)
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
index 09221efb56c..46cd2f04f4d 100644
--- a/app/models/blob_viewer/package_json.rb
+++ b/app/models/blob_viewer/package_json.rb
@@ -16,7 +16,25 @@ module BlobViewer
@package_name ||= package_name_from_json('name')
end
+ def package_type
+ private? ? 'private package' : super
+ end
+
def package_url
+ private? ? homepage : npm_url
+ end
+
+ private
+
+ def private?
+ !!json_data['private']
+ end
+
+ def homepage
+ json_data['homepage']
+ end
+
+ def npm_url
"https://www.npmjs.com/package/#{package_name}"
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7cf8bdd968b..b65daa376d2 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -80,7 +80,7 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
- after_create do |build|
+ after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
@@ -468,7 +468,14 @@ module Ci
end
def cache
- [options[:cache]]
+ cache = options[:cache]
+
+ if cache && project.jobs_cache_index
+ cache = cache.merge(
+ key: "#{cache[:key]}:#{project.jobs_cache_index}")
+ end
+
+ [cache]
end
def credentials
@@ -498,7 +505,6 @@ module Ci
end
def valid_dependency?
- return false unless complete?
return false if artifacts_expired?
return false if erased?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index eebbf7c4218..d4690da3be6 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -228,6 +228,10 @@ module Ci
statuses.select(:stage).distinct.count
end
+ def total_size
+ statuses.count(:id)
+ end
+
def stages_names
statuses.order(:stage_idx).distinct
.pluck(:stage, :stage_idx).map(&:first)
@@ -283,8 +287,12 @@ module Ci
Ci::Pipeline.truncate_sha(sha)
end
+ # NOTE: This is loaded lazily and will never be nil, even if the commit
+ # cannot be found.
+ #
+ # Use constructs like: `pipeline.commit.present?`
def commit
- @commit ||= project.commit_by(oid: sha)
+ @commit ||= Commit.lazy(project, sha)
end
def branch?
@@ -334,12 +342,9 @@ module Ci
end
def latest?
- return false unless ref
-
- commit = project.commit(ref)
- return false unless commit
+ return false unless ref && commit.present?
- commit.sha == sha
+ project.commit(ref) == commit
end
def retried
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index c7949d11ef8..193bb48e54d 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -3,32 +3,19 @@ module Clusters
class Helm < ActiveRecord::Base
self.table_name = 'clusters_applications_helm'
+ include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
- belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
-
default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
- validates :cluster, presence: true
-
- after_initialize :set_initial_status
-
- def self.application_name
- self.to_s.demodulize.underscore
- end
-
def set_initial_status
return unless not_installable?
self.status = 'installable' if cluster&.platform_kubernetes_active?
end
- def name
- self.class.application_name
- end
-
def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, true)
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, install_helm: true)
end
end
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 44bd979741e..9024f1df1cd 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -3,41 +3,22 @@ module Clusters
class Ingress < ActiveRecord::Base
self.table_name = 'clusters_applications_ingress'
+ include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
- belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
-
- validates :cluster, presence: true
-
default_value_for :ingress_type, :nginx
default_value_for :version, :nginx
- after_initialize :set_initial_status
-
enum ingress_type: {
nginx: 1
}
- def self.application_name
- self.to_s.demodulize.underscore
- end
-
- def set_initial_status
- return unless not_installable?
-
- self.status = 'installable' if cluster&.application_helm_installed?
- end
-
- def name
- self.class.application_name
- end
-
def chart
'stable/nginx-ingress'
end
def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, false, chart)
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart)
end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
new file mode 100644
index 00000000000..9b0787ee6ca
--- /dev/null
+++ b/app/models/clusters/applications/prometheus.rb
@@ -0,0 +1,26 @@
+module Clusters
+ module Applications
+ class Prometheus < ActiveRecord::Base
+ VERSION = "2.0.0".freeze
+
+ self.table_name = 'clusters_applications_prometheus'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+
+ default_value_for :version, VERSION
+
+ def chart
+ 'stable/prometheus'
+ end
+
+ def chart_values_file
+ "#{Rails.root}/vendor/#{name}/values.yaml"
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 55419189282..5ecbd4cbceb 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -6,7 +6,8 @@ module Clusters
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
- Applications::Ingress.application_name => Applications::Ingress
+ Applications::Ingress.application_name => Applications::Ingress,
+ Applications::Prometheus.application_name => Applications::Prometheus
}.freeze
belongs_to :user
@@ -21,6 +22,7 @@ module Clusters
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
+ has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
@@ -62,7 +64,8 @@ module Clusters
def applications
[
application_helm || build_application_helm,
- application_ingress || build_application_ingress
+ application_ingress || build_application_ingress,
+ application_prometheus || build_application_prometheus
]
end
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
new file mode 100644
index 00000000000..a98fa85a5ff
--- /dev/null
+++ b/app/models/clusters/concerns/application_core.rb
@@ -0,0 +1,29 @@
+module Clusters
+ module Concerns
+ module ApplicationCore
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ validates :cluster, presence: true
+
+ after_initialize :set_initial_status
+
+ def set_initial_status
+ return unless not_installable?
+
+ self.status = 'installable' if cluster&.application_helm_installed?
+ end
+
+ def self.application_name
+ self.to_s.demodulize.underscore
+ end
+
+ def name
+ self.class.application_name
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 307e4fcedfe..39d7f5b159d 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -52,6 +52,20 @@ class Commit
diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
end
+ def order_by(collection:, order_by:, sort:)
+ return collection unless %w[email name commits].include?(order_by)
+ return collection unless %w[asc desc].include?(sort)
+
+ collection.sort do |a, b|
+ operands = [a, b].tap { |o| o.reverse! if sort == 'desc' }
+
+ attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend
+
+ # use case insensitive comparison for string values
+ order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2
+ end
+ end
+
# Truncate sha to 8 characters
def truncate_sha(sha)
sha[0..MIN_SHA_LENGTH]
@@ -72,6 +86,20 @@ class Commit
def valid_hash?(key)
!!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
end
+
+ def lazy(project, oid)
+ BatchLoader.for({ project: project, oid: oid }).batch do |items, loader|
+ items_by_project = items.group_by { |i| i[:project] }
+
+ items_by_project.each do |project, commit_ids|
+ oids = commit_ids.map { |i| i[:oid] }
+
+ project.repository.commits_by(oids: oids).each do |commit|
+ loader.call({ project: commit.project, oid: commit.id }, commit) if commit
+ end
+ end
+ end
+ end
end
attr_accessor :raw
@@ -89,7 +117,7 @@ class Commit
end
def ==(other)
- (self.class === other) && (raw == other.raw)
+ other.is_a?(self.class) && raw == other.raw
end
def self.reference_prefix
@@ -210,8 +238,8 @@ class Commit
notes.includes(:author)
end
- def method_missing(m, *args, &block)
- @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(method, include_private = false)
@@ -343,7 +371,7 @@ class Commit
#
# Returns a symbol
def uri_type(path)
- entry = @raw.tree.path(path)
+ entry = @raw.rugged_tree_entry(path)
if entry[:type] == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob
diff --git a/app/models/concerns/blocks_json_serialization.rb b/app/models/concerns/blocks_json_serialization.rb
new file mode 100644
index 00000000000..8019e6adc1c
--- /dev/null
+++ b/app/models/concerns/blocks_json_serialization.rb
@@ -0,0 +1,16 @@
+# Overrides `as_json` and `to_json` to raise an exception when called in order
+# to prevent accidentally exposing attributes
+#
+# Not that that would ever happen... but just in case.
+module BlocksJsonSerialization
+ extend ActiveSupport::Concern
+
+ JsonSerializationError = Class.new(StandardError)
+
+ def to_json(*)
+ raise JsonSerializationError,
+ "JSON serialization has been disabled on #{self.class.name}"
+ end
+
+ alias_method :as_json, :to_json
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 98776eab424..4ae5dd8c677 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,7 +11,7 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
- CACHE_VERSION = 2
+ CACHE_VERSION = 3
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
@@ -85,8 +85,7 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
- cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend
- return false unless cached
+ return false if cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? # rubocop:disable GitlabSecurity/PublicSend
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
new file mode 100644
index 00000000000..89d0474a596
--- /dev/null
+++ b/app/models/concerns/deployment_platform.rb
@@ -0,0 +1,48 @@
+module DeploymentPlatform
+ def deployment_platform
+ @deployment_platform ||=
+ find_cluster_platform_kubernetes ||
+ find_kubernetes_service_integration ||
+ build_cluster_and_deployment_platform
+ end
+
+ private
+
+ def find_cluster_platform_kubernetes
+ clusters.find_by(enabled: true)&.platform_kubernetes
+ end
+
+ def find_kubernetes_service_integration
+ services.deployment.reorder(nil).find_by(active: true)
+ end
+
+ def build_cluster_and_deployment_platform
+ return unless kubernetes_service_template
+
+ cluster = ::Clusters::Cluster.create(cluster_attributes_from_service_template)
+ cluster.platform_kubernetes if cluster.persisted?
+ end
+
+ def kubernetes_service_template
+ @kubernetes_service_template ||= KubernetesService.active.find_by_template
+ end
+
+ def cluster_attributes_from_service_template
+ {
+ name: 'kubernetes-template',
+ projects: [self],
+ provider_type: :user,
+ platform_type: :kubernetes,
+ platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template
+ }
+ end
+
+ def platform_kubernetes_attributes_from_service_template
+ {
+ api_url: kubernetes_service_template.api_url,
+ ca_pem: kubernetes_service_template.ca_pem,
+ token: kubernetes_service_template.token,
+ namespace: kubernetes_service_template.namespace
+ }
+ end
+end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index 4b4d519f3df..db9770fabf4 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -9,7 +9,6 @@ module DiscussionOnDiff
:original_line_code,
:diff_file,
:diff_line,
- :for_line?,
:active?,
:created_at_diff?,
@@ -39,17 +38,16 @@ module DiscussionOnDiff
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
+
+ initial_line_index = [diff_line.index - NUMBER_OF_TRUNCATED_DIFF_LINES + 1, 0].max
+
prev_lines = []
- lines.each do |line|
+ lines[initial_line_index..diff_line.index].each do |line|
if line.meta?
prev_lines.clear
else
prev_lines << line
-
- break if for_line?(line)
-
- prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5ca4a7086cb..4251561a0a0 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -96,7 +96,7 @@ module Issuable
strip_attributes :title
- after_save :record_metrics, unless: :imported?
+ 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
@@ -335,11 +335,6 @@ module Issuable
false
end
- def record_metrics
- metrics = self.metrics || create_metrics
- metrics.record!
- end
-
##
# Override in issuable specialization
#
@@ -347,6 +342,10 @@ module Issuable
false
end
+ def ensure_metrics
+ self.metrics || create_metrics
+ end
+
##
# Overriden in MergeRequest
#
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index b43eaeaeea0..c013e5a708f 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,13 +44,11 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
- @extractors ||= {}
-
# Use custom extractor if it's passed in the function parameters.
if extractor
- @extractors[current_user] = extractor
+ extractors[current_user] = extractor
else
- extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
+ extractor = extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
extractor.reset_memoized_values
end
@@ -69,6 +67,10 @@ module Mentionable
extractor
end
+ def extractors
+ @extractors ||= {}
+ end
+
def mentioned_users(current_user = nil)
all_references(current_user).users
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 7026f565706..fd6703831e4 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -103,9 +103,11 @@ module Milestoneish
end
def memoize_per_user(user, method_name)
- @memoized ||= {}
- @memoized[method_name] ||= {}
- @memoized[method_name][user&.id] ||= yield
+ memoized_users[method_name][user&.id] ||= yield
+ end
+
+ def memoized_users
+ @memoized_users ||= Hash.new { |h, k| h[k] = {} }
end
# override in a class that includes this module to get a faster query
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index f734952fa6c..510b8868462 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -14,10 +14,6 @@ module NoteOnDiff
raise NotImplementedError
end
- def for_line?(line)
- raise NotImplementedError
- end
-
def original_line_code
raise NotImplementedError
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 5d75b2aa6a3..86f28f30032 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -46,6 +46,7 @@ module Noteable
notes.inc_relations_for_view.grouped_diff_discussions(*args)
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def resolvable_discussions
@resolvable_discussions ||=
if defined?(@discussions)
@@ -54,6 +55,7 @@ module Noteable
discussion_notes.resolvable.discussions(self)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def discussions_resolvable?
resolvable_discussions.any?(&:resolvable?)
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index ce69fd34ac5..e48bc0be410 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -56,15 +56,17 @@ module Participable
#
# Returns an Array of User instances.
def participants(current_user = nil)
- @participants ||= Hash.new do |hash, user|
- hash[user] = raw_participants(user)
- end
-
- @participants[current_user]
+ all_participants[current_user]
end
private
+ def all_participants
+ @all_participants ||= Hash.new do |hash, user|
+ hash[user] = raw_participants(user)
+ end
+ end
+
def raw_participants(current_user = nil)
current_user ||= author
ext = Gitlab::ReferenceExtractor.new(project, current_user)
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index cb59b4da3d7..b3fec99c816 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -2,6 +2,7 @@
#
# 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.
+require 'gitlab/utils'
module ProjectFeaturesCompatibility
extend ActiveSupport::Concern
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index e961c97e337..afacdb8cb12 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -10,12 +10,12 @@ module RelativePositioning
after_save :save_positionable_neighbours
end
- def project_ids
- [project.id]
+ def min_relative_position
+ self.class.in_parents(parent_ids).minimum(:relative_position)
end
def max_relative_position
- self.class.in_projects(project_ids).maximum(:relative_position)
+ self.class.in_parents(parent_ids).maximum(:relative_position)
end
def prev_relative_position
@@ -23,7 +23,7 @@ module RelativePositioning
if self.relative_position
prev_pos = self.class
- .in_projects(project_ids)
+ .in_parents(parent_ids)
.where('relative_position < ?', self.relative_position)
.maximum(:relative_position)
end
@@ -36,7 +36,7 @@ module RelativePositioning
if self.relative_position
next_pos = self.class
- .in_projects(project_ids)
+ .in_parents(parent_ids)
.where('relative_position > ?', self.relative_position)
.minimum(:relative_position)
end
@@ -52,7 +52,7 @@ module RelativePositioning
# to its predecessor. This process will recursively move all the predecessors until we have a place
if (after.relative_position - before.relative_position) < 2
before.move_before
- @positionable_neighbours = [before]
+ @positionable_neighbours = [before] # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
self.relative_position = position_between(before.relative_position, after.relative_position)
@@ -63,9 +63,9 @@ module RelativePositioning
pos_after = before.next_relative_position
if before.shift_after?
- issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after)
+ issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_after)
issue_to_move.move_after
- @positionable_neighbours = [issue_to_move]
+ @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
pos_after = issue_to_move.relative_position
end
@@ -78,9 +78,9 @@ module RelativePositioning
pos_before = after.prev_relative_position
if after.shift_before?
- issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before)
+ issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_before)
issue_to_move.move_before
- @positionable_neighbours = [issue_to_move]
+ @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
pos_before = issue_to_move.relative_position
end
@@ -92,6 +92,10 @@ module RelativePositioning
self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
end
+ def move_to_start
+ self.relative_position = position_between(min_relative_position || START_POSITION, MIN_POSITION)
+ end
+
# Indicates if there is an issue that should be shifted to free the place
def shift_after?
next_pos = next_relative_position
@@ -132,6 +136,7 @@ module RelativePositioning
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def save_positionable_neighbours
return unless @positionable_neighbours
@@ -140,4 +145,5 @@ module RelativePositioning
status
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index f006a271327..b6c7b6735b9 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -31,15 +31,11 @@ module ResolvableDiscussion
end
def resolvable?
- return @resolvable if @resolvable.present?
-
- @resolvable = potentially_resolvable? && notes.any?(&:resolvable?)
+ @resolvable ||= potentially_resolvable? && notes.any?(&:resolvable?)
end
def resolved?
- return @resolved if @resolved.present?
-
- @resolved = resolvable? && notes.none?(&:to_be_resolved?)
+ @resolved ||= resolvable? && notes.none?(&:to_be_resolved?)
end
def first_note
@@ -49,13 +45,13 @@ module ResolvableDiscussion
def first_note_to_resolve
return unless resolvable?
- @first_note_to_resolve ||= notes.find(&:to_be_resolved?)
+ @first_note_to_resolve ||= notes.find(&:to_be_resolved?) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def last_resolved_note
return unless resolved?
- @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
+ @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def resolved_notes
@@ -95,7 +91,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
- @notes = notes_relation.fresh.to_a
+ @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |var|
instance_variable_set(:"@#{var}", nil)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 22fde2eb134..5c1cce98ad4 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -88,7 +88,7 @@ module Routable
def full_name
if route && route.name.present?
- @full_name ||= route.name
+ @full_name ||= route.name # rubocop:disable Gitlab/ModuleWithInstanceVariables
else
update_route if persisted?
@@ -112,7 +112,7 @@ module Routable
def expires_full_path_cache
RequestStore.delete(full_path_key) if RequestStore.active?
- @full_path = nil
+ @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def build_full_path
@@ -127,7 +127,7 @@ module Routable
def uncached_full_path
if route && route.path.present?
- @full_path ||= route.path
+ @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
else
update_route if persisted?
@@ -166,7 +166,7 @@ module Routable
route || build_route(source: self)
route.path = build_full_path
route.name = build_full_name
- @full_path = nil
- @full_name = nil
+ @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @full_name = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 731d9b9a745..5e4274619c4 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -12,6 +12,7 @@ module Spammable
attr_accessor :spam
attr_accessor :spam_log
+ alias_method :spam?, :spam
after_validation :check_for_spam, on: [:create, :update]
@@ -34,10 +35,6 @@ module Spammable
end
end
- def spam?
- @spam
- end
-
def check_for_spam
error_msg = if Gitlab::Recaptcha.enabled?
"Your #{spammable_entity_type} has been recognized as spam. "\
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index b3020484738..99dbd4fbacf 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -34,6 +34,8 @@ module Storage
# So we basically we mute exceptions in next actions
begin
send_update_instructions
+ write_projects_repository_config
+
true
rescue
# Returning false does not rollback after_* transaction but gives
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 25e2d8ea24e..d07041c2fdf 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -39,7 +39,7 @@ module Taskable
def task_list_items
return [] if description.blank?
- @task_list_items ||= Taskable.get_tasks(description)
+ @task_list_items ||= Taskable.get_tasks(description) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def tasks
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 9f403d96ed5..5911b56c34c 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -21,9 +21,10 @@ module TimeTrackable
has_many :timelogs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def spend_time(options)
@time_spent = options[:duration]
- @time_spent_user = options[:user]
+ @time_spent_user = User.find(options[:user_id])
@spent_at = options[:spent_at]
@original_total_time_spent = nil
@@ -36,6 +37,7 @@ module TimeTrackable
end
end
alias_method :spend_time=, :spend_time
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def total_time_spent
timelogs.sum(:time_spent)
@@ -52,9 +54,10 @@ module TimeTrackable
private
def reset_spent_time
- timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user)
+ timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def add_or_subtract_spent_time
timelogs.new(
time_spent: time_spent,
@@ -62,16 +65,19 @@ module TimeTrackable
spent_at: @spent_at
)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def check_negative_time_spent
return if time_spent.nil? || time_spent == :reset
- # we need to cache the total time spent so multiple calls to #valid?
- # doesn't give a false error
- @original_total_time_spent ||= total_time_spent
-
- if time_spent < 0 && (time_spent.abs > @original_total_time_spent)
+ if time_spent < 0 && (time_spent.abs > original_total_time_spent)
errors.add(:time_spent, 'Time to subtract exceeds the total time spent')
end
end
+
+ # we need to cache the total time spent so multiple calls to #valid?
+ # doesn't give a false error
+ def original_total_time_spent
+ @original_total_time_spent ||= total_time_spent
+ end
end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 4a65738214b..bd6af622bfb 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -22,12 +22,14 @@ class DiffDiscussion < Discussion
def merge_request_version_params
return unless for_merge_request?
- return {} if active?
- if on_merge_request_commit?
- { commit_id: commit_id }
- else
- noteable.version_params_for(position.diff_refs)
+ version_params = get_params
+
+ return version_params unless on_merge_request_commit? && commit_id
+
+ version_params ||= {}
+ version_params.tap do |params|
+ params[:commit_id] = commit_id
end
end
@@ -37,4 +39,12 @@ class DiffDiscussion < Discussion
position: position.to_json
)
end
+
+ private
+
+ def get_params
+ return {} if active?
+
+ noteable.version_params_for(position.diff_refs)
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index b53d44cda95..15122cbc693 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -21,7 +21,7 @@ class DiffNote < Note
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
- before_validation :set_line_code
+ before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits
def discussion_class(*)
@@ -61,10 +61,6 @@ class DiffNote < Note
@diff_line ||= diff_file&.line_for_position(self.original_position)
end
- def for_line?(line)
- diff_file.position(line) == self.original_position
- end
-
def original_line_code
return unless on_text?
diff --git a/app/models/event.rb b/app/models/event.rb
index 0997b056c6a..8a79100de5a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -48,7 +48,18 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
- belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+
+ belongs_to :target, -> {
+ # If the association for "target" defines an "author" association we want to
+ # eager-load this so Banzai & friends don't end up performing N+1 queries to
+ # get the authors of notes, issues, etc.
+ if reflections['events'].active_record.reflect_on_association(:author)
+ includes(:author)
+ else
+ self
+ end
+ }, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+
has_one :push_event_payload
# Callbacks
diff --git a/app/models/identity.rb b/app/models/identity.rb
index ff811e19f8a..b3fa7d8176a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -8,20 +8,30 @@ class Identity < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider }
+ before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
+
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
end
def ldap?
- provider.starts_with?('ldap')
+ Gitlab::OAuth::Provider.ldap_provider?(provider)
end
def self.normalize_uid(provider, uid)
- if provider.to_s.starts_with?('ldap')
+ if Gitlab::OAuth::Provider.ldap_provider?(provider)
Gitlab::LDAP::Person.normalize_dn(uid)
else
uid.to_s
end
end
+
+ private
+
+ def ensure_normalized_extern_uid
+ return if extern_uid.nil?
+
+ self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid)
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 33db197e612..ad4a3c737ff 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -10,6 +10,9 @@ class Issue < ActiveRecord::Base
include RelativePositioning
include TimeTrackable
include ThrottledTouch
+ include IgnorableColumn
+
+ ignore_column :assignee_id, :branch_name
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -32,6 +35,8 @@ class Issue < ActiveRecord::Base
validates :project, presence: true
+ alias_attribute :parent_ids, :project_id
+
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
@@ -75,6 +80,10 @@ class Issue < ActiveRecord::Base
acts_as_paranoid
+ class << self
+ alias_method :in_parents, :in_projects
+ end
+
def self.reference_prefix
'#'
end
@@ -273,6 +282,11 @@ class Issue < ActiveRecord::Base
private
+ def ensure_metrics
+ super
+ metrics.record!
+ end
+
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index c36be956ff0..d90cafd14b4 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -38,11 +38,7 @@ class LegacyDiffNote < Note
end
def diff_line
- @diff_line ||= diff_file.line_for_line_code(self.line_code) if diff_file
- end
-
- def for_line?(line)
- line.discussable? && diff_file.line_code(line) == self.line_code
+ @diff_line ||= diff_file&.line_for_line_code(self.line_code)
end
def original_line_code
diff --git a/app/models/member.rb b/app/models/member.rb
index 2fe5fda985f..c47145667b5 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -4,6 +4,7 @@ class Member < ActiveRecord::Base
include Importable
include Expirable
include Gitlab::Access
+ include Presentable
attr_accessor :raw_invite_token
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 422f138c4ea..ef58816937c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -8,6 +8,7 @@ class MergeRequest < ActiveRecord::Base
include ManualInverseAssociation
include EachBatch
include ThrottledTouch
+ include Gitlab::Utils::StrongMemoize
ignore_column :locked_at,
:ref_fetched
@@ -52,6 +53,7 @@ class MergeRequest < ActiveRecord::Base
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
after_create :ensure_merge_request_diff, unless: :importing?
+ after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
# When this attribute is true some MR validation is ignored
@@ -83,6 +85,14 @@ class MergeRequest < ActiveRecord::Base
transition locked: :opened
end
+ before_transition any => :opened do |merge_request|
+ merge_request.merge_jid = nil
+
+ merge_request.run_after_commit do
+ UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
+ end
+ end
+
state :opened
state :closed
state :merged
@@ -146,6 +156,13 @@ class MergeRequest < ActiveRecord::Base
'!'
end
+ def rebase_in_progress?
+ # The source project can be deleted
+ return false unless source_project
+
+ source_project.repository.rebase_in_progress?(id)
+ end
+
# Use this method whenever you need to make sure the head_pipeline is synced with the
# branch head commit, for example checking if a merge request can be merged.
# For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
@@ -387,13 +404,17 @@ class MergeRequest < ActiveRecord::Base
end
def source_branch_head
- return unless source_project
-
- source_project.repository.commit(source_branch_ref) if source_branch_ref
+ strong_memoize(:source_branch_head) do
+ if source_project && source_branch_ref
+ source_project.repository.commit(source_branch_ref)
+ end
+ end
end
def target_branch_head
- target_project.repository.commit(target_branch_ref)
+ strong_memoize(:target_branch_head) do
+ target_project.repository.commit(target_branch_ref)
+ end
end
def branch_merge_base_commit
@@ -525,6 +546,13 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def clear_memoized_shas
+ @target_branch_sha = @source_branch_sha = nil
+
+ clear_memoization(:source_branch_head)
+ clear_memoization(:target_branch_head)
+ end
+
def reload_diff_if_branch_changed
if (source_branch_changed? || target_branch_changed?) &&
(source_branch_head && target_branch_head)
@@ -586,7 +614,7 @@ class MergeRequest < ActiveRecord::Base
check_if_can_be_merged
- can_be_merged?
+ can_be_merged? && !should_be_rebased?
end
def mergeable_state?(skip_ci_check: false)
@@ -866,11 +894,11 @@ class MergeRequest < ActiveRecord::Base
def state_icon_name
if merged?
- "check"
+ "git-merge"
elsif closed?
- "times"
+ "close"
else
- "circle-o"
+ "issue-open-m"
end
end
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index cdc408738be..9e660eccd86 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,12 +1,6 @@
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
-
- def record!
- if merge_request.merged? && self.merged_at.blank?
- self.merged_at = Time.now
- end
-
- self.save
- end
+ belongs_to :latest_closed_by, class_name: 'User'
+ belongs_to :merged_by, class_name: 'User'
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c37aa0a594b..e35de9b97ee 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -104,19 +104,19 @@ class MergeRequestDiff < ActiveRecord::Base
def base_commit
return unless base_commit_sha
- project.commit(base_commit_sha)
+ project.commit_by(oid: base_commit_sha)
end
def start_commit
return unless start_commit_sha
- project.commit(start_commit_sha)
+ project.commit_by(oid: start_commit_sha)
end
def head_commit
return unless head_commit_sha
- project.commit(head_commit_sha)
+ project.commit_by(oid: head_commit_sha)
end
def commit_shas
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 901dbf2ba69..bdcc9159d26 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -40,6 +40,7 @@ class Namespace < ActiveRecord::Base
namespace_path: true
validate :nesting_level_allowed
+ validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true
@@ -257,4 +258,21 @@ class Namespace < ActiveRecord::Base
Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true)
end
+
+ def allowed_path_by_redirects
+ return if path.nil?
+
+ errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
+ end
+
+ def namespace_previously_created_with_same_path?
+ RedirectRoute.permanent.exists?(path: path)
+ end
+
+ def write_projects_repository_config
+ all_projects.find_each do |project|
+ project.expires_full_path_cache # we need to clear cache to validate renames correctly
+ project.write_repository_config
+ end
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index c4c2ab8e67d..184fbd5f5ae 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -360,6 +360,16 @@ class Note < ActiveRecord::Base
end
end
+ def references
+ refs = [noteable]
+
+ if part_of_discussion?
+ refs += discussion.notes.take_while { |n| n.id < id }
+ end
+
+ refs
+ end
+
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
@@ -401,6 +411,9 @@ class Note < ActiveRecord::Base
end
noteable_object&.touch
+
+ # We return the noteable object so we can re-use it in EE for ElasticSearch.
+ noteable_object
end
def banzai_render_context(field)
diff --git a/app/models/project.rb b/app/models/project.rb
index 6ae15a0a50f..fbe65e700a4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,6 +19,7 @@ class Project < ActiveRecord::Base
include Routable
include GroupDescendant
include Gitlab::SQL::Pattern
+ include DeploymentPlatform
extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
@@ -226,7 +227,7 @@ 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, to: :team
+ delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
# Validations
validates :creator, presence: true, on: :create
@@ -639,7 +640,7 @@ class Project < ActiveRecord::Base
end
def import?
- external_import? || forked? || gitlab_project_import?
+ external_import? || forked? || gitlab_project_import? || bare_repository_import?
end
def no_import?
@@ -659,7 +660,8 @@ class Project < ActiveRecord::Base
end
def import_started?
- import? && import_status == 'started'
+ # import? does SQL work so only run it if it looks like there's an import running
+ import_status == 'started' && import?
end
def import_scheduled?
@@ -678,6 +680,10 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new(import_url).masked_url
end
+ def bare_repository_import?
+ import_type == 'bare_repository'
+ end
+
def gitlab_project_import?
import_type == 'gitlab_project'
end
@@ -899,12 +905,6 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
- # TODO: This will be extended for multiple enviroment clusters
- def deployment_platform
- @deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
- @deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
- end
-
def monitoring_services
services.where(category: :monitoring)
end
@@ -950,7 +950,9 @@ class Project < ActiveRecord::Base
def send_move_instructions(old_path_with_namespace)
# New project path needs to be committed to the DB or notification will
# retrieve stale information
- run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) }
+ run_after_commit do
+ NotificationService.new.project_was_moved(self, old_path_with_namespace)
+ end
end
def owner
@@ -962,15 +964,19 @@ class Project < ActiveRecord::Base
end
def execute_hooks(data, hooks_scope = :push_hooks)
- hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend
- hook.async_execute(data, hooks_scope.to_s)
+ run_after_commit_or_now do
+ hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend
+ hook.async_execute(data, hooks_scope.to_s)
+ end
end
end
def execute_services(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
- services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
- service.async_execute(data)
+ run_after_commit_or_now do
+ services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
+ service.async_execute(data)
+ end
end
end
@@ -981,10 +987,6 @@ class Project < ActiveRecord::Base
false
end
- def repo
- repository.rugged
- end
-
def url_to_repo
gitlab_shell.url_to_repo(full_path)
end
@@ -1409,6 +1411,8 @@ class Project < ActiveRecord::Base
end
def after_rename_repo
+ write_repository_config
+
path_before_change = previous_changes['path'].first
# We need to check if project had been rolled out to move resource to hashed storage or not and decide
@@ -1421,6 +1425,16 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
end
+ def write_repository_config(gl_full_path: full_path)
+ # We'd need to keep track of project full path otherwise directory tree
+ # created with hashed storage enabled cannot be usefully imported using
+ # the import rake task.
+ repository.rugged.config['gitlab.fullpath'] = gl_full_path
+ rescue Gitlab::Git::Repository::NoRepository => e
+ Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
+ nil
+ end
+
def rename_repo_notify!
send_move_instructions(full_path_was)
expires_full_path_cache
@@ -1436,6 +1450,7 @@ class Project < ActiveRecord::Base
import_finish
remove_import_jid
update_project_counter_caches
+ after_create_default_branch
end
def update_project_counter_caches
@@ -1449,6 +1464,27 @@ class Project < ActiveRecord::Base
end
end
+ def after_create_default_branch
+ return unless default_branch
+
+ # Ensure HEAD points to the default branch in case it is not master
+ change_head(default_branch)
+
+ if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
+ params = {
+ name: default_branch,
+ push_access_levels_attributes: [{
+ access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }],
+ merge_access_levels_attributes: [{
+ access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }]
+ }
+
+ ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true)
+ end
+ end
+
def remove_import_jid
return unless import_jid
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 1c065e1ddbd..2be35b6ea9d 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -46,6 +46,8 @@ class JiraService < IssueTrackerService
context_path: url.path,
auth_type: :basic,
read_timeout: 120,
+ use_cookies: true,
+ additional_cookies: ['OBBasicAuth=fromDialog'],
use_ssl: url.scheme == 'https'
}
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index b82567ce2b3..c72b01b64af 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -31,6 +31,7 @@ class KubernetesService < DeploymentService
before_validation :enforce_namespace_to_lower_case
+ validate :deprecation_validation, unless: :template?
validates :namespace,
allow_blank: true,
length: 1..63,
@@ -145,6 +146,17 @@ class KubernetesService < DeploymentService
@kubeclient ||= build_kubeclient!
end
+ def deprecated?
+ !active
+ end
+
+ def deprecation_message
+ content = <<-MESSAGE.strip_heredoc
+ Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page
+ MESSAGE
+ content.html_safe
+ end
+
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private
@@ -226,4 +238,20 @@ class KubernetesService < DeploymentService
def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase
end
+
+ def deprecation_validation
+ return if active_changed?(from: true, to: false)
+
+ if deprecated?
+ errors[:base] << deprecation_message
+ end
+ end
+
+ def deprecated_message_content
+ if active?
+ "Your cluster information on this page is still editable, but you are advised to disable and reconfigure"
+ else
+ "Fields on this page are now uneditable, you can configure"
+ end
+ end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index c679758973a..a9e5cfb8240 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -7,36 +7,24 @@ class ProjectTeam
@project = project
end
- # Shortcut to add users
- #
- # Use:
- # @team << [@user, :master]
- # @team << [@users, :master]
- #
- def <<(args)
- users, access, current_user = *args
-
- if users.respond_to?(:each)
- add_users(users, access, current_user: current_user)
- else
- add_user(users, access, current_user: current_user)
- end
- end
-
def add_guest(user, current_user: nil)
- self << [user, :guest, current_user]
+ add_user(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user: nil)
- self << [user, :reporter, current_user]
+ add_user(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user: nil)
- self << [user, :developer, current_user]
+ add_user(user, :developer, current_user: current_user)
end
def add_master(user, current_user: nil)
- self << [user, :master, current_user]
+ add_user(user, :master, current_user: current_user)
+ end
+
+ def add_role(user, role, current_user: nil)
+ send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
end
def find_member(user_id)
diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb
index 31de204d824..20532527346 100644
--- a/app/models/redirect_route.rb
+++ b/app/models/redirect_route.rb
@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%")
end
+
+ scope :permanent, -> do
+ if column_permanent_exists?
+ where(permanent: true)
+ else
+ none
+ end
+ end
+
+ scope :temporary, -> do
+ if column_permanent_exists?
+ where(permanent: [false, nil])
+ else
+ all
+ end
+ end
+
+ default_value_for :permanent, false
+
+ def permanent=(value)
+ if self.class.column_permanent_exists?
+ super
+ end
+ end
+
+ def self.column_permanent_exists?
+ ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 751306188a0..9c879e2006b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -4,6 +4,7 @@ class Repository
REF_MERGE_REQUEST = 'merge-requests'.freeze
REF_KEEP_AROUND = 'keep-around'.freeze
REF_ENVIRONMENTS = 'environments'.freeze
+ MAX_DIVERGING_COUNT = 1000
RESERVED_REFS_NAMES = %W[
heads
@@ -117,6 +118,18 @@ class Repository
@commit_cache[oid] = find_commit(oid)
end
+ def commits_by(oids:)
+ return [] unless oids.present?
+
+ commits = Gitlab::Git::Commit.batch_by_oid(raw_repository, oids)
+
+ if commits.present?
+ Commit.decorate(commits, @project)
+ else
+ []
+ end
+ end
+
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
options = {
repo: raw_repository,
@@ -220,6 +233,12 @@ class Repository
branch_names.include?(branch_name)
end
+ def tag_exists?(tag_name)
+ return false unless raw_repository
+
+ tag_names.include?(tag_name)
+ end
+
def ref_exists?(ref)
!!raw_repository&.ref_exists?(ref)
rescue ArgumentError
@@ -260,11 +279,12 @@ class Repository
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
- number_commits_behind = raw_repository
- .count_commits_between(branch.dereferenced_target.sha, root_ref_hash)
-
- number_commits_ahead = raw_repository
- .count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
+ number_commits_behind, number_commits_ahead =
+ raw_repository.count_commits_between(
+ root_ref_hash,
+ branch.dereferenced_target.sha,
+ left_right: true,
+ max_count: MAX_DIVERGING_COUNT)
{ behind: number_commits_behind, ahead: number_commits_ahead }
end
@@ -690,7 +710,9 @@ class Repository
def tags_sorted_by(value)
case value
- when 'name'
+ when 'name_asc'
+ VersionSorter.sort(tags) { |tag| tag.name }
+ when 'name_desc'
VersionSorter.rsort(tags) { |tag| tag.name }
when 'updated_desc'
tags_sorted_by_committed_date.reverse
@@ -701,10 +723,14 @@ class Repository
end
end
- def contributors
+ # Params:
+ #
+ # order_by: name|email|commits
+ # sort: asc|desc default: 'asc'
+ def contributors(order_by: nil, sort: 'asc')
commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
- commits.group_by(&:author_email).map do |email, commits|
+ commits = commits.group_by(&:author_email).map do |email, commits|
contributor = Gitlab::Contributor.new
contributor.email = email
@@ -718,6 +744,7 @@ class Repository
contributor
end
+ Commit.order_by(collection: commits, order_by: order_by, sort: sort)
end
def refs_contains_sha(ref_type, sha)
@@ -756,34 +783,30 @@ class Repository
end
def create_dir(user, path, **options)
- options[:user] = user
options[:actions] = [{ action: :create_dir, file_path: path }]
- multi_action(**options)
+ multi_action(user, **options)
end
def create_file(user, path, content, **options)
- options[:user] = user
options[:actions] = [{ action: :create, file_path: path, content: content }]
- multi_action(**options)
+ multi_action(user, **options)
end
def update_file(user, path, content, **options)
previous_path = options.delete(:previous_path)
action = previous_path && previous_path != path ? :move : :update
- options[:user] = user
options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
- multi_action(**options)
+ multi_action(user, **options)
end
def delete_file(user, path, **options)
- options[:user] = user
options[:actions] = [{ action: :delete, file_path: path }]
- multi_action(**options)
+ multi_action(user, **options)
end
def with_cache_hooks
@@ -797,59 +820,14 @@ class Repository
result.newrev
end
- def with_branch(user, *args)
- with_cache_hooks do
- Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
- yield start_commit
- end
- end
- end
+ def multi_action(user, **options)
+ start_project = options.delete(:start_project)
- # rubocop:disable Metrics/ParameterLists
- def multi_action(
- user:, branch_name:, message:, actions:,
- author_email: nil, author_name: nil,
- start_branch_name: nil, start_project: project)
-
- with_branch(
- user,
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_project.repository.raw_repository) do |start_commit|
-
- index = Gitlab::Git::Index.new(raw_repository)
-
- if start_commit
- index.read_tree(start_commit.rugged_commit.tree)
- parents = [start_commit.sha]
- else
- parents = []
- end
-
- actions.each do |options|
- index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- options = {
- tree: index.write_tree,
- message: message,
- parents: parents
- }
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
-
- create_commit(options)
+ if start_project
+ options[:start_repository] = start_project.repository.raw_repository
end
- end
- # rubocop:enable Metrics/ParameterLists
- def get_committer_and_author(user, email: nil, name: nil)
- committer = user_to_committer(user)
- author = Gitlab::Git.committer_hash(email: email, name: name) || committer
-
- {
- author: author,
- committer: committer
- }
+ with_cache_hooks { raw.multi_action(user, **options) }
end
def can_be_merged?(source_sha, target_branch)
@@ -931,7 +909,7 @@ class Repository
def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
- rugged.merge_base(first_commit_id, second_commit_id)
+ raw_repository.merge_base(first_commit_id, second_commit_id)
rescue Rugged::ReferenceError
nil
end
@@ -971,8 +949,7 @@ class Repository
tmp_remote_name = true
end
- add_remote(remote_name, url)
- set_remote_as_mirror(remote_name, refmap: refmap)
+ add_remote(remote_name, url, mirror_refmap: refmap)
fetch_remote(remote_name, forced: forced)
ensure
remove_remote(remote_name) if tmp_remote_name
@@ -986,10 +963,6 @@ class Repository
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
- def remote_exists?(name)
- raw_repository.remote_exists?(name)
- end
-
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
end
@@ -1079,6 +1052,13 @@ class Repository
@project.repository_storage_path
end
+ def rebase(user, merge_request)
+ raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
+ branch_sha: merge_request.source_branch_sha,
+ remote_repository: merge_request.target_project.repository.raw,
+ remote_branch: merge_request.target_branch)
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
diff --git a/app/models/route.rb b/app/models/route.rb
index 97e8a6ad9e9..7ba3ec06041 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
+ validate :ensure_permanent_paths
+
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants.
- route.create_redirect(old_path) if attributes[:path]
+ route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path]
end
end
end
@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base
end
def conflicting_redirects
- RedirectRoute.matching_path_and_descendants(path)
+ RedirectRoute.temporary.matching_path_and_descendants(path)
end
- def create_redirect(path)
- RedirectRoute.create(source: source, path: path)
+ def create_redirect(path, permanent: false)
+ RedirectRoute.create(source: source, path: path, permanent: permanent)
end
private
def create_redirect_for_old_path
- create_redirect(path_was) if path_changed?
+ create_redirect(path_was, permanent: permanent_redirect?) if path_changed?
+ end
+
+ def permanent_redirect?
+ source_type != "Project"
+ end
+
+ def ensure_permanent_paths
+ return if path.nil?
+
+ errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
+ end
+
+ def conflicting_redirect_exists?
+ RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 3c4f1885dd0..24ba3039707 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -44,6 +44,7 @@ class Service < ActiveRecord::Base
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
+ scope :deployment, -> { where(category: 'deployment') }
default_value_for :category, 'common'
@@ -263,6 +264,18 @@ class Service < ActiveRecord::Base
service
end
+ def deprecated?
+ false
+ end
+
+ def deprecation_message
+ nil
+ end
+
+ def self.find_by_template
+ find_by(template: true)
+ end
+
private
def cache_project_has_external_issue_tracker
diff --git a/app/models/user.rb b/app/models/user.rb
index af1c36d9c93..4484ee9ff4c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -18,6 +18,7 @@ class User < ActiveRecord::Base
include CreatedAtFilterable
include IgnorableColumn
include BulkMemberAccessLoad
+ include BlocksJsonSerialization
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -93,8 +94,8 @@ class User < ActiveRecord::Base
has_one :user_synced_attributes_metadata, autosave: true
# Groups
- has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+ has_many :members
+ 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
@@ -102,7 +103,7 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
- has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -315,6 +316,8 @@ class User < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
+ query = query.downcase
+
order = <<~SQL
CASE
WHEN users.name = %{query} THEN 0
@@ -324,8 +327,11 @@ class User < ActiveRecord::Base
END
SQL
- fuzzy_search(query, [:name, :email, :username])
- .reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
+ where(
+ fuzzy_arel_match(:name, query)
+ .or(fuzzy_arel_match(:username, query))
+ .or(arel_table[:email].eq(query))
+ ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
end
# searches user by given pattern
@@ -333,15 +339,17 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query)
+ query = query.downcase
+
email_table = Email.arel_table
matched_by_emails_user_ids = email_table
.project(email_table[:user_id])
- .where(Email.fuzzy_arel_match(:email, query))
+ .where(email_table[:email].eq(query))
where(
fuzzy_arel_match(:name, query)
- .or(fuzzy_arel_match(:email, query))
.or(fuzzy_arel_match(:username, query))
+ .or(arel_table[:email].eq(query))
.or(arel_table[:id].in(matched_by_emails_user_ids))
)
end
@@ -731,7 +739,7 @@ class User < ActiveRecord::Base
def ldap_user?
if identities.loaded?
- identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? }
+ identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
else
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
end
@@ -786,10 +794,7 @@ class User < ActiveRecord::Base
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
- return unless has_attribute?(:projects_limit)
-
- connection_default_value_defined = new_record? && !projects_limit_changed?
- return unless projects_limit.nil? || connection_default_value_defined
+ return unless has_attribute?(:projects_limit) && projects_limit.nil?
self.projects_limit = current_application_settings.default_projects_limit
end
@@ -1054,13 +1059,13 @@ class User < ActiveRecord::Base
end
def todos_done_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
+ Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
TodosFinder.new(self, state: :done).execute.count
end
end
def todos_pending_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
+ Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
TodosFinder.new(self, state: :pending).execute.count
end
end
diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb
index 9f374304164..548b99b69d9 100644
--- a/app/models/user_synced_attributes_metadata.rb
+++ b/app/models/user_synced_attributes_metadata.rb
@@ -6,11 +6,11 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
SYNCABLE_ATTRIBUTES = %i[name email location].freeze
def read_only?(attribute)
- Gitlab.config.omniauth.sync_profile_from_provider && synced?(attribute)
+ sync_profile_from_provider? && synced?(attribute)
end
def read_only_attributes
- return [] unless Gitlab.config.omniauth.sync_profile_from_provider
+ return [] unless sync_profile_from_provider?
SYNCABLE_ATTRIBUTES.select { |key| synced?(key) }
end
@@ -22,4 +22,10 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
def set_attribute_synced(attribute, value)
write_attribute("#{attribute}_synced", value)
end
+
+ private
+
+ def sync_profile_from_provider?
+ Gitlab::OAuth::Provider.sync_profile_from_provider?(provider)
+ end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 4e689a9efd5..6363c382ff8 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -2,16 +2,18 @@ module Ci
class PipelinePolicy < BasePolicy
delegate { @subject.project }
- condition(:protected_ref) do
- access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
+ condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) }
- if @subject.tag?
- !access.can_create_tag?(@subject.ref)
+ rule { protected_ref }.prevent :update_pipeline
+
+ def ref_protected?(user, project, tag, ref)
+ access = ::Gitlab::UserAccess.new(user, project: project)
+
+ if tag
+ !access.can_create_tag?(ref)
else
- !access.can_update_branch?(@subject.ref)
+ !access.can_update_branch?(ref)
end
end
-
- rule { protected_ref }.prevent :update_pipeline
end
end
diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb
index 6b7598e1821..abcf536b2f7 100644
--- a/app/policies/ci/pipeline_schedule_policy.rb
+++ b/app/policies/ci/pipeline_schedule_policy.rb
@@ -2,13 +2,23 @@ module Ci
class PipelineSchedulePolicy < PipelinePolicy
alias_method :pipeline_schedule, :subject
+ condition(:protected_ref) do
+ ref_protected?(@user, @subject.project, @subject.project.repository.tag_exists?(@subject.ref), @subject.ref)
+ end
+
condition(:owner_of_schedule) do
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end
+ rule { can?(:developer_access) }.policy do
+ enable :play_pipeline_schedule
+ end
+
rule { can?(:master_access) | owner_of_schedule }.policy do
enable :update_pipeline_schedule
enable :admin_pipeline_schedule
end
+
+ rule { protected_ref }.prevent :play_pipeline_schedule
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index d2d45e402b0..f0bcba588a2 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled }
- rule { public_group } .enable :read_group
+ rule { public_group }.policy do
+ enable :read_group
+ enable :read_list
+ enable :read_label
+ end
+
rule { logged_in_viewable }.enable :read_group
rule { guest }.policy do
enable :read_group
enable :upload_file
+ enable :read_label
end
rule { admin } .enable :read_group
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index f599eab42f2..1dd8f0a25a9 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -241,7 +241,6 @@ class ProjectPolicy < BasePolicy
rule { repository_disabled }.policy do
prevent :push_code
- prevent :push_code_to_protected_branches
prevent :download_code
prevent :fork_project
prevent :read_commit_status
diff --git a/app/presenters/group_member_presenter.rb b/app/presenters/group_member_presenter.rb
new file mode 100644
index 00000000000..8f53dfa105e
--- /dev/null
+++ b/app/presenters/group_member_presenter.rb
@@ -0,0 +1,15 @@
+class GroupMemberPresenter < MemberPresenter
+ private
+
+ def admin_member_permission
+ :admin_group_member
+ end
+
+ def update_member_permission
+ :update_group_member
+ end
+
+ def destroy_member_permission
+ :destroy_group_member
+ end
+end
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
new file mode 100644
index 00000000000..7d2f9303b8f
--- /dev/null
+++ b/app/presenters/member_presenter.rb
@@ -0,0 +1,38 @@
+class MemberPresenter < Gitlab::View::Presenter::Delegated
+ presents :member
+
+ def access_level_roles
+ member.class.access_level_roles
+ end
+
+ def can_resend_invite?
+ invite? &&
+ can?(current_user, admin_member_permission, source)
+ end
+
+ def can_update?
+ can?(current_user, update_member_permission, member)
+ end
+
+ def can_remove?
+ can?(current_user, destroy_member_permission, member)
+ end
+
+ def can_approve?
+ request? && can_update?
+ end
+
+ private
+
+ def admin_member_permission
+ raise NotImplementedError
+ end
+
+ def update_member_permission
+ raise NotImplementedError
+ end
+
+ def destroy_member_permission
+ raise NotImplementedError
+ end
+end
diff --git a/app/presenters/members_presenter.rb b/app/presenters/members_presenter.rb
new file mode 100644
index 00000000000..e4aba37b69e
--- /dev/null
+++ b/app/presenters/members_presenter.rb
@@ -0,0 +1,15 @@
+class MembersPresenter < Gitlab::View::Presenter::Delegated
+ include Enumerable
+
+ presents :members
+
+ def to_ary
+ to_a
+ end
+
+ def each
+ members.each do |member|
+ yield member.present(current_user: current_user)
+ end
+ end
+end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index ab4c87c0169..c6806b7cc26 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -76,6 +76,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
+ def rebase_path
+ if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch?
+ rebase_project_merge_request_path(project, merge_request)
+ end
+ end
+
def target_branch_tree_path
if target_branch_exists?
project_tree_path(project, target_branch)
@@ -152,6 +158,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
user_can_collaborate_with_project? && can_be_cherry_picked?
end
+ def can_push_to_source_branch?
+ source_branch_exists? && user_can_push_to_source_branch?
+ end
+
private
def conflicts
@@ -174,6 +184,14 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence
end
+ def user_can_push_to_source_branch?
+ return false unless source_branch_exists?
+
+ ::Gitlab::UserAccess
+ .new(current_user, project: source_project)
+ .can_push_to_branch?(source_branch)
+ end
+
def user_can_collaborate_with_project?
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
diff --git a/app/presenters/project_member_presenter.rb b/app/presenters/project_member_presenter.rb
new file mode 100644
index 00000000000..7f42d2b70df
--- /dev/null
+++ b/app/presenters/project_member_presenter.rb
@@ -0,0 +1,15 @@
+class ProjectMemberPresenter < MemberPresenter
+ private
+
+ def admin_member_permission
+ :admin_project_member
+ end
+
+ def update_member_permission
+ :update_project_member
+ end
+
+ def destroy_member_permission
+ :destroy_project_member
+ end
+end
diff --git a/app/serializers/concerns/with_pagination.rb b/app/serializers/concerns/with_pagination.rb
index d29e22d6740..89631b73fcf 100644
--- a/app/serializers/concerns/with_pagination.rb
+++ b/app/serializers/concerns/with_pagination.rb
@@ -14,7 +14,7 @@ module WithPagination
# we shouldn't try to paginate single resources
def represent(resource, opts = {})
if paginated? && resource.respond_to?(:page)
- super(@paginator.paginate(resource), opts)
+ super(paginator.paginate(resource), opts)
else
super(resource, opts)
end
diff --git a/app/serializers/event_entity.rb b/app/serializers/event_entity.rb
deleted file mode 100644
index 935d67a4f37..00000000000
--- a/app/serializers/event_entity.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-class EventEntity < Grape::Entity
- expose :author, using: UserEntity
- expose :updated_at
-end
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
index 37240bfb0b1..aca4e4ca488 100644
--- a/app/serializers/group_child_entity.rb
+++ b/app/serializers/group_child_entity.rb
@@ -1,6 +1,7 @@
class GroupChildEntity < Grape::Entity
include ActionView::Helpers::NumberHelper
include RequestAwareEntity
+ include MarkupHelper
expose :id, :name, :description, :visibility, :full_name,
:created_at, :updated_at, :avatar_url
@@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity
number_with_delimiter(instance.member_count)
end
+ expose :markdown_description do |instance|
+ markdown_description
+ end
+
private
def membership
@@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity
def type
object.class.name.downcase
end
+
+ def markdown_description
+ markdown_field(object, :description)
+ end
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 3b5a4fd4f79..6f31fbd6b7c 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -3,14 +3,6 @@ class IssuableEntity < Grape::Entity
expose :id
expose :iid
- expose :author_id
expose :description
- expose :lock_version
- expose :milestone_id
expose :title
- expose :updated_by_id
- expose :created_at
- expose :updated_at
- expose :milestone, using: API::Entities::Milestone
- expose :labels, using: LabelEntity
end
diff --git a/app/serializers/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_entity.rb
index ff23d8bf0c7..29138c803df 100644
--- a/app/serializers/issuable_sidebar_entity.rb
+++ b/app/serializers/issuable_sidebar_entity.rb
@@ -1,4 +1,5 @@
class IssuableSidebarEntity < Grape::Entity
+ include TimeTrackableEntity
include RequestAwareEntity
expose :participants, using: ::API::Entities::UserBasic do |issuable|
@@ -8,9 +9,4 @@ class IssuableSidebarEntity < Grape::Entity
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
-
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 9d52b8d9752..0bdd4d7a272 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -2,7 +2,15 @@ class IssueEntity < IssuableEntity
include TimeTrackableEntity
expose :state
+ expose :milestone_id
+ expose :updated_by_id
+ expose :created_at
+ expose :updated_at
expose :deleted_at
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+ expose :lock_version
+ expose :author_id
expose :confidential
expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 72e56a2c77f..523b522d449 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -4,6 +4,8 @@ class JobEntity < Grape::Entity
expose :id
expose :name
+ expose :started?, as: :started
+
expose :build_path do |build|
build.target_url || path_to(:namespace_project_job, build)
end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index d54a6516aed..e4aec977f01 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -4,4 +4,5 @@ class MergeRequestBasicEntity < IssuableSidebarEntity
expose :merge_error
expose :state
expose :source_branch_exists?, as: :source_branch_exists
+ expose :rebase_in_progress?, as: :rebase_in_progress
end
diff --git a/app/serializers/merge_request_metrics_entity.rb b/app/serializers/merge_request_metrics_entity.rb
new file mode 100644
index 00000000000..3548107ac16
--- /dev/null
+++ b/app/serializers/merge_request_metrics_entity.rb
@@ -0,0 +1,6 @@
+class MergeRequestMetricsEntity < Grape::Entity
+ expose :latest_closed_at, as: :closed_at
+ expose :merged_at
+ expose :latest_closed_by, as: :closed_by, using: UserEntity
+ expose :merged_by, using: UserEntity
+end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index e9d98d8baca..caf193bdae3 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -1,14 +1,14 @@
class MergeRequestSerializer < BaseSerializer
# This overrided method takes care of which entity should be used
- # to serialize the `merge_request` based on `basic` key in `opts` param.
+ # to serialize the `merge_request` based on `serializer` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
entity =
case opts[:serializer]
when 'basic', 'sidebar'
MergeRequestBasicEntity
- else
- MergeRequestEntity
+ else # It's 'widget'
+ MergeRequestWidgetEntity
end
super(merge_request, opts, entity)
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_widget_entity.rb
index eece9445dca..48cd2317f46 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -1,8 +1,5 @@
-class MergeRequestEntity < IssuableEntity
- include TimeTrackableEntity
-
+class MergeRequestWidgetEntity < IssuableEntity
expose :state
- expose :deleted_at
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
expose :merge_error
@@ -20,9 +17,21 @@ class MergeRequestEntity < IssuableEntity
merge_request.project.merge_requests_ff_only_enabled
end
- # Events
- expose :merge_event, using: EventEntity
- expose :closed_event, using: EventEntity
+ expose :metrics do |merge_request|
+ metrics = build_metrics(merge_request)
+
+ MergeRequestMetricsEntity.new(metrics).as_json
+ end
+
+ expose :rebase_commit_sha
+ expose :rebase_in_progress?, as: :rebase_in_progress
+
+ expose :can_push_to_source_branch do |merge_request|
+ presenter(merge_request).can_push_to_source_branch?
+ end
+ expose :rebase_path do |merge_request|
+ presenter(merge_request).rebase_path
+ end
# User entities
expose :merge_user, using: UserEntity
@@ -181,4 +190,27 @@ class MergeRequestEntity < IssuableEntity
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user)
end
+
+ # Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
+ # we can remove this method and just serialize MergeRequest#metrics
+ # instead. See https://gitlab.com/gitlab-org/gitlab-ce/issues/41587
+ def build_metrics(merge_request)
+ # There's no need to query and serialize metrics data for merge requests that are not
+ # merged or closed.
+ return unless merge_request.merged? || merge_request.closed?
+ return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
+ return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
+
+ build_metrics_from_events(merge_request)
+ end
+
+ def build_metrics_from_events(merge_request)
+ closed_event = merge_request.closed_event
+ merge_event = merge_request.merge_event
+
+ MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
+ latest_closed_by: closed_event&.author,
+ merged_at: merge_event&.updated_at,
+ merged_by: merge_event&.author)
+ end
end
diff --git a/app/services/check_gcp_project_billing_service.rb b/app/services/check_gcp_project_billing_service.rb
new file mode 100644
index 00000000000..854adf2177d
--- /dev/null
+++ b/app/services/check_gcp_project_billing_service.rb
@@ -0,0 +1,8 @@
+class CheckGcpProjectBillingService
+ def execute(token)
+ client = GoogleApi::CloudPlatform::Client.new(token, nil)
+ client.projects_list.select do |project|
+ client.projects_get_billing_info(project.name).billingEnabled
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 1e5f2ed4dd2..c8b112132b3 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -12,18 +12,19 @@ module Ci
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
@pipeline = Ci::Pipeline.new
- command = OpenStruct.new(source: source,
- origin_ref: params[:ref],
- checkout_sha: params[:checkout_sha],
- after_sha: params[:after],
- before_sha: params[:before],
- trigger_request: trigger_request,
- schedule: schedule,
- ignore_skip_ci: ignore_skip_ci,
- save_incompleted: save_on_errors,
- seeds_block: block,
- project: project,
- current_user: current_user)
+ command = Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: source,
+ origin_ref: params[:ref],
+ checkout_sha: params[:checkout_sha],
+ after_sha: params[:after],
+ before_sha: params[:before],
+ trigger_request: trigger_request,
+ schedule: schedule,
+ ignore_skip_ci: ignore_skip_ci,
+ save_incompleted: save_on_errors,
+ seeds_block: block,
+ project: project,
+ current_user: current_user)
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
@@ -80,7 +81,7 @@ module Ci
end
def related_merge_requests
- MergeRequest.where(source_project: pipeline.project, source_branch: pipeline.ref)
+ MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref)
end
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index c8b6450c9b5..f832b79ef21 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -38,11 +38,15 @@ module Ci
begin
# In case when 2 runners try to assign the same build, second runner will be declined
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
- build.runner_id = runner.id
- build.run!
- register_success(build)
-
- return Result.new(build, true)
+ begin
+ build.runner_id = runner.id
+ build.run!
+ register_success(build)
+
+ return Result.new(build, true)
+ rescue Ci::Build::MissingDependenciesError
+ build.drop!(:missing_dependency_failure)
+ end
rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
# We are looping to find another build that is not conflicting
# It also indicates that this build can be picked and passed to runner.
@@ -54,9 +58,6 @@ module Ci
# we still have to return 409 in the end,
# to make sure that this is properly handled by runner.
valid = false
- rescue Ci::Build::MissingDependenciesError
- build.drop!(:missing_dependency_failure)
- valid = false
end
end
diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb
index 9a4ce31cb39..cba1b920f7c 100644
--- a/app/services/clusters/applications/base_helm_service.rb
+++ b/app/services/clusters/applications/base_helm_service.rb
@@ -18,7 +18,7 @@ module Clusters
end
def helm_api
- @helm_api ||= Gitlab::Kubernetes::Helm.new(kubeclient)
+ @helm_api ||= Gitlab::Kubernetes::Helm::Api.new(kubeclient)
end
def install_command
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
index 7d45b4aa26a..26eb274f4d5 100644
--- a/app/services/concerns/issues/resolve_discussions.rb
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -1,24 +1,28 @@
module Issues
module ResolveDiscussions
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def filter_resolve_discussion_params
@merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of)
@discussion_to_resolve_id ||= params.delete(:discussion_to_resolve)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def merge_request_to_resolve_discussions_of
- return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of)
-
- @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id)
- .execute
- .find_by(iid: merge_request_to_resolve_discussions_of_iid)
+ 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
def discussions_to_resolve
return [] unless merge_request_to_resolve_discussions_of
- @discussions_to_resolve ||=
+ @discussions_to_resolve ||= # rubocop:disable Gitlab/ModuleWithInstanceVariables
if discussion_to_resolve_id
discussion_or_nil = merge_request_to_resolve_discussions_of
.find_discussion(discussion_to_resolve_id)
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 6328d567a07..44dc90b3462 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -103,6 +103,6 @@ class EventCreateService
author_id: current_user.id
)
- Event.create(attributes)
+ Event.create!(attributes)
end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 38231f66009..8d4b9f14780 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,11 +1,14 @@
module Files
class BaseService < Commits::CreateService
+ FileChangedError = Class.new(StandardError)
+
def initialize(*args)
super
@author_email = params[:author_email]
@author_name = params[:author_name]
@commit_message = params[:commit_message]
+ @last_commit_sha = params[:last_commit_sha]
@file_path = params[:file_path]
@previous_path = params[:previous_path]
@@ -13,5 +16,16 @@ module Files
@file_content = params[:file_content]
@file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
end
+
+ def file_has_changed?(path, commit_id)
+ return false unless commit_id
+
+ last_commit = Gitlab::Git::Commit
+ .last_for_path(@start_project.repository, @start_branch, path)
+
+ return false unless last_commit
+
+ last_commit.sha != commit_id
+ end
end
end
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 7952e5c95d4..32a57484d4e 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -11,5 +11,15 @@ module Files
start_project: @start_project,
start_branch_name: @start_branch)
end
+
+ private
+
+ def validate!
+ super
+
+ if file_has_changed?(@file_path, @last_commit_sha)
+ raise FileChangedError, "You are attempting to delete a file that has been previously updated."
+ end
+ end
end
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index bfacc462847..a03c59f569d 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,8 +1,10 @@
module Files
class MultiService < Files::BaseService
+ UPDATE_FILE_ACTIONS = %w(update move delete).freeze
+
def create_commit!
repository.multi_action(
- user: current_user,
+ current_user,
message: @commit_message,
branch_name: @branch_name,
actions: params[:actions],
@@ -11,6 +13,8 @@ module Files
start_project: @start_project,
start_branch_name: @start_branch
)
+ rescue ArgumentError => e
+ raise_error(e)
end
private
@@ -18,14 +22,16 @@ module Files
def validate!
super
- params[:actions].each do |action|
- validate_action!(action)
- end
+ params[:actions].each { |action| validate_file_status!(action) }
end
- def validate_action!(action)
- unless Gitlab::Git::Index::ACTIONS.include?(action[:action].to_s)
- raise_error("Unknown action '#{action[:action]}'")
+ def validate_file_status!(action)
+ return unless UPDATE_FILE_ACTIONS.include?(action[:action])
+
+ file_path = action[:previous_path] || action[:file_path]
+
+ if file_has_changed?(file_path, action[:last_commit_id])
+ raise_error("The file has changed since you started editing it: #{file_path}")
end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index bcca1386bed..1902d1cea72 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,13 +1,5 @@
module Files
class UpdateService < Files::BaseService
- FileChangedError = Class.new(StandardError)
-
- def initialize(*args)
- super
-
- @last_commit_sha = params[:last_commit_sha]
- end
-
def create_commit!
repository.update_file(current_user, @file_path, @file_content,
message: @commit_message,
@@ -21,21 +13,10 @@ module Files
private
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
- def last_commit
- @last_commit ||= Gitlab::Git::Commit
- .last_for_path(@start_project.repository, @start_branch, @file_path)
- end
-
def validate!
super
- if file_has_changed?
+ if file_has_changed?(@file_path, @last_commit_sha)
raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index bb61136e33b..e6fd193ffb3 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -154,24 +154,7 @@ class GitPushService < BaseService
offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max
@push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
- # Ensure HEAD points to the default branch in case it is not master
- project.change_head(branch_name)
-
- # Set protection on the default branch if configured
- if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch)
-
- params = {
- name: @project.default_branch,
- push_access_levels_attributes: [{
- access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }],
- merge_access_levels_attributes: [{
- access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }]
- }
-
- ProtectedBranches::CreateService.new(@project, current_user, params).execute
- end
+ @project.after_create_default_branch
end
def build_push_data
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
index 0610b401213..7197a426a72 100644
--- a/app/services/issuable/destroy_service.rb
+++ b/app/services/issuable/destroy_service.rb
@@ -1,8 +1,10 @@
module Issuable
class DestroyService < IssuableBaseService
def execute(issuable)
- if issuable.destroy
- issuable.update_project_counter_caches
+ TodoService.new.destroy_target(issuable) do |issuable|
+ if issuable.destroy
+ issuable.update_project_counter_caches
+ end
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2c51ac13815..e7463e6e25c 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -106,12 +106,14 @@ class IssuableBaseService < BaseService
end
def merge_quick_actions_into_params!(issuable)
+ original_description = params.fetch(:description, issuable.description)
+
description, command_params =
QuickActions::InterpretService.new(project, current_user)
- .execute(params[:description], issuable)
+ .execute(original_description, issuable)
# Avoid a description already set on an issuable to be overwritten by a nil
- params[:description] = description if params.key?(:description)
+ params[:description] = description if description
params.merge!(command_params)
end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index c13f289f61e..2a2bb0cae5b 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -35,8 +35,17 @@ module Members
def can_update_access_requester?(access_requester, opts = {})
access_requester && (
opts[:force] ||
- can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ can?(current_user, update_member_permission(access_requester), access_requester)
)
end
+
+ def update_member_permission(member)
+ case member
+ when GroupMember
+ :update_group_member
+ when ProjectMember
+ :update_project_member
+ end
+ end
end
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 46c505baf8b..05b93ac8fdb 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -36,7 +36,16 @@ module Members
end
def can_destroy_member?(member)
- member && can?(current_user, action_member_permission(:destroy, member), member)
+ member && can?(current_user, destroy_member_permission(member), member)
+ end
+
+ def destroy_member_permission(member)
+ case member
+ when GroupMember
+ :destroy_group_member
+ when ProjectMember
+ :destroy_project_member
+ end
end
end
end
diff --git a/app/services/merge_request_metrics_service.rb b/app/services/merge_request_metrics_service.rb
new file mode 100644
index 00000000000..9248de14a53
--- /dev/null
+++ b/app/services/merge_request_metrics_service.rb
@@ -0,0 +1,19 @@
+class MergeRequestMetricsService
+ delegate :update!, to: :@merge_request_metrics
+
+ def initialize(merge_request_metrics)
+ @merge_request_metrics = merge_request_metrics
+ end
+
+ def merge(event)
+ update!(merged_by_id: event.author_id, merged_at: event.created_at)
+ end
+
+ def close(event)
+ update!(latest_closed_by_id: event.author_id, latest_closed_at: event.created_at)
+ end
+
+ def reopen
+ update!(latest_closed_by_id: nil, latest_closed_at: nil)
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 6b32d65a74b..20a2b50d3de 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -24,6 +24,10 @@ module MergeRequests
private
+ def merge_request_metrics_service(merge_request)
+ MergeRequestMetricsService.new(merge_request.metrics)
+ end
+
def create_assignee_note(merge_request)
SystemNoteService.change_assignee(
merge_request, merge_request.project, current_user, merge_request.assignee)
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 40213c99014..f727ec002e7 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -8,7 +8,7 @@ module MergeRequests
merge_request.allow_broken = true
if merge_request.close
- event_service.close_mr(merge_request, current_user)
+ create_event(merge_request)
create_note(merge_request)
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
@@ -19,5 +19,16 @@ module MergeRequests
merge_request
end
+
+ private
+
+ def create_event(merge_request)
+ # Making sure MergeRequest::Metrics updates are in sync with
+ # Event creation.
+ Event.transaction do
+ close_event = event_service.close_mr(merge_request, current_user)
+ merge_request_metrics_service(merge_request).close(close_event)
+ end
+ end
end
end
diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb
index 0f677a996f7..ca9a33678e4 100644
--- a/app/services/merge_requests/conflicts/list_service.rb
+++ b/app/services/merge_requests/conflicts/list_service.rb
@@ -23,7 +23,7 @@ module MergeRequests
# when there are no conflict files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
- rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
+ rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index b1d6bac4d4a..c78e78afcd1 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -9,7 +9,7 @@ module MergeRequests
close_issues(merge_request)
todo_service.merge_merge_request(merge_request, current_user)
merge_request.mark_as_merged
- create_merge_event(merge_request, current_user)
+ create_event(merge_request)
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
@@ -34,5 +34,14 @@ module MergeRequests
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
end
+
+ def create_event(merge_request)
+ # Making sure MergeRequest::Metrics updates are in sync with
+ # Event creation.
+ Event.transaction do
+ merge_event = create_merge_event(merge_request, current_user)
+ merge_request_metrics_service(merge_request).merge(merge_event)
+ end
+ end
end
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
new file mode 100644
index 00000000000..0d5a25fa28e
--- /dev/null
+++ b/app/services/merge_requests/rebase_service.rb
@@ -0,0 +1,30 @@
+module MergeRequests
+ class RebaseService < MergeRequests::WorkingCopyBaseService
+ def execute(merge_request)
+ @merge_request = merge_request
+
+ if rebase
+ success
+ else
+ error('Failed to rebase. Should be done manually')
+ end
+ end
+
+ def rebase
+ if merge_request.rebase_in_progress?
+ log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
+ return false
+ end
+
+ rebase_sha = repository.rebase(current_user, merge_request)
+
+ merge_request.update_attributes(rebase_commit_sha: rebase_sha)
+
+ true
+ rescue => e
+ log_error('Failed to rebase branch:')
+ log_error(e.message, save_message_on_model: true)
+ false
+ end
+ end
+end
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index c599a90f9fe..120677a7149 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
return merge_request unless can?(current_user, :update_merge_request, merge_request)
if merge_request.reopen
- event_service.reopen_mr(merge_request, current_user)
+ create_event(merge_request)
create_note(merge_request, 'reopened')
notification_service.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen')
@@ -16,5 +16,16 @@ module MergeRequests
merge_request
end
+
+ private
+
+ def create_event(merge_request)
+ # Making sure MergeRequest::Metrics updates are in sync with
+ # Event creation.
+ Event.transaction do
+ event_service.reopen_mr(merge_request, current_user)
+ merge_request_metrics_service(merge_request).reopen
+ end
+ end
end
end
diff --git a/app/services/merge_requests/working_copy_base_service.rb b/app/services/merge_requests/working_copy_base_service.rb
new file mode 100644
index 00000000000..186e05bf966
--- /dev/null
+++ b/app/services/merge_requests/working_copy_base_service.rb
@@ -0,0 +1,24 @@
+module MergeRequests
+ class WorkingCopyBaseService < MergeRequests::BaseService
+ attr_reader :merge_request
+
+ def source_project
+ @source_project ||= merge_request.source_project
+ end
+
+ def target_project
+ @target_project ||= merge_request.target_project
+ end
+
+ def log_error(message, save_message_on_model: false)
+ Gitlab::GitLogger.error("#{self.class.name} error (#{merge_request.to_reference(full: true)}): #{message}")
+
+ merge_request.update(merge_error: message) if save_message_on_model
+ end
+
+ # Don't try to print expensive instance variables.
+ def inspect
+ "#<#{self.class} #{merge_request.to_reference(full: true)}>"
+ end
+ end
+end
diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb
index b819bd17039..fb78420d324 100644
--- a/app/services/notes/destroy_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -1,7 +1,9 @@
module Notes
class DestroyService < BaseService
def execute(note)
- note.destroy
+ TodoService.new.destroy_target(note) do |note|
+ note.destroy
+ end
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 71533da31b1..01838ec6b5d 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -56,11 +56,7 @@ module Projects
after_create_actions if @project.persisted?
- if @project.errors.empty?
- @project.import_schedule if @project.import?
- else
- fail(error: @project.errors.full_messages.join(', '))
- end
+ import_schedule
@project
rescue ActiveRecord::RecordInvalid => e
@@ -92,6 +88,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
+ @project.write_repository_config
@project.create_wiki unless skip_wiki?
create_services_from_active_templates(@project)
@@ -164,5 +161,15 @@ module Projects
@project.path = @project.name.dup.parameterize
end
end
+
+ private
+
+ def import_schedule
+ if @project.errors.empty?
+ @project.import_schedule if @project.import? && !@project.bare_repository_import?
+ else
+ fail(error: @project.errors.full_messages.join(', '))
+ end
+ end
end
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 03be7039b2a..348eb0bf8d8 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -26,7 +26,7 @@ module Projects
name: @project.name,
path: @project.path,
shared_runners_enabled: @project.shared_runners_enabled,
- namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
+ namespace_id: target_namespace.id
}
if @project.avatar.present? && @project.avatar.image?
@@ -74,14 +74,14 @@ module Projects
Projects::ForksCountService.new(@project).refresh_cache
end
+ def target_namespace
+ @target_namespace ||= @params[:namespace] || current_user.namespace
+ end
+
def allowed_visibility_level
- project_level = @project.visibility_level
+ target_level = [@project.visibility_level, target_namespace.visibility_level].min
- if Gitlab::VisibilityLevel.non_restricted_level?(project_level)
- project_level
- else
- Gitlab::VisibilityLevel.highest_allowed_level
- end
+ Gitlab::VisibilityLevel.closest_allowed_level(target_level)
end
end
end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index 7212e7524ab..67178de75de 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -27,7 +27,9 @@ module Projects
result &&= move_repository("#{@old_wiki_disk_path}", "#{@new_disk_path}.wiki")
end
- unless result
+ if result
+ project.write_repository_config
+ else
rollback_folder_move
project.storage_version = nil
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index e5cd6fcdfe3..26765e5c3f3 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -75,6 +75,8 @@ module Projects
project.old_path_with_namespace = @old_path
project.expires_full_path_cache
+ write_repository_config(@new_path)
+
execute_system_hooks
end
rescue Exception # rubocop:disable Lint/RescueException
@@ -98,6 +100,10 @@ module Projects
project.save!
end
+ def write_repository_config(full_path)
+ project.write_repository_config(gl_full_path: full_path)
+ end
+
def refresh_permissions
# This ensures we only schedule 1 job for every user that has access to
# the namespaces.
@@ -110,6 +116,7 @@ module Projects
def rollback_side_effects
rollback_folder_move
update_namespace_and_visibility(@old_namespace)
+ write_repository_config(@old_path)
end
def rollback_folder_move
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index c499f384426..842fe4e09c4 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -5,7 +5,7 @@ module Projects
if fork_source = @project.fork_source
fork_source.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project
+ lfs_object.projects << @project unless lfs_object.projects.include?(@project)
end
refresh_forks_count(fork_source)
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index a84e335340d..6212fd69077 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -2,8 +2,8 @@ module ProtectedBranches
class CreateService < BaseService
attr_reader :protected_branch
- def execute
- raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
+ def execute(skip_authorization: false)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
project.protected_branches.create(params)
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 06ac86cd5a9..669c1ba0a22 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -405,7 +405,7 @@ module QuickActions
if time_spent
@updates[:spend_time] = {
duration: time_spent,
- user: current_user,
+ user_id: current_user.id,
spent_at: time_spent_date
}
end
@@ -428,7 +428,7 @@ module QuickActions
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :remove_time_spent do
- @updates[:spend_time] = { duration: :reset, user: current_user }
+ @updates[:spend_time] = { duration: :reset, user_id: current_user.id }
end
desc "Append the comment with #{SHRUG}"
diff --git a/app/services/reset_project_cache_service.rb b/app/services/reset_project_cache_service.rb
new file mode 100644
index 00000000000..a162a6eedb9
--- /dev/null
+++ b/app/services/reset_project_cache_service.rb
@@ -0,0 +1,5 @@
+class ResetProjectCacheService < BaseService
+ def execute
+ @project.increment!(:jobs_cache_index)
+ end
+end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index ff188102b62..92a32e703af 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -1,13 +1,16 @@
module Search
class GlobalService
attr_accessor :current_user, :params
+ attr_reader :default_project_filter
def initialize(user, params)
@current_user, @params = user, params.dup
+ @default_project_filter = true
end
def execute
- Gitlab::SearchResults.new(current_user, projects, params[:search])
+ Gitlab::SearchResults.new(current_user, projects, params[:search],
+ default_project_filter: default_project_filter)
end
def projects
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
index 29478e3251f..b4efba68715 100644
--- a/app/services/search/group_service.rb
+++ b/app/services/search/group_service.rb
@@ -5,6 +5,7 @@ module Search
def initialize(user, group, params)
super(user, params)
+ @default_project_filter = false
@group = group
end
diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb
index 11030bee8f1..d4ade869777 100644
--- a/app/services/spam_check_service.rb
+++ b/app/services/spam_check_service.rb
@@ -7,16 +7,19 @@
# - params with :request
#
module SpamCheckService
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def filter_spam_check_params
@request = params.delete(:request)
@api = params.delete(:api)
@recaptcha_verified = params.delete(:recaptcha_verified)
@spam_log_id = params.delete(:spam_log_id)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
# In order to be proceed to the spam check process, @spammable has to be
# a dirty instance, which means it should be already assigned with the new
# attribute values.
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def spam_check(spammable, user)
spam_service = SpamService.new(spammable, @request)
@@ -24,4 +27,5 @@ module SpamCheckService
user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 575853fd66b..c2ca404b179 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -31,12 +31,20 @@ class TodoService
mark_pending_todos_as_done(issue, current_user)
end
- # When we destroy an issuable we should:
+ # When we destroy a todo target we should:
#
- # * refresh the todos count cache for the current user
+ # * refresh the todos count cache for all users with todos on the target
#
- def destroy_issuable(issuable, user)
- user.update_todos_count_cache
+ # This needs to yield back to the caller to destroy the target, because it
+ # collects the todo users before the todos themselves are deleted, then
+ # updates the todo counts for those users.
+ #
+ def destroy_target(target)
+ todo_users = User.where(id: target.todos.pending.select(:user_id)).to_a
+
+ yield target
+
+ todo_users.each(&:update_todos_count_cache)
end
# When we reassign an issue we should:
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 8e20de8dfa5..00db8a2c434 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -31,6 +31,11 @@ module Users
return user
end
+ # Calling all before/after_destroy hooks for the user because
+ # there is no dependent: destroy in the relationship. And the removal
+ # is done by a foreign_key. Otherwise they won't be called
+ user.members.find_each { |member| member.run_callbacks(:destroy) }
+
user.solo_owned_groups.each do |group|
Groups::DestroyService.new(group, current_user).execute
end
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 535251fef5e..25946ba6eaf 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -11,23 +11,7 @@
.search-field-holder
= search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name'
= icon("search", class: "search-icon")
- .dropdown
- - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right
- %li.dropdown-header
- Sort by
- %li
- = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do
- = sort_title_recently_created
- = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do
- = sort_title_oldest_created
- = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do
- = sort_title_recently_updated
- = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
- = sort_title_oldest_updated
- = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do
- = sort_title_largest_group
+ = render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= link_to new_admin_group_path, class: "btn btn-new" do
New group
%ul.content-list
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 6bf979a937e..23f9927cfee 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -15,7 +15,7 @@
Unable to collect CPU info
.col-sm-4
.light-well
- %h4 Memory
+ %h4 Memory Usage
.data
- if @memory
%h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}
@@ -24,7 +24,7 @@
Unable to collect memory info
.col-sm-4
.light-well
- %h4 Disks
+ %h4 Disk Usage
.data
- @disks.each do |disk|
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
@@ -34,4 +34,4 @@
.light-well
%h4 Uptime
.data
- %h1= time_ago_with_tooltip(Rails.application.config.booted_at)
+ %h1= distance_of_time_in_words_to_now(Rails.application.config.booted_at)
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 98ff592eb64..63c5a15de1c 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -157,7 +157,6 @@
%ul
%li User will not be able to login
%li User will not be able to access git repositories
- %li User will be removed from joined projects and groups
%li Personal projects will be left
%li Owned groups will be left
%br
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 2bac69bc536..6e399fc7392 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -10,5 +10,7 @@
%p.settings-message.text-center.append-bottom-0
No variables found, add one with the form above.
- else
- = render "ci/variables/table"
- %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
+ .js-secret-variable-table
+ = render "ci/variables/table"
+ %button.btn.btn-info.js-secret-value-reveal-button{ data: { secret_reveal_status: 'false' } }
+ = n_('Reveal value', 'Reveal values', @variables.size)
diff --git a/app/views/ci/variables/_table.html.haml b/app/views/ci/variables/_table.html.haml
index 71a0b56c4f4..2298930d0c7 100644
--- a/app/views/ci/variables/_table.html.haml
+++ b/app/views/ci/variables/_table.html.haml
@@ -15,7 +15,11 @@
- if variable.id?
%tr
%td.variable-key= variable.key
- %td.variable-value{ "data-value" => variable.value }******
+ %td.variable-value
+ %span.js-secret-value-placeholder
+ = '*' * 6
+ %span.hide.js-secret-value
+ = variable.value
%td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
= link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index a5686002328..20ca6ec969a 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -83,12 +83,12 @@
You're all done!
- elsif current_user.todos.any?
.todos-all-done
- .svg-content
+ .svg-content.svg-250
= image_tag 'illustrations/todos_all_done.svg'
- if todos_filter_empty?
%h4.text-center
= Gitlab.config.gitlab.no_todos_messages.sample
- %p.text-center
+ %p
Are you looking for things to do? Take a look at
= succeed "," do
= link_to "the opened issues", issues_dashboard_path
@@ -104,7 +104,7 @@
= image_tag 'illustrations/todos_empty.svg'
.todos-empty-content
%h4
- Todos let you see what you should do next.
+ Todos let you see what you should do next
%p
When an issue or merge request is assigned to you, or when you
%strong
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 72eab964766..6364f0be4a3 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Applications", oauth_applications_path
+- breadcrumb_title @application.name
- page_title @application.name, "Applications"
- @content_class = "limit-container-width" unless fluid_layout
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 9a763887b30..f85f5c5be88 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -7,7 +7,8 @@
%span.pushed #{event.action_name} #{event.ref_type}
%strong
- commits_link = project_commits_path(project, event.ref_name)
- = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link, class: 'ref-name'
+ - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
+ = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 16038ef2f79..76a8099d7c0 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -13,13 +13,13 @@
= group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
- if @group.avatar?
- You can change your group avatar here
+ 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: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
+ = 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
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 021de4f0caf..b8692009225 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -1,3 +1,5 @@
+= webpack_bundle_tag 'docs'
+
%div
- if current_application_settings.help_page_text.present?
= markdown_field(current_application_settings, :help_page_text)
@@ -37,8 +39,12 @@
Quick help
%ul.well-list
%li= link_to 'See our website for getting help', support_url
- %li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)'
- %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.toggleHelp()'
+ %li
+ %button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
+ Use the search bar on the top of this page
+ %li
+ %button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
+ Use shortcuts
- unless current_application_settings.help_page_hide_commercial_content?
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
new file mode 100644
index 00000000000..cb413f197de
--- /dev/null
+++ b/app/views/ide/index.html.haml
@@ -0,0 +1,12 @@
+- page_title 'IDE'
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'ide'
+
+.ide-flash-container.flash-container
+
+#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
+ .text-center
+ = icon('spinner spin 2x')
+ %h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 25ed610466a..eba9cd253bb 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,7 +1,7 @@
-.page-with-sidebar{ class: page_with_sidebar_class }
+.layout-page{ class: page_with_sidebar_class }
- if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
- .content-wrapper.page-with-new-nav
+ .content-wrapper
= render 'shared/outdated_browser'
.mobile-overlay
.alert-wrapper
diff --git a/app/views/layouts/_recaptcha_verification.html.haml b/app/views/layouts/_recaptcha_verification.html.haml
index 77c77dc6754..e6f87ddd383 100644
--- a/app/views/layouts/_recaptcha_verification.html.haml
+++ b/app/views/layouts/_recaptcha_verification.html.haml
@@ -1,5 +1,4 @@
- humanized_resource_name = spammable.class.model_name.human.downcase
-- resource_name = spammable.class.model_name.singular
%h3.page-title
Anti-spam verification
@@ -8,16 +7,4 @@
%p
#{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
-= form_for form do |f|
- .recaptcha
- - params[resource_name].each do |field, value|
- = hidden_field(resource_name, field, value: value)
- = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
- = hidden_field_tag(:recaptcha_verification, true)
- = recaptcha_tags
-
- -# Yields a block with given extra params.
- = yield
-
- .row-content-block.footer-block
- = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
+= render 'shared/recaptcha_form', spammable: spammable
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 99e7f3b568d..39eb71c2bac 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -56,6 +56,8 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
+ %li
+ = link_to "Turn on multi edit", profile_preferences_path
- if current_user
%li
= link_to "Help", help_path
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index e0d8d9cb402..4013181da9c 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -49,6 +49,12 @@
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects
+ - if current_controller?('ide')
+ %li.line-separator.hidden-xs
+ = 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
- if current_user.admin?
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index a7370180bf6..32a24c101fc 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,4 +1,4 @@
-- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted?
+- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar
%ul
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 0ec07605631..dd086f70641 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: 'Admin Overview' do
@@ -130,7 +130,7 @@
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
- = link_to admin_broadcast_messages_path do
+ = 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))
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 0bf318b0b66..0c27b09f7b1 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,7 +1,7 @@
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to group_path(@group), title: @group.name do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 7e23f9c1f05..a5a62a0695f 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: 'Profile Settings' do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 53a9162b703..1fa3a3041fd 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
- can_edit = can?(current_user, :admin_project, @project)
.context-header
@@ -299,9 +299,10 @@
Charts
-# Shortcut to Issues > New Issue
- %li.hidden
- = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
- Create a new issue
+ - if project_nav_tab?(:issues)
+ %li.hidden
+ = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
+ Create a new issue
-# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds
@@ -316,5 +317,6 @@
Commits
-# Shortcut to issue boards
- %li.hidden
- = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
+ - if project_nav_tab?(:issues)
+ %li.hidden
+ = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/nav_only.html.haml b/app/views/layouts/nav_only.html.haml
new file mode 100644
index 00000000000..6fa4b39dc10
--- /dev/null
+++ b/app/views/layouts/nav_only.html.haml
@@ -0,0 +1,13 @@
+!!! 5
+%html{ lang: I18n.locale, class: page_class }
+ = render "layouts/head"
+ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } }
+ = render 'peek/bar'
+ = render "layouts/header/default"
+ = render 'shared/outdated_browser'
+ .mobile-overlay
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = yield :flash_message
+ = render "layouts/flash"
+ = yield
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 574a8f2fa50..bae37292d62 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -109,7 +109,7 @@
API
%tr
%td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
- - job_count = @pipeline.statuses.latest.size
+ - job_count = @pipeline.total_size
- stage_count = @pipeline.stages_count
successfully completed
#{job_count} #{'job'.pluralize(job_count)}
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index ddced2279e1..39622cf7f02 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -22,11 +22,11 @@ Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
-<% build_count = @pipeline.statuses.latest.size -%>
+<% job_count = @pipeline.total_size -%>
<% stage_count = @pipeline.stages_count -%>
<% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
-successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
+successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index ced58dffcdc..79e197ad08b 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -17,10 +17,6 @@
Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
- if current_user.two_factor_enabled?
= link_to 'Manage two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-info'
- = link_to 'Disable', profile_two_factor_auth_path,
- method: :delete,
- data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
- class: 'btn btn-danger'
- else
.append-bottom-10
= link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success'
@@ -88,11 +84,13 @@
= s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user
+ %button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal',
+ target: '#delete-account-modal' } }
+ = s_('Profiles|Delete account')
+
#delete-account-modal{ data: { action_url: user_registration_path,
confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
username: current_user.username } }
- %button.btn.btn-danger.disabled
- = s_('Profiles|Delete account')
- else
- if @user.solo_owned_groups.present?
%p
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 1a392e29e2a..cbea5ca605a 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -4,7 +4,7 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
- %h3.prepend-top-0
+ %h4.prepend-top-0
= page_title
%p
This is a security log of important events involving your account.
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 8dbb8aef31b..86ebec0179c 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -1,4 +1,5 @@
- page_title "GPG Keys"
+- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default
diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml
index 172c0450381..7b7960708c4 100644
--- a/app/views/profiles/keys/show.html.haml
+++ b/app/views/profiles/keys/show.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "SSH Keys", profile_keys_path
+- breadcrumb_title @key.title
- page_title @key.title, "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index e98fdfc7a3d..202eccb7bb6 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -10,16 +10,16 @@
%li= msg
= hidden_field_tag :notification_type, 'global'
- .row
+ .row.prepend-top-default
.col-lg-4.profile-settings-sidebar
- %h4
+ %h4.prepend-top-0
= page_title
%p
You can specify notification level per group or per project.
%p
By default, all projects and groups will use the global notifications setting.
.col-lg-8
- %h5
+ %h5.prepend-top-0
Global notification settings
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 66d1d1e8d44..65328791ce5 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -3,6 +3,23 @@
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
+ .col-lg-4
+ %h4.prepend-top-0
+ GitLab multi file editor
+ %p Unlock an additional editing experience which makes it possible to edit and commit multiple files
+ .col-lg-8.multi-file-editor-options
+ = label_tag do
+ .preview.append-bottom-10= image_tag "multi-editor-off.png"
+ = f.radio_button :multi_file, "off", checked: true
+ Off
+ = label_tag do
+ .preview.append-bottom-10= image_tag "multi-editor-on.png"
+ = f.radio_button :multi_file, "on", checked: false
+ On
+
+ .col-sm-12
+ %hr
+
.col-lg-4.application-theme
%h4.prepend-top-0
GitLab navigation theme
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 79f334176a5..0f773933ac2 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -22,18 +22,15 @@
.clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
- %h5.prepend-top-0
- Upload new avatar
+ %h5.prepend-top-0= _("Upload new avatar")
.prepend-top-5.append-bottom-10
- %a.btn.js-choose-user-avatar-button
- Browse file...
- %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
+ %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.
+ .help-block= _("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-gray'
+ = link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
%hr
.row
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 0b03276efcc..5207dac3ac2 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -1,5 +1,5 @@
- page_title 'Two-Factor Authentication', 'Account'
-- add_to_breadcrumbs("Account", profile_account_path)
+- add_to_breadcrumbs("Two-Factor Authentication", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
@@ -18,7 +18,12 @@
Use an app on your mobile device to enable two-factor authentication (2FA).
.col-lg-8
- if current_user.two_factor_otp_enabled?
- = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page."
+ %p
+ You've already enabled two-factor authentication using mobile authenticator applications. In order to register a different device, you must first disable two-factor authentication.
+ = link_to 'Disable two-factor authentication', profile_two_factor_auth_path,
+ method: :delete,
+ data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
+ class: 'btn btn-danger'
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 3a7a99462a6..79530e78154 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -7,7 +7,7 @@
.nav-block
= render 'projects/tree/tree_header', tree: @tree
- - if !show_new_repo? && commit
+ - if commit
= render 'shared/commit_well', commit: commit, ref: ref, project: project
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 56eecece54c..b68eb47c6b4 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,18 +1,19 @@
- event = last_push_event
- if event && show_last_push_widget?(event)
- .row-content-block.top-block.hidden-xs.white
- .event-last-push
- .event-last-push-text
- %span= s_("LastPushEvent|You pushed to")
- %strong
- = link_to event.ref_name, project_commits_path(event.project, event.ref_name), class: 'ref-name'
+ %div{ class: container_class }
+ .row-content-block.top-block.hidden-xs.white
+ .event-last-push
+ .event-last-push-text
+ %span= s_("LastPushEvent|You pushed to")
+ %strong
+ = link_to event.ref_name, project_commits_path(event.project, event.ref_name), class: 'ref-name'
- - if event.project != @project
- %span= s_("LastPushEvent|at")
- %strong= link_to_project event.project
+ - if event.project != @project
+ %span= s_("LastPushEvent|at")
+ %strong= link_to_project event.project
- #{time_ago_with_tooltip(event.created_at)}
+ #{time_ago_with_tooltip(event.created_at)}
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm" do
- #{ _('Create merge request') }
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm" do
+ #{ _('Create merge request') }
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index c5e3a7945bd..8212ab9a31e 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -17,7 +17,7 @@
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
- %li.md-header-toolbar
+ %li.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml
index 9d357293a2f..8129c72feb2 100644
--- a/app/views/projects/_merge_request_fast_forward_settings.html.haml
+++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml
@@ -10,4 +10,4 @@
No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
%br
%span.descr
- When fast-forward merge is not possible, the user must first rebase locally.
+ 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 1dd8778f800..f6e5712ce81 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -8,7 +8,7 @@
%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')
+ = 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
diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml
index c52e09573a6..54e0b73d24c 100644
--- a/app/views/projects/_merge_request_rebase_settings.html.haml
+++ b/app/views/projects/_merge_request_rebase_settings.html.haml
@@ -10,4 +10,4 @@
This way you could make sure that if this merge request would build, after merging to target branch it would also build.
%br
%span.descr
- When fast-forward merge is not possible, the user must first rebase locally.
+ When fast-forward merge is not possible, the user is given the option to rebase.
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index fe02cbcbf95..7ff7466e561 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -18,7 +18,7 @@
.tree-controls
= link_to download_project_job_artifacts_path(@project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
- = icon('download')
+ = sprite_icon('download')
Download artifacts archive
.tree-content-holder
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 7777f55ddd7..c9fa90acd11 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -3,7 +3,7 @@
.file-holder-bottom-radius.file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix{ data: { current_action: action } }
.editor-ref
- = icon('code-fork')
+ = sprite_icon('fork', size: 12)
= ref
%span.editor-file-name
- if current_action?(:edit) || current_action?(:update)
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 281363d2e01..2a77dedd9a2 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -12,6 +12,7 @@
.btn-group{ role: "group" }<
= edit_blob_link
+ = ide_blob_link
- if current_user
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 98bedae650a..5d457a50c49 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -8,3 +8,6 @@
%small
= number_to_human_size(blob.raw_size)
+
+ - if blob.stored_externally? && blob.external_storage == :lfs
+ %span.label.label-lfs.append-right-5 LFS
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index 2a178325041..5b092427496 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -3,15 +3,15 @@
Template
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
- = dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } )
+ = dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector', title: "Choose a template type" } )
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag("Apply a license template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
+ = dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
+ = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+ = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
+ = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
.template-selectors-undo-menu.hidden
%span.text-info Template applied
%button.btn.btn-sm.btn-info Undo
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index c4712bf3736..4d358052d43 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -6,21 +6,14 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'blob'
- - if show_new_repo?
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'repo'
-
= render 'projects/last_push'
%div{ class: container_class }
- - if show_new_repo?
- = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_blob_path(@project, @id)
- - else
- #tree-holder.tree-holder
- = render 'blob', blob: @blob
+ #tree-holder.tree-holder
+ = render 'blob', blob: @blob
- if can_modify_blob?(@blob)
= render 'projects/blob/remove'
- - title = "Replace #{@blob.name}"
- = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
+ - title = "Replace #{@blob.name}"
+ = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml
index a0f0215a5ff..87aa7c1dbf8 100644
--- a/app/views/projects/blob/viewers/_dependency_manager.html.haml
+++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml
@@ -6,6 +6,6 @@
- if viewer.package_name
and defines a #{viewer.package_type} named
%strong<
- = link_to viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
+ = link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
index 253566c43be..f9b1da05a00 100644
--- a/app/views/projects/blob/viewers/_download.html.haml
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -2,6 +2,6 @@
.center.render-error.vertical-center
= link_to blob_raw_path do
%h1.light
- = icon('download')
+ = sprite_icon('download')
%h4
Download (#{number_to_human_size(viewer.blob.raw_size)})
diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml
index 26ea028c5d7..2a8cefac005 100644
--- a/app/views/projects/blob/viewers/_image.html.haml
+++ b/app/views/projects/blob/viewers/_image.html.haml
@@ -1,2 +1,3 @@
.file-content.image_file
- = image_tag(blob_raw_path, alt: viewer.blob.name)
+ -# Uses the full URL rather than the path, to prevent it from getting prefixed with the asset host.
+ = image_tag(blob_raw_url, alt: viewer.blob.name)
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 573050e597d..1da0e865a41 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -8,7 +8,8 @@
%li{ class: "js-branch-#{branch.name}" }
%div
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated ref-name' do
- = icon('code-fork', class: 'append-right-5') + "#{branch.name}"
+ = sprite_icon('fork', size: 12)
+ = branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
@@ -65,16 +66,16 @@
= icon("trash-o")
- if branch.name != @repository.root_ref
- .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: number_commits_behind,
+ .divergence-graph{ 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: number_commits_ahead } }
+ number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
.bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
- %span.count.count-behind= number_commits_behind
+ %span.count.count-behind= diverging_count_label(number_commits_behind)
.graph-separator
.graph-side
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
- %span.count.count-ahead= number_commits_ahead
+ %span.count.count-ahead= diverging_count_label(number_commits_ahead)
- if commit
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 2baaaf6ac5b..e9d8fc75142 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -20,7 +20,7 @@
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, default_ref
- = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
+ = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref
= icon('chevron-down')
= render 'shared/ref_dropdown', dropdown_class: 'wide'
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index fa6f6d0b588..fa9a9bfc8f7 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -3,7 +3,7 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline>
%button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') }
- = icon('download')
+ = sprite_icon('download')
= icon("caret-down")
%span.sr-only= _('Select Archive Format')
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 86510b8ab93..0cd2d45c74b 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -22,7 +22,7 @@
- if ref
- if job.ref
.icon-container
- = job.tag? ? icon('tag') : icon('code-fork')
+ = job.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
= link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else
.light none
@@ -96,7 +96,7 @@
.pull-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
- = icon('download')
+ = sprite_icon('download')
- if can?(current_user, :update_build, job)
- if job.active?
= link_to cancel_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 7032b892029..8a13713ae02 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -11,5 +11,5 @@
%label.text-danger
= s_('ClusterIntegration|Remove cluster integration')
%p
- = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.')
- = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"})
+ = s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.")
+ = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")})
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml
index 76a66fb92a2..26ca3307a4a 100644
--- a/app/views/projects/clusters/_banner.html.haml
+++ b/app/views/projects/clusters/_banner.html.haml
@@ -1,6 +1,6 @@
-%h4= s_('ClusterIntegration|Enable cluster integration')
-.settings-content
+%h4= s_('ClusterIntegration|Cluster integration')
+.settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
%p.js-error-reason
@@ -11,11 +11,4 @@
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
- %p
- - if @cluster.enabled?
- - if can?(current_user, :update_cluster, @cluster)
- = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
- - else
- = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- - else
- = s_('ClusterIntegration|Cluster integration is disabled for this project.')
+ %p= s_('ClusterIntegration|Control how your cluster integrates with GitLab')
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml
index 18ca01d2d49..3943dfc0856 100644
--- a/app/views/projects/clusters/_cluster.html.haml
+++ b/app/views/projects/clusters/_cluster.html.haml
@@ -4,7 +4,7 @@
.table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern")
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
.table-mobile-content= cluster.environment_scope
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
@@ -16,7 +16,8 @@
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?,
- data: { "enabled-text": s_("ClusterIntegration|Active"),
- "disabled-text": s_("ClusterIntegration|Inactive"),
- endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
+ data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
= icon("spinner spin", class: "loading-icon")
+ %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')
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
index e629cc58b06..b525f4efc83 100644
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -1,12 +1,12 @@
.row.empty-state
.col-xs-12
.svg-content= image_tag 'illustrations/clusters_empty.svg'
- .col-xs-12.text-center
+ .col-xs-12
.text-content
- %h4= s_('ClusterIntegration|Integrate cluster automation')
+ %h4.text-center= s_('ClusterIntegration|Integrate cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
- %p
+ .text-center
= link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml
deleted file mode 100644
index 70c677f7856..00000000000
--- a/app/views/projects/clusters/_enabled.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
- = form_errors(@cluster)
- .form-group.append-bottom-20
- %label.append-bottom-10
- = field.hidden_field :enabled, { class: 'js-toggle-input'}
-
- %button{ type: 'button',
- class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
- "aria-label": s_("ClusterIntegration|Toggle Cluster"),
- disabled: !can?(current_user, :update_cluster, @cluster),
- data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } }
-
- - if can?(current_user, :update_cluster, @cluster)
- .form-group
- = field.submit _('Save'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
new file mode 100644
index 00000000000..9d593ffc021
--- /dev/null
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -0,0 +1,33 @@
+= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group.append-bottom-20
+ %h5= s_('ClusterIntegration|Integration status')
+ %p
+ - if @cluster.enabled?
+ - if can?(current_user, :update_cluster, @cluster)
+ = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is enabled for this project.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is disabled for this project.')
+ %label.append-bottom-10
+ = field.hidden_field :enabled, { class: 'js-toggle-input'}
+
+ %button{ type: 'button',
+ class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
+ "aria-label": s_("ClusterIntegration|Toggle Cluster"),
+ disabled: !can?(current_user, :update_cluster, @cluster) }
+ %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')
+
+ .form-group
+ %h5= s_('ClusterIntegration|Environment scope')
+ %p
+ = s_("ClusterIntegration|Choose which of your project's environments will use this 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 can?(current_user, :update_cluster, @cluster)
+ .form-group
+ = field.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_tabs.html.haml b/app/views/projects/clusters/_tabs.html.haml
deleted file mode 100644
index c8120e806fa..00000000000
--- a/app/views/projects/clusters/_tabs.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.top-area.scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left= icon("angle-left")
- .fade-right= icon("angle-right")
- %ul.nav-links.scrolling-tabs
- %li{ class: ('active' if @scope == 'active') }>
- = link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do
- = s_("ClusterIntegration|Active")
- %span.badge= @active_count
- %li{ class: ('active' if @scope == 'inactive') }>
- = link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do
- = s_("ClusterIntegration|Inactive")
- %span.badge= @inactive_count
- %li{ class: ('active' if @scope.nil? || @scope == 'all') }>
- = link_to project_clusters_path(@project), class: "js-all-tab" do
- = s_("ClusterIntegration|All")
- %span.badge= @all_count
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 0f6bae97571..e384b60d8d9 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -7,6 +7,9 @@
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|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')
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml
index f23d5b80e4f..e2d7326a312 100644
--- a/app/views/projects/clusters/gcp/_header.html.haml
+++ b/app/views/projects/clusters/gcp/_header.html.haml
@@ -10,5 +10,5 @@
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), target: '_blank', rel: 'noopener noreferrer')
+ - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index 3fa9f69708a..f3122a1bf47 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -8,6 +8,7 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
+
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index e97ce01893a..878ebaded88 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -12,6 +12,8 @@
- 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/index.html.haml b/app/views/projects/clusters/index.html.haml
index 104e39b0e06..74dbe859eea 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/projects/clusters/index.html.haml
@@ -2,23 +2,21 @@
- page_title "Clusters"
.clusters-container
- - if !@clusters.empty?
- = render "tabs"
+ - if @clusters.empty?
+ = render "empty_state"
+ - else
+ .top-area.adjust
+ .nav-text
+ = s_("ClusterIntegration|Clusters can be used to deploy applications and to provide Review Apps for this project")
.ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Cluster")
.table-section.section-30{ role: "rowheader" }
- = s_("ClusterIntegration|Environment pattern")
+ = s_("ClusterIntegration|Environment scope")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
= paginate @clusters, theme: "gitlab"
- - elsif @scope == 'all'
- = render "empty_state"
- - else
- = render "tabs"
- .prepend-top-20.text-center
- = s_("ClusterIntegration|There are no clusters to show")
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index fe6dacf1f0d..c7c84b5a42c 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -9,6 +9,7 @@
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
+ install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
@@ -17,9 +18,9 @@
.js-cluster-application-notice
.flash-container
- %section.settings.no-animate.expanded
+ %section.settings.no-animate.expanded#cluster-integration
= render 'banner'
- = render 'enabled'
+ = render 'integration_form'
.cluster-applications-table#js-cluster-applications
@@ -40,6 +41,6 @@
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
- %p= s_('ClusterIntegration|Manage cluster integration on your GitLab project')
+ %p= s_("ClusterIntegration|Advanced options on this cluster's integration")
.settings-content
= render 'advanced_settings'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index 4a9bd5186c6..babfca0c567 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -3,6 +3,9 @@
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|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')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml
index 84a52d49487..a264f3517c4 100644
--- a/app/views/projects/commit/_limit_exceeded_message.html.haml
+++ b/app/views/projects/commit/_limit_exceeded_message.html.haml
@@ -1,7 +1,7 @@
.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: "Project has too many #{label_for_message} to search"} }
.limit-icon
- if objects == :branch
- = icon('code-fork')
+ = sprite_icon('fork', size: 12)
- else
= icon('tag')
.limit-message
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 618a6355d23..d66066a6d0b 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -38,8 +38,8 @@
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom')
- - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
+ - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
+ - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
.commit-actions.flex-row.hidden-xs
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index a518fced2b4..d0c8a699608 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -5,22 +5,24 @@
= 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 Source
+ %span.input-group-addon
+ = 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'
+ .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag")
= render 'shared/ref_dropdown'
.compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
- %span.input-group-addon Target
+ %span.input-group-addon
+ = 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'
+ .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
= render 'shared/ref_dropdown'
&nbsp;
- = button_tag "Compare", class: "btn btn-create commits-compare-btn"
+ = 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'
+ = 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'
+ = link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn'
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 3ad0166e9cd..14c64b3534a 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -3,22 +3,16 @@
- page_title "Compare"
%div{ class: container_class }
+ %h3.page-title
+ = _("Compare Git revisions")
.sub-header-block
- Compare Git revisions.
- %br
- Choose a branch/tag (e.g.
- = succeed ')' do
+ - example_master = capture do
%code.ref-name master
- or enter a commit SHA (e.g.
- = succeed ')' do
+ - example_sha = capture do
%code.ref-name 4eedf23
- to see what's changed or to create a merge request.
+ = (_("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.") % { master: example_master, sha: example_sha }).html_safe
%br
- Changes are shown as if the
- %b source
- revision was being merged into the
- %b target
- revision.
+ = (_("Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision.")).html_safe
.prepend-top-20
= render "form"
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index f87f1d476f5..8da55664878 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- add_to_breadcrumbs "Compare Revisions", project_compare_index_path(@project)
+- add_to_breadcrumbs _("Compare Revisions"), project_compare_index_path(@project)
- page_title "#{params[:from]}...#{params[:to]}"
%div{ class: container_class }
@@ -13,12 +13,13 @@
.light-well
.center
%h4
- There isn't anything to compare.
+ = s_("CompareBranches|There isn't anything to compare.")
%p.slead
- if params[:to] == params[:from]
- %span.ref-name= params[:from]
- and
- %span.ref-name= params[:to]
- are the same.
+ - source_branch = capture do
+ %span.ref-name= params[:from]
+ - target_branch = capture do
+ %span.ref-name= params[:to]
+ = (s_("CompareBranches|%{source_branch} and %{target_branch} are the same.") % { source_branch: source_branch, target_branch: target_branch }).html_safe
- else
- You'll need to use different branch names to get a valid comparison.
+ = _("You'll need to use different branch names to get a valid comparison.")
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index e75ae87e771..75dd4c9ae15 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle.qa-expand-deploy-keys
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 014486be868..c7ac687e4a6 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -2,7 +2,7 @@
.branch-commit
- if deployment.ref
%span.icon-container
- = deployment.tag? ? icon('tag') : icon('code-fork')
+ = deployment.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
= link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
.icon-container.commit-icon
= custom_icon("icon_commit")
diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml
index 8fc232b464e..6dffc7c4390 100644
--- a/app/views/projects/diffs/_replaced_image_diff.html.haml
+++ b/app/views/projects/diffs/_replaced_image_diff.html.haml
@@ -1,7 +1,7 @@
- blob = diff_file.blob
- old_blob = diff_file.old_blob
-- blob_raw_path = diff_file_blob_raw_path(diff_file)
-- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- blob_raw_url = diff_file_blob_raw_url(diff_file)
+- old_blob_raw_url = diff_file_old_blob_raw_url(diff_file)
- click_to_comment = local_assigns.fetch(:click_to_comment, true)
- diff_view_data = local_assigns.fetch(:diff_view_data, '')
- class_name = ''
@@ -13,7 +13,7 @@
.two-up.view
.wrap
.frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false)
%p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size)
|
@@ -23,7 +23,7 @@
%strong H:
%span.meta-height
.wrap
- = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path }
%p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size)
|
@@ -36,9 +36,9 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false)
.swipe-wrap
- = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path }
%span.swipe-bar
%span.top-handle
%span.bottom-handle
@@ -46,8 +46,8 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
- = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false)
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path }
.controls
.transparent
.drag-track
diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml
index 6b0c6bbe48f..12be8beab39 100644
--- a/app/views/projects/diffs/_single_image_diff.html.haml
+++ b/app/views/projects/diffs/_single_image_diff.html.haml
@@ -1,7 +1,7 @@
- blob = diff_file.blob
- old_blob = diff_file.old_blob
-- blob_raw_path = diff_file_blob_raw_path(diff_file)
-- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- blob_raw_url = diff_file_blob_raw_url(diff_file)
+- old_blob_raw_url = diff_file_old_blob_raw_url(diff_file)
- click_to_comment = local_assigns.fetch(:click_to_comment, true)
- diff_view_data = local_assigns.fetch(:diff_view_data, '')
- class_name = ''
@@ -12,5 +12,5 @@
.image.js-single-image{ data: diff_view_data }
.wrap
- single_class_name = diff_file.deleted_file? ? 'deleted' : 'added'
- = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.file_path }
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.file_path }
%p.image-info= number_to_human_size(blob.size)
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 71206f3a386..e16d132f869 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -42,24 +42,23 @@
= f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%fieldset.features
- %h5.prepend-top-0
- Project avatar
+ %h5.prepend-top-0= _("Project avatar")
.form-group
- if @project.avatar?
- .avatar-container.s160
+ .avatar-container.s160.append-bottom-15
= project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160')
- %p.light
- - if @project.avatar_in_git
- Project avatar in repository: #{ @project.avatar_in_git }
- %a.choose-btn.btn.js-choose-project-avatar-button
- Browse 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.
+ - if @project.avatar_in_git
+ %p.light
+ = _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git }
+ .prepend-top-5.append-bottom-10
+ %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.")
- if @project.avatar?
%hr
- = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- = f.submit 'Save changes', class: "btn btn-save"
+ = 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"
%section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index e0aedcac5e1..ad94113fffd 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -15,8 +15,10 @@
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
- "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started'),
- "empty-loading-svg-path": image_path('illustrations/monitoring/loading'),
- "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'),
+ "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
+ "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
+ "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json),
+ "project-path": project_path(@project),
+ "tags-path": project_tags_path(@project),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } }
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index d365bcd4ecc..e8a89b8c6fc 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -2,7 +2,7 @@
- if @forked_project && !@forked_project.saved?
.alert.alert-danger.alert-block
%h4
- %i.fa.fa-code-fork
+ = sprite_icon('fork', size: 16)
Fork Error!
%p
You tried to fork
@@ -21,5 +21,4 @@
%p
= link_to new_project_fork_path(@project), title: "Fork", class: "btn" do
- %i.fa.fa-code-fork
Try to fork again
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 b98dc09534f..2599ce5c4b8 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
@@ -19,7 +19,7 @@
- if ref
- if generic_commit_status.ref
.icon-container
- = generic_commit_status.tags.any? ? icon('tag') : icon('code-fork')
+ = generic_commit_status.tags.any? ? icon('tag') : sprite_icon('fork', size: 10)
= link_to generic_commit_status.ref, project_commits_path(generic_commit_status.project, generic_commit_status.ref)
- else
.light none
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2f7aece7440..1f28d8acff6 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -14,12 +14,12 @@
.detail-page-header
.detail-page-header-body
- .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
- = icon('check', class: "hidden-sm hidden-md hidden-lg")
+ .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
Closed
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
- = icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
+ = sprite_icon('issue-open-m', size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
%span.hidden-xs Open
.issuable-meta
@@ -39,8 +39,6 @@
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- - if can_update_issue
- %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
@@ -52,9 +50,6 @@
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- - if can_update_issue
- = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
-
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam
diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml
new file mode 100644
index 00000000000..c66313bdbf3
--- /dev/null
+++ b/app/views/projects/jobs/_empty_state.html.haml
@@ -0,0 +1,17 @@
+- illustration = local_assigns.fetch(:illustration)
+- illustration_size = local_assigns.fetch(:illustration_size)
+- title = local_assigns.fetch(:title)
+- content = local_assigns.fetch(:content)
+- action = local_assigns.fetch(:action, nil)
+
+.row.empty-state
+ .col-xs-12
+ .svg-content{ class: illustration_size }
+ = image_tag illustration
+ .col-xs-12
+ .text-content
+ %h4.text-center= title
+ %p= content
+ - if action
+ .text-center
+ = action
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 1d0aaa47b60..8b05440fc78 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -54,41 +54,53 @@
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
+ - if @build.started?
+ .build-trace-container.prepend-top-default
+ .top-bar.js-top-bar
+ .js-truncated-info.truncated-info.hidden-xs.pull-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
- .build-trace-container.prepend-top-default
- .top-bar.js-top-bar
- .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
- Showing last
- %span.js-truncated-info-size.truncated-info-size><
- KiB of log -
- %a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
+ .controllers.pull-right
+ - if @build.has_trace?
+ = link_to raw_project_job_path(@project, @build),
+ title: 'Show complete raw',
+ data: { placement: 'top', container: 'body' },
+ class: 'js-raw-link-controller has-tooltip controllers-buttons' do
+ = icon('file-text-o')
- .controllers.pull-right
- - if @build.has_trace?
- = link_to raw_project_job_path(@project, @build),
- title: 'Show complete raw',
- data: { placement: 'top', container: 'body' },
- class: 'js-raw-link-controller has-tooltip controllers-buttons' do
- = icon('file-text-o')
-
- - if @build.erasable? && can?(current_user, :erase_build, @build)
- = link_to erase_project_job_path(@project, @build),
- method: :post,
- data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
- title: 'Erase job log',
- class: 'has-tooltip js-erase-link controllers-buttons' do
- = icon('trash')
- .has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
- %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
- = custom_icon('scroll_up')
- .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
- %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
+ - if @build.erasable? && can?(current_user, :erase_build, @build)
+ = link_to erase_project_job_path(@project, @build),
+ method: :post,
+ data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
+ title: 'Erase job log',
+ class: 'has-tooltip js-erase-link controllers-buttons' do
+ = icon('trash')
+ .has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
+ %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
+ = custom_icon('scroll_up')
+ .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
+ %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
+ - elsif @build.playable?
+ = render 'empty_state',
+ illustration: 'illustrations/manual_action.svg',
+ illustration_size: 'svg-394',
+ title: _('This job requires a manual action'),
+ content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.'),
+ action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), class: 'btn btn-primary', title: _('Trigger this manual action') )
+ - else
+ = render 'empty_state',
+ illustration: 'illustrations/job_not_triggered.svg',
+ illustration_size: 'svg-306',
+ title: _('This job has not been triggered yet'),
+ content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered.')
= render "sidebar"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 2b5e8711b0a..f45a000833b 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -30,7 +30,7 @@
%span.project-ref-path
&nbsp;
= link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do
- = icon('code-fork')
+ = sprite_icon('fork', size: 12, css_class: 'fork-sprite')
= merge_request.target_branch
- if merge_request.labels.any?
&nbsp;
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 135f9ab0aff..22c8b6b513d 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -7,7 +7,7 @@
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
- = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
+ = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
%span.hidden-xs
= @merge_request.state_human_name
@@ -27,7 +27,7 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can_update_merge_request
- %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
+ %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- unless current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
@@ -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 issuable-edit"
+ = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm 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/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index 60c91024b23..986ba5ae02d 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -4,14 +4,17 @@
= render 'projects/merge_requests/diffs/commit_widget'
- if @merge_request_diff&.empty?
- .nothing-here-block
- = image_tag 'illustrations/merge_request_changes_empty.svg'
- = succeed '.' do
- No changes between
- %span.ref-name= @merge_request.source_branch
- and
- %span.ref-name= @merge_request.target_branch
- %p= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
+ .row.empty-state.nothing-here-block
+ .col-xs-12
+ .svg-content= image_tag 'illustrations/merge_request_changes_empty.svg'
+ .col-xs-12
+ .text-content.text-center
+ %p
+ No changes between
+ %span.ref-name= @merge_request.source_branch
+ and
+ %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
- if diff_viewable
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index abff702fd9d..8740c6895df 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -20,7 +20,7 @@
-# haml-lint:disable InlineJavaScript
:javascript
window.gl = window.gl || {};
- window.gl.mrWidgetData = #{serialize_issuable(@merge_request)}
+ window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')}
#js-vue-mr-widget.mr-widget
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index bd8c38292d6..800e234275c 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -3,7 +3,7 @@
%td
= pipeline_schedule.description
%td.branch-name-cell
- = icon('code-fork')
+ = sprite_icon('fork', size: 12)
- if pipeline_schedule.ref.present?
= link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
%td
@@ -26,10 +26,12 @@
= pipeline_schedule.owner&.name
%td
.pull-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')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership')
- - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do
= icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index f5149306734..85946aec1f2 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,6 +1,6 @@
#js-pipeline-header-vue.pipeline-header-container
-- if @commit
+- if @commit.present?
.commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
@@ -8,28 +8,28 @@
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line))
-.info-well
- - if @commit.status
- .well-segment.pipeline-info
- .icon-container
- = icon('clock-o')
- = pluralize @pipeline.statuses.count(:id), "job"
- - if @pipeline.ref
- from
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ .info-well
+ - if @commit.status
+ .well-segment.pipeline-info
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.total_size, "job"
+ - if @pipeline.ref
+ from
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
- .well-segment.branch-info
- .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
- %span.text-expander
- \...
- %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")
+ .well-segment.branch-info
+ .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
+ %span.text-expander
+ \...
+ %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 ad61f033a1c..398a1c46746 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -8,7 +8,7 @@
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
Jobs
- %span.badge.js-builds-counter= pipeline.statuses.count
+ %span.badge.js-builds-counter= pipeline.total_size
- if failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index b2e71cff6ce..f8555f11aab 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -10,7 +10,8 @@
"new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"has-ci" => @repository.gitlab_ci_yml,
- "ci-lint-path" => ci_lint_path } }
+ "ci-lint-path" => ci_lint_path,
+ "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pipelines')
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index c63e716180c..c5f9f5aa15b 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -40,10 +40,14 @@
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
- .form-group.append-bottom-default
+ .form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light'
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ .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
+ %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
+ = _('Reveal value')
%hr
.form-group
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index e71d58ec26d..16bcf671c25 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,11 +1,13 @@
+- project = local_assigns.fetch(:project)
+- members = local_assigns.fetch(:members)
+
.panel.panel-default
.panel-heading.flex-project-members-panel
%span.flex-project-title
Members of
- %strong
- #{@project.name}
- %span.badge= @project_members.total_count
- = form_tag project_project_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
+ %strong= project.name
+ %span.badge= 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" }
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index fd5d3ec56da..d81103c3a92 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -37,5 +37,5 @@
- if @group_links.any?
= render 'projects/project_members/groups', group_links: @group_links
- = render 'projects/project_members/team', members: @project_members
+ = render 'projects/project_members/team', project: @project, members: @project_members
= paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 25d862ab4de..6376496ee1a 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -17,6 +17,10 @@
.pull-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?" }
+ - else
+ = link_to 'Resume', resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
- if runner.belongs_to_one_project?
= link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- else
diff --git a/app/views/projects/services/_deprecated_message.html.haml b/app/views/projects/services/_deprecated_message.html.haml
new file mode 100644
index 00000000000..fea9506a4bb
--- /dev/null
+++ b/app/views/projects/services/_deprecated_message.html.haml
@@ -0,0 +1,3 @@
+.flash-container.flash-container-page
+ .flash-alert.deprecated-service
+ %span= @service.deprecation_message
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index c0b1c62e8ef..21acd857ce7 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -13,10 +13,7 @@
= render 'shared/service_settings', form: form, subject: @service
- if @service.editable?
.footer-block.row-content-block
- %button.btn.btn-save{ type: 'submit' }
- = icon('spinner spin', class: 'hidden js-btn-spinner')
- %span.js-btn-label
- Save changes
+ = service_save_button(@service)
&nbsp;
- if @service.valid? && @service.activated?
- unless @service.can_test?
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 25770df1c90..df1fd583670 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -2,4 +2,6 @@
- page_title @service.title, "Services"
- add_to_breadcrumbs("Settings", edit_project_path(@project))
+= render 'deprecated_message' if @service.deprecation_message
+
= render 'form'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 031efa903c5..6e105a5521a 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -14,13 +14,13 @@
.form-group
= label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
- = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control'
+ = 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'
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, default_ref
- = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
+ = 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
@@ -28,7 +28,7 @@
.form-group
= label_tag :message, nil, class: 'control-label'
.col-sm-10
- = text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5
+ = text_area_tag :message, @message, required: false, class: 'form-control', rows: 5
.help-block
= s_('TagsPage|Optionally, add a message to the tag.')
%hr
@@ -41,6 +41,6 @@
.help-block
= s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
.form-actions
- = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create', tabindex: 3
+ = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index c51af901699..8c1c532cb3e 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -1,9 +1,12 @@
+- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id)
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
- file_name = blob_item.name
= 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
%td.tree-time-ago.cgray.text-right
= render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_old_tree_content.html.haml b/app/views/projects/tree/_old_tree_content.html.haml
deleted file mode 100644
index 6ea78851b8d..00000000000
--- a/app/views/projects/tree/_old_tree_content.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
- .table-holder
- %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
- %thead
- %tr
- %th= s_('ProjectFileTree|Name')
- %th.hidden-xs
- .pull-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
-
- = render_tree(tree)
-
- - if tree.readme
- = render "projects/tree/readme", readme: tree.readme
-
-- if can_edit_tree?
- = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
- = render 'projects/blob/new_dir'
diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml
deleted file mode 100644
index 7f636b7e0e8..00000000000
--- a/app/views/projects/tree/_old_tree_header.html.haml
+++ /dev/null
@@ -1,64 +0,0 @@
-- if on_top_of_branch?
- - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
-- 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
- = link_to project_tree_path(@project, @ref) do
- = @project.path
- - path_breadcrumbs do |title, path|
- %li
- = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
-
- - if current_user
- %li
- %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')
- - if on_top_of_branch?
- .add-to-tree-dropdown
- %ul.dropdown-menu
- - if can_edit_tree?
- %li
- = link_to project_new_blob_path(@project, @id) do
- #{ _('New file') }
- %li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
- #{ _('Upload file') }
- %li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
- #{ _('New directory') }
- - elsif can?(current_user, :fork_project, @project)
- %li
- - continue_params = { to: project_new_blob_path(@project, @id),
- notice: edit_in_new_fork_notice,
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('New file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to upload a file again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('Upload file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to create a new directory again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('New directory') }
-
- %li.divider
- %li
- = link_to new_project_branch_path(@project) do
- #{ _('New branch') }
- %li
- = link_to new_project_tag_path(@project) do
- #{ _('New tag') }
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index a4bdd67209d..6ea78851b8d 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,5 +1,24 @@
-- content_url = local_assigns.fetch(:content_url, nil)
-- if show_new_repo?
- = render 'shared/repo/repo', project: @project, content_url: content_url
-- else
- = render 'projects/tree/old_tree_content', tree: tree
+.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
+ .table-holder
+ %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
+ %thead
+ %tr
+ %th= s_('ProjectFileTree|Name')
+ %th.hidden-xs
+ .pull-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
+
+ = render_tree(tree)
+
+ - if tree.readme
+ = render "projects/tree/readme", readme: tree.readme
+
+- if can_edit_tree?
+ = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
+ = render 'projects/blob/new_dir'
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index c02f7ee37ed..d1ecef39475 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,16 +2,78 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- - if show_new_repo? && can_push_branch?(@project, @ref)
- .js-new-dropdown
- - else
- = render 'projects/tree/old_tree_header'
+ - if on_top_of_branch?
+ - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
+ - 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
+ = link_to project_tree_path(@project, @ref) do
+ = @project.path
+ - path_breadcrumbs do |title, path|
+ %li
+ = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
+
+ - if current_user
+ %li
+ %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')
+ - if on_top_of_branch?
+ .add-to-tree-dropdown
+ %ul.dropdown-menu
+ - if can_edit_tree?
+ %li
+ = link_to project_new_blob_path(@project, @id) do
+ #{ _('New file') }
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
+ #{ _('Upload file') }
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
+ #{ _('New directory') }
+ - elsif can?(current_user, :fork_project, @project)
+ %li
+ - continue_params = { to: project_new_blob_path(@project, @id),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('New file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to upload a file again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('Upload file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to create a new directory again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('New directory') }
+
+ %li.divider
+ %li
+ = link_to new_project_branch_path(@project) do
+ #{ _('New branch') }
+ %li
+ = link_to new_project_tag_path(@project) do
+ #{ _('New tag') }
.tree-controls
- - if show_new_repo?
- .editable-mode
- - else
- = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
+ - if show_new_ide?
+ = succeed " " do
+ = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
+ = ide_edit_text
+
+ = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link'
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 64cc70053ef..709be20e00f 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -6,11 +6,7 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
-- if show_new_repo?
- - content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'repo'
+= render 'projects/last_push'
-%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
- = render 'projects/last_push'
+%div{ class: [(container_class), ("limit-container-width" unless fluid_layout)] }
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index 94295970acf..75c65520350 100644
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ b/app/views/shared/_choose_group_avatar_button.html.haml
@@ -1,7 +1,4 @@
-%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' }
- %i.fa.fa-paperclip
- %span Choose File ...
-&nbsp;
-%span.file_name.js-avatar-filename File name...
-= f.file_field :avatar, class: 'js-group-avatar-input hidden'
-.light The maximum file size allowed is 200KB.
+%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.")
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index 795447a9ca6..aea0a8fd8e0 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -7,6 +7,7 @@
- choices = field[:choices]
- default_choice = field[:default_choice]
- help = field[:help]
+- disabled = disable_fields_service?(@service)
.form-group
- if type == "password" && value.present?
@@ -15,14 +16,14 @@
= form.label name, title, class: "control-label"
.col-sm-10
- if type == 'text'
- = form.text_field name, class: "form-control", placeholder: placeholder, required: required
+ = form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
- elsif type == 'textarea'
- = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required
+ = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
- elsif type == 'checkbox'
- = form.check_box name
+ = form.check_box name, disabled: disabled
- elsif type == 'select'
- = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
+ = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control", disabled: disabled}
- elsif type == 'password'
- = form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && :required
+ = form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required, disabled: disabled
- if help
%span.help-block= help
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 3fcc33044e9..81d07074325 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -2,6 +2,8 @@
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
- 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)
%li{ id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
@@ -12,12 +14,14 @@
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right
%ul
- %li
- = link_to_label(label, subject: subject, type: :merge_request) do
- View merge requests
- %li
- = link_to_label(label, subject: subject) do
- View open issues
+ - 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)
@@ -35,13 +39,20 @@
%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?'}
+ = 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.hidden-md
- = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do
- view merge requests
- = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do
- view open issues
+ - if show_label_merge_requests_link
+ = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do
+ view merge requests
+ - if show_label_issues_link
+ = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do
+ view open issues
- if current_user
.label-subscription.inline
diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml
new file mode 100644
index 00000000000..93a4301f366
--- /dev/null
+++ b/app/views/shared/_recaptcha_form.html.haml
@@ -0,0 +1,20 @@
+- resource_name = spammable.class.model_name.singular
+- humanized_resource_name = spammable.class.model_name.human.downcase
+- script = local_assigns.fetch(:script, true)
+- method = params[:action] == 'create' ? :post : :put
+- has_submit = local_assigns.fetch(:has_submit, true)
+
+= form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
+ .recaptcha
+ - params[resource_name].each do |field, value|
+ = hidden_field(resource_name, field, value: value)
+ = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
+ = hidden_field_tag(:recaptcha_verification, true)
+ = recaptcha_tags script: script, callback: 'recaptchaDialogCallback'
+
+ -# Yields a block with given extra params.
+ = yield
+
+ - if has_submit
+ .row-content-block.footer-block
+ = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index f4a4bfaec54..479bd2cdb38 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,6 +1,6 @@
- show_create = local_assigns.fetch(:show_create, false)
-- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
+- show_new_branch_form = show_new_ide? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 7ca14ac93cc..61b39afb5d4 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -11,7 +11,7 @@
.form-group
= form.label :active, "Active", class: "control-label"
.col-sm-10
- = form.check_box :active
+ = form.check_box :active, disabled: disable_fields_service?(@service)
- if @service.supported_events.present?
.form-group
diff --git a/app/views/shared/_show_aside.html.haml b/app/views/shared/_show_aside.html.haml
deleted file mode 100644
index 3ac9b11b4fa..00000000000
--- a/app/views/shared/_show_aside.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-= link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index b3f73e96b81..8e5e32e9f16 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -1,5 +1,4 @@
-%board-sidebar{ "inline-template" => true,
- ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
+%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json }
%transition{ name: "boards-sidebar-slide" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index de26fa8bbf3..62437f5fc9d 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -6,18 +6,22 @@
.col-xs-12
.svg-content
= image_tag 'illustrations/issues.svg'
- .col-xs-12.text-center
+ .col-xs-12
.text-content
- - if has_button && current_user
+ - if current_user
%h4
- The Issue Tracker is the place to add things that need to be improved or solved in a project
+ = _("The Issue Tracker is the place to add things that need to be improved or solved in a project")
%p
- Issues can be bugs, tasks or ideas to be discussed.
- Also, issues are searchable and filterable.
- - if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
- - else
- = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
+ = _("Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.")
+ - if has_button
+ .text-center
+ - if project_select_button
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
+ - else
+ = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
- else
+ %h4.text-center= _("There are no issues to show")
+ %p
+ = _("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.")
.text-center
- %h4 There are no issues to show.
+ = link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success'
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index a65634dce53..04db9de3606 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -2,10 +2,10 @@
.col-xs-12
.svg-content
= image_tag 'illustrations/labels.svg'
- .col-xs-12.text-center
+ .col-xs-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.
+ %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.")
- if can?(current_user, :admin_label, @project)
- = link_to 'New label', new_project_label_path(@project), class: 'btn btn-new', title: 'New label', id: 'new_label_link'
- = link_to 'Generate a default set of labels', generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link'
+ = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-new', title: _('New label'), id: 'new_label_link'
+ = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 67f906903e9..2edf3557df4 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -6,17 +6,18 @@
.col-xs-12
.svg-content
= image_tag 'illustrations/merge_requests.svg'
- .col-xs-12.text-center
+ .col-xs-12
.text-content
- if has_button
%h4
- Merge requests are a place to propose changes you've made to a project and discuss those changes with others.
+ = _("Merge requests are a place to propose changes you've made to a project and discuss those changes with others")
%p
- Interested parties can even contribute by pushing commits if they want to.
- - if project_select_button
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request', type: :merge_requests
- - else
- = link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link'
+ = _("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
+ - else
+ = link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link'
- else
%h4.text-center
- There are no merge requests to show.
+ = _("There are no merge requests to show")
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 8e6747ca740..1a259b679c7 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -1,3 +1,4 @@
+- options_hash = local_assigns.fetch(:options_hash, groups_sort_options_hash)
- show_archive_options = local_assigns.fetch(:show_archive_options, false)
- if @sort.present?
- default_sort_by = @sort
@@ -10,12 +11,12 @@
.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-label
- = sort_options_hash[default_sort_by]
+ = options_hash[default_sort_by]
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- - groups_sort_options_hash.each do |value, title|
+ - options_hash.each do |value, title|
%li.js-filter-sort-order
= link_to filter_groups_path(sort: value), class: ("is-active" if default_sort_by == value) do
= title
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index 217af7c9fac..fc86f855865 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -1,14 +1,10 @@
-- max_render = 3
-- max = [max_render, issue.assignees.length].min
+- max_render = 4
+- assignees_rendering_overflow = issue.assignees.size > max_render
+- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
+- more_assignees_count = issue.assignees.size - render_count
-- issue.assignees.take(max).each do |assignee|
+- issue.assignees.take(render_count).each do |assignee|
= link_to_member(@project, assignee, name: false, title: "Assigned to :name")
-- if issue.assignees.length > max_render
- - counter = issue.assignees.length - max_render
-
- %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{counter} more assignees" } }
- - if counter < 99
- = "+#{counter}"
- - else
- 99+
+- if more_assignees_count.positive?
+ %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{more_assignees_count} more assignees" } } +#{more_assignees_count}
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2c27dd638a7..71878e93255 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,9 +1,9 @@
- show_roles = local_assigns.fetch(:show_roles, true)
- show_controls = local_assigns.fetch(:show_controls, true)
- force_mobile_view = local_assigns.fetch(:force_mobile_view, false)
+- member = local_assigns.fetch(:member)
- user = local_assigns.fetch(:user, member.user)
- source = member.source
-- can_admin_member = can?(current_user, action_member_permission(:update, member), member)
%li.member{ class: dom_class(member), id: dom_id(member) }
%span.list-item-name
@@ -50,18 +50,17 @@
.controls.member-controls
- if show_controls && member.source == current_resource
- - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source)
+ - 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',
title: 'Resend invite'
- - if user != current_user && can_admin_member
+ - if user != current_user && member.can_update?
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
- disabled: !can_admin_member,
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
%span.dropdown-toggle-text
= member.human_access
@@ -70,23 +69,22 @@
= dropdown_title("Change permissions")
.dropdown-content
%ul
- - member.class.access_level_roles.each do |role, role_id|
+ - member.access_level_roles.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) }
.prepend-left-5.clearable-input.member-form-control
- = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) }
+ = f.text_field :expires_at,
+ class: 'form-control js-access-expiration-date js-member-update-control',
+ placeholder: 'Expiration date',
+ id: "member_expires_at_#{member.id}",
+ data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input
- else
%span.member-access-text= member.human_access
- - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source)
- = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
- method: :post,
- class: 'btn btn-default prepend-left-10 visible-xs-block'
-
- - elsif member.request? && can_admin_member
+ - if member.can_approve?
= link_to polymorphic_path([:approve_access_request, member]),
method: :post,
class: 'btn btn-success prepend-left-10',
@@ -96,7 +94,7 @@
- unless force_mobile_view
= icon('check inverse', class: 'hidden-xs')
- - if can?(current_user, action_member_permission(:destroy, member), member)
+ - if member.can_remove?
- if current_user == user
= link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
method: :delete,
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
index 09b9944082f..1fbd6bcc4cb 100644
--- a/app/views/shared/members/_requests.html.haml
+++ b/app/views/shared/members/_requests.html.haml
@@ -1,10 +1,13 @@
+- membership_source = local_assigns.fetch(:membership_source)
+- requesters = local_assigns.fetch(:requesters)
- force_mobile_view = local_assigns.fetch(:force_mobile_view, false)
-- if requesters.any?
- .panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) }
- .panel-heading
- Users requesting access to
- %strong= membership_source.name
- %span.badge= requesters.size
- %ul.content-list.members-list
- = render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view }
+- return if requesters.empty?
+
+.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) }
+ .panel-heading
+ Users requesting access to
+ %strong= membership_source.name
+ %span.badge= 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/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index c978d9e4821..98e0161f7d1 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -20,8 +20,8 @@
- if note.is_a?(DiffNote) && note.on_image?
- if show_image_comment_badge && note_counter == 0
-# Only show this for the first comment in the discussion
- %span.image-comment-badge.inverted
- = icon('comment-o')
+ %span.image-comment-badge
+ = sprite_icon('image-comment-dark')
- elsif note_counter == 0
- counter = badge_counter if local_assigns[:badge_counter]
- badge_class = "hidden" if @fresh_discussion || counter.nil?
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 003f5fa52eb..63aa4e29ec9 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -52,7 +52,7 @@
= render_project_pipeline_status(project.pipeline_status)
- if forks
%span.prepend-left-10
- = icon('code-fork')
+ = sprite_icon('fork')
= number_with_delimiter(project.forks_count)
- if stars
%span.prepend-left-10
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
deleted file mode 100644
index 87e8c416194..00000000000
--- a/app/views/shared/repo/_repo.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- @no_container = true;
-#repo{ data: { root: @path.empty?.to_s,
- root_url: project_tree_path(project),
- url: content_url,
- current_branch: @ref,
- ref: @commit.id,
- project_name: project.name,
- project_url: project_path(project),
- project_id: project.id,
- new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),
- can_commit: (!!can_push_branch?(project, @ref)).to_s,
- on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
- current_path: @path } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
new file mode 100644
index 00000000000..50e876b1d19
--- /dev/null
+++ b/app/workers/all_queues.yml
@@ -0,0 +1,101 @@
+---
+- cronjob:admin_email
+- cronjob:expire_build_artifacts
+- cronjob:gitlab_usage_ping
+- cronjob:import_export_project_cleanup
+- cronjob:pipeline_schedule
+- cronjob:prune_old_events
+- cronjob:remove_expired_group_links
+- cronjob:remove_expired_members
+- cronjob:remove_old_web_hook_logs
+- cronjob:remove_unreferenced_lfs_objects
+- cronjob:repository_archive_cache
+- cronjob:repository_check_batch
+- cronjob:requests_profiles
+- cronjob:schedule_update_user_activity
+- cronjob:stuck_ci_jobs
+- cronjob:stuck_import_jobs
+- cronjob:stuck_merge_jobs
+- cronjob:trending_projects
+
+- 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
+
+- 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_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_pull_requests
+- github_importer:github_import_stage_import_repository
+
+- pipeline_cache:expire_job_cache
+- pipeline_cache:expire_pipeline_cache
+- pipeline_creation:create_pipeline
+- pipeline_creation:run_pipeline_schedule
+- pipeline_default:build_coverage
+- pipeline_default:build_trace_sections
+- pipeline_default:pipeline_metrics
+- pipeline_default:pipeline_notification
+- pipeline_default:update_head_pipeline_for_merge_request
+- pipeline_hooks:build_hooks
+- pipeline_hooks:pipeline_hooks
+- pipeline_processing:build_finished
+- pipeline_processing:build_queue
+- pipeline_processing:build_success
+- pipeline_processing:pipeline_process
+- pipeline_processing:pipeline_success
+- pipeline_processing:pipeline_update
+- pipeline_processing:stage_update
+
+- repository_check:repository_check_clear
+- repository_check:repository_check_single_repository
+
+- default
+- mailers # ActionMailer::DeliveryJob.queue_name
+
+- authorized_projects
+- background_migration
+- create_gpg_signature
+- delete_merged_branches
+- delete_user
+- email_receiver
+- emails_on_push
+- expire_build_instance_artifacts
+- git_garbage_collect
+- gitlab_shell
+- group_destroy
+- invalid_gpg_signature_update
+- irker
+- merge
+- namespaceless_project_destroy
+- new_issue
+- new_merge_request
+- new_note
+- pages
+- post_receive
+- process_commit
+- project_cache
+- project_destroy
+- project_export
+- project_migrate_hashed_storage
+- project_service
+- propagate_service_template
+- reactive_caching
+- rebase
+- repository_fork
+- repository_import
+- storage_migrator
+- system_hook_push
+- update_merge_requests
+- update_user_activity
+- upload_checksum
+- web_hook
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
index aeb3bc019b9..376703f6319 100644
--- a/app/workers/background_migration_worker.rb
+++ b/app/workers/background_migration_worker.rb
@@ -1,10 +1,53 @@
class BackgroundMigrationWorker
include ApplicationWorker
+ # The minimum amount of time between processing two jobs of the same migration
+ # class.
+ #
+ # This interval is set to 5 minutes so autovacuuming and other maintenance
+ # related tasks have plenty of time to clean up after a migration has been
+ # performed.
+ MIN_INTERVAL = 5.minutes.to_i
+
# Performs the background migration.
#
# See Gitlab::BackgroundMigration.perform for more information.
+ #
+ # class_name - The class name of the background migration to run.
+ # arguments - The arguments to pass to the migration class.
def perform(class_name, arguments = [])
- Gitlab::BackgroundMigration.perform(class_name, arguments)
+ should_perform, ttl = perform_and_ttl(class_name)
+
+ if should_perform
+ Gitlab::BackgroundMigration.perform(class_name, arguments)
+ else
+ # If the lease could not be obtained this means either another process is
+ # running a migration of this class or we ran one recently. In this case
+ # we'll reschedule the job in such a way that it is picked up again around
+ # the time the lease expires.
+ self.class.perform_in(ttl || MIN_INTERVAL, class_name, arguments)
+ end
+ end
+
+ def perform_and_ttl(class_name)
+ if always_perform?
+ # In test environments `perform_in` will run right away. This can then
+ # lead to stack level errors in the above `#perform`. To work around this
+ # we'll just perform the migration right away in the test environment.
+ [true, nil]
+ else
+ lease = lease_for(class_name)
+
+ [lease.try_obtain, lease.ttl]
+ end
+ end
+
+ def lease_for(class_name)
+ Gitlab::ExclusiveLease
+ .new("#{self.class.name}:#{class_name}", timeout: MIN_INTERVAL)
+ end
+
+ def always_perform?
+ Rails.env.test?
end
end
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 5efa9180f5e..97d80305bec 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -2,7 +2,7 @@ class BuildFinishedWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index 6705a1c2709..cbfca8c342c 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -2,7 +2,7 @@ class BuildHooksWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :hooks
+ queue_namespace :pipeline_hooks
def perform(build_id)
Ci::Build.find_by(id: build_id)
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index fc775a84dc0..e4f4e6c1d9e 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -2,7 +2,7 @@ class BuildQueueWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index ec049821ad7..4b9097bc5e4 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -2,7 +2,7 @@ class BuildSuccessWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/check_gcp_project_billing_worker.rb b/app/workers/check_gcp_project_billing_worker.rb
new file mode 100644
index 00000000000..557af14ee57
--- /dev/null
+++ b/app/workers/check_gcp_project_billing_worker.rb
@@ -0,0 +1,59 @@
+require 'securerandom'
+
+class CheckGcpProjectBillingWorker
+ include ApplicationWorker
+ include ClusterQueue
+
+ LEASE_TIMEOUT = 15.seconds.to_i
+ SESSION_KEY_TIMEOUT = 5.minutes
+ BILLING_TIMEOUT = 1.hour
+
+ 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.redis_shared_state_key_for(token)
+ "gitlab:gcp:#{token.hash}:billing_enabled"
+ end
+
+ def perform(token_key)
+ return unless token_key
+
+ token = self.get_session_token(token_key)
+ return unless token
+ return unless try_obtain_lease_for(token)
+
+ billing_enabled_projects = CheckGcpProjectBillingService.new.execute(token)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(self.class.redis_shared_state_key_for(token),
+ !billing_enabled_projects.empty?,
+ ex: BILLING_TIMEOUT)
+ end
+ 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 try_obtain_lease_for(token)
+ Gitlab::ExclusiveLease
+ .new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
+ .try_obtain
+ end
+end
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 9c3bdabc49e..37586e161c9 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -3,13 +3,23 @@ Sidekiq::Worker.extend ActiveSupport::Concern
module ApplicationWorker
extend ActiveSupport::Concern
- include Sidekiq::Worker
+ include Sidekiq::Worker # rubocop:disable Cop/IncludeSidekiqWorker
included do
- sidekiq_options queue: base_queue_name
+ set_queue
end
module ClassMethods
+ def inherited(subclass)
+ subclass.set_queue
+ end
+
+ def set_queue
+ queue_name = [queue_namespace, base_queue_name].compact.join(':')
+
+ sidekiq_options queue: queue_name # rubocop:disable Cop/SidekiqOptionsQueue
+ end
+
def base_queue_name
name
.sub(/\AGitlab::/, '')
@@ -18,6 +28,16 @@ module ApplicationWorker
.tr('/', '_')
end
+ def queue_namespace(new_namespace = nil)
+ if new_namespace
+ sidekiq_options queue_namespace: new_namespace
+
+ set_queue
+ else
+ get_sidekiq_options['queue_namespace']&.to_s
+ end
+ end
+
def queue
get_sidekiq_options['queue'].to_s
end
diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb
index a5074d13220..24b9f145220 100644
--- a/app/workers/concerns/cluster_queue.rb
+++ b/app/workers/concerns/cluster_queue.rb
@@ -5,6 +5,6 @@ module ClusterQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: :gcp_cluster
+ queue_namespace :gcp_cluster
end
end
diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb
index e918bb011e0..b6581779f6a 100644
--- a/app/workers/concerns/cronjob_queue.rb
+++ b/app/workers/concerns/cronjob_queue.rb
@@ -4,6 +4,7 @@ module CronjobQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: :cronjob, retry: false
+ queue_namespace :cronjob
+ sidekiq_options retry: false
end
end
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
index a2bee361b86..22c2ce458e8 100644
--- a/app/workers/concerns/gitlab/github_import/queue.rb
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -4,12 +4,14 @@ module Gitlab
extend ActiveSupport::Concern
included do
+ queue_namespace :github_importer
+
# If a job produces an error it may block a stage from advancing
# forever. To prevent this from happening we prevent jobs from going to
# the dead queue. This does mean some resources may not be imported, but
# this is better than a project being stuck in the "import" state
# forever.
- sidekiq_options queue: 'github_importer', dead: false, retry: 5
+ sidekiq_options dead: false, retry: 5
end
end
end
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index eb0d6c9c36c..526ed0bad07 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -9,15 +9,15 @@ module NewIssuable
end
def set_user(user_id)
- @user = User.find_by(id: user_id)
+ @user = User.find_by(id: user_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- log_error(User, user_id) unless @user
+ log_error(User, user_id) unless @user # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def set_issuable(issuable_id)
- @issuable = issuable_class.find_by(id: issuable_id)
+ @issuable = issuable_class.find_by(id: issuable_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- log_error(issuable_class, issuable_id) unless @issuable
+ log_error(issuable_class, issuable_id) unless @issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def log_error(record_class, record_id)
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
index ddf45b91345..e77093a6902 100644
--- a/app/workers/concerns/pipeline_queue.rb
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -5,14 +5,6 @@ module PipelineQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: 'pipeline_default'
- end
-
- class_methods do
- def enqueue_in(group:)
- raise ArgumentError, 'Unspecified queue group!' if group.empty?
-
- sidekiq_options queue: "pipeline_#{group}"
- end
+ queue_namespace :pipeline_default
end
end
diff --git a/app/workers/concerns/project_import_options.rb b/app/workers/concerns/project_import_options.rb
new file mode 100644
index 00000000000..10b971344f7
--- /dev/null
+++ b/app/workers/concerns/project_import_options.rb
@@ -0,0 +1,23 @@
+module ProjectImportOptions
+ extend ActiveSupport::Concern
+
+ included do
+ IMPORT_RETRY_COUNT = 5
+
+ sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+
+ # We only want to mark the project as failed once we exhausted all retries
+ sidekiq_retries_exhausted do |job|
+ project = Project.find(job['args'].first)
+
+ action = if project.forked?
+ "fork"
+ else
+ "import"
+ end
+
+ project.mark_import_as_failed("Every #{action} attempt has failed: #{job['error_message']}. Please try again.")
+ Sidekiq.logger.warn "Failed #{job['class']} with #{job['args']}: #{job['error_message']}"
+ end
+ end
+end
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
index 0704ebbb0fd..4e55a1ee3d6 100644
--- a/app/workers/concerns/project_start_import.rb
+++ b/app/workers/concerns/project_start_import.rb
@@ -1,3 +1,4 @@
+# Used in EE by mirroring
module ProjectStartImport
def start(project)
if project.import_started? && project.import_jid == self.jid
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
index a597321ccf4..43fb66c31b0 100644
--- a/app/workers/concerns/repository_check_queue.rb
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -3,6 +3,8 @@ module RepositoryCheckQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: :repository_check, retry: false
+ queue_namespace :repository_check
+
+ sidekiq_options retry: false
end
end
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index 00cd7b85b9f..c3ac35e54f5 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -2,7 +2,7 @@ class CreatePipelineWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :creation
+ queue_namespace :pipeline_creation
def perform(project_id, user_id, ref, source, params = {})
project = Project.find(project_id)
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index a591e2da519..7217364a9f2 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -2,7 +2,7 @@ class ExpireJobCacheWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :cache
+ queue_namespace :pipeline_cache
def perform(job_id)
job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id)
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index a3ac32b437d..db73d37868a 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -2,7 +2,7 @@ class ExpirePipelineCacheWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :cache
+ queue_namespace :pipeline_cache
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
@@ -13,7 +13,7 @@ class ExpirePipelineCacheWorker
store.touch(project_pipelines_path(project))
store.touch(project_pipeline_path(project, pipeline))
- store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit
+ store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil?
store.touch(new_merge_request_pipelines_path(project))
each_pipelines_merge_request_path(project, pipeline) do |path|
store.touch(path)
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 400396d5755..f7f498af840 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -9,7 +9,7 @@ module Gitlab
class AdvanceStageWorker
include ApplicationWorker
- sidekiq_options queue: 'github_importer_advance_stage', dead: false
+ sidekiq_options dead: false
INTERVAL = 30.seconds.to_i
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 62f733c02fc..3ec81d040b4 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -1,7 +1,7 @@
class PagesWorker
include ApplicationWorker
- sidekiq_options queue: :pages, retry: false
+ sidekiq_options retry: false
def perform(action, *arg)
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index 661c29efe88..c94918ff4ee 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -2,7 +2,7 @@ class PipelineHooksWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :hooks
+ queue_namespace :pipeline_hooks
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 07dbf6a971e..24424b3f472 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -2,7 +2,7 @@ class PipelineProcessWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 68c40a259e1..2ab0739a17f 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -2,7 +2,7 @@ class PipelineSuccessWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 24a8a9fbed5..fc9da2d45b1 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -2,7 +2,7 @@ class PipelineUpdateWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb
new file mode 100644
index 00000000000..090987778a2
--- /dev/null
+++ b/app/workers/rebase_worker.rb
@@ -0,0 +1,12 @@
+class RebaseWorker
+ include ApplicationWorker
+
+ def perform(merge_request_id, current_user_id)
+ current_user = User.find(current_user_id)
+ merge_request = MergeRequest.find(merge_request_id)
+
+ MergeRequests::RebaseService
+ .new(merge_request.source_project, current_user)
+ .execute(merge_request)
+ end
+end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index a07ef1705a1..d1c57b82681 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,11 +1,8 @@
class RepositoryForkWorker
- ForkError = Class.new(StandardError)
-
include ApplicationWorker
include Gitlab::ShellAdapter
include ProjectStartImport
-
- sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ include ProjectImportOptions
def perform(project_id, forked_from_repository_storage_path, source_disk_path)
project = Project.find(project_id)
@@ -18,20 +15,12 @@ class RepositoryForkWorker
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_disk_path,
project.repository_storage_path, project.disk_path)
- raise ForkError, "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
+ raise "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
project.repository.after_import
- raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
+ raise "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
project.import_finish
- rescue ForkError => ex
- fail_fork(project, ex.message)
- raise
- rescue => ex
- return unless project
-
- fail_fork(project, ex.message)
- raise ForkError, "#{ex.class} #{ex.message}"
end
private
@@ -42,9 +31,4 @@ class RepositoryForkWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
end
-
- def fail_fork(project, message)
- Rails.logger.error(message)
- project.mark_import_as_failed(message)
- end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 55715c83cb1..31e2798c36b 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,11 +1,8 @@
class RepositoryImportWorker
- ImportError = Class.new(StandardError)
-
include ApplicationWorker
include ExceptionBacktrace
include ProjectStartImport
-
- sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ include ProjectImportOptions
def perform(project_id)
project = Project.find(project_id)
@@ -23,17 +20,9 @@ class RepositoryImportWorker
# to those importers to mark the import process as complete.
return if service.async?
- raise ImportError, result[:message] if result[:status] == :error
+ raise result[:message] if result[:status] == :error
project.after_import
- rescue ImportError => ex
- fail_import(project, ex.message)
- raise
- rescue => ex
- return unless project
-
- fail_import(project, ex.message)
- raise ImportError, "#{ex.class} #{ex.message}"
end
private
@@ -44,8 +33,4 @@ class RepositoryImportWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
end
-
- def fail_import(project, message)
- project.mark_import_as_failed(message)
- end
end
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
new file mode 100644
index 00000000000..8f5138fc873
--- /dev/null
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -0,0 +1,22 @@
+class RunPipelineScheduleWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ queue_namespace :pipeline_creation
+
+ def perform(schedule_id, user_id)
+ schedule = Ci::PipelineSchedule.find_by(id: schedule_id)
+ user = User.find_by(id: user_id)
+
+ return unless schedule && user
+
+ run_pipeline_schedule(schedule, user)
+ end
+
+ def run_pipeline_schedule(schedule, user)
+ Ci::CreatePipelineService.new(schedule.project,
+ user,
+ ref: schedule.ref)
+ .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ end
+end
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index 69f2318d83b..e4b683fca33 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -2,7 +2,7 @@ class StageUpdateWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(stage_id)
Ci::Stage.find_by(id: stage_id).try do |stage|
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 36d2a2e6466..16394293c79 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -23,7 +23,12 @@ class StuckMergeJobsWorker
merge_requests = MergeRequest.where(id: completed_ids)
merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged)
- merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil)
+
+ merge_requests_to_reopen = merge_requests.where(merge_commit_sha: nil)
+
+ # Do not reopen merge requests using direct queries.
+ # We rely on state machine callbacks to update head_pipeline_id
+ merge_requests_to_reopen.each(&:unlock_mr)
Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end
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 0a2e9b63578..f09d89aa170 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -1,15 +1,25 @@
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
-
- sidekiq_options queue: 'pipeline_default'
+ include PipelineQueue
def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id)
pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
return unless pipeline && pipeline.latest?
- raise ArgumentError, 'merge request sha does not equal pipeline sha' if merge_request.diff_head_sha != pipeline.sha
+
+ if merge_request.diff_head_sha != pipeline.sha
+ log_error_message_for(merge_request)
+
+ return
+ end
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
+
+ def log_error_message_for(merge_request)
+ Rails.logger.error(
+ "Outdated head pipeline for active merge request: id=#{merge_request.id}, source_branch=#{merge_request.source_branch}, diff_head_sha=#{merge_request.diff_head_sha}"
+ )
+ end
end
diff --git a/changelogs/unreleased/13634-broadcast-message.yml b/changelogs/unreleased/13634-broadcast-message.yml
deleted file mode 100644
index 26c4c133443..00000000000
--- a/changelogs/unreleased/13634-broadcast-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broadcast message not showing up on login page
-merge_request: 15578
-author:
-type: fixed
diff --git a/changelogs/unreleased/13695-order-contributors-in-api.yml b/changelogs/unreleased/13695-order-contributors-in-api.yml
new file mode 100644
index 00000000000..26bf8650a4a
--- /dev/null
+++ b/changelogs/unreleased/13695-order-contributors-in-api.yml
@@ -0,0 +1,5 @@
+---
+title: Adds ordering to projects contributors in API
+merge_request: 15469
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml b/changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml
deleted file mode 100644
index a4934c8896d..00000000000
--- a/changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Initializes the branches dropdown when the 'Start new pipeline' failed due to validation errors
-merge_request: 15588
-author: Christiaan Van den Poel
-type: fixed
diff --git a/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml b/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml
deleted file mode 100644
index 1179b3f20e6..00000000000
--- a/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'fix #39233 - 500 in merge request'
-merge_request: 15774
-author: Martin Nowak
-type: fixed
diff --git a/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml b/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml
new file mode 100644
index 00000000000..9d6c958cb3e
--- /dev/null
+++ b/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error that was preventing users to change the access level of access requests for Groups or Projects
+merge_request: 15832
+author:
+type: fixed
diff --git a/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml b/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml
new file mode 100644
index 00000000000..db2bd6e692b
--- /dev/null
+++ b/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml
@@ -0,0 +1,5 @@
+---
+title: 'Validate file status when commiting multiple files'
+merge_request: 15922
+author:
+type: added
diff --git a/changelogs/unreleased/15955-improve-search-query.yml b/changelogs/unreleased/15955-improve-search-query.yml
new file mode 100644
index 00000000000..80cb8af617f
--- /dev/null
+++ b/changelogs/unreleased/15955-improve-search-query.yml
@@ -0,0 +1,5 @@
+---
+title: Improve search query for merge requests.
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/16036-ignore-lost-found-folder-during-backup-on-a-volume.yml b/changelogs/unreleased/16036-ignore-lost-found-folder-during-backup-on-a-volume.yml
new file mode 100644
index 00000000000..833650559a3
--- /dev/null
+++ b/changelogs/unreleased/16036-ignore-lost-found-folder-during-backup-on-a-volume.yml
@@ -0,0 +1,5 @@
+---
+title: "Ignore lost+found folder during backup on a volume"
+merge_request: 16036
+author: Julien Millau
+type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/39727-add-axios-to-common.yml b/changelogs/unreleased/16117-improve-search-for-issues.yml
index 688757d2486..92d5820ddd2 100644
--- a/changelogs/unreleased/39727-add-axios-to-common.yml
+++ b/changelogs/unreleased/16117-improve-search-for-issues.yml
@@ -1,5 +1,5 @@
---
-title: Add axios to common file
+title: Improve search query for issues.
merge_request:
author:
type: performance
diff --git a/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml
deleted file mode 100644
index e3c7ffc8046..00000000000
--- a/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds Rubocop rule for line break after guard clause
-merge_request: 15188
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/20035-pause-resume-runners.yml b/changelogs/unreleased/20035-pause-resume-runners.yml
new file mode 100644
index 00000000000..98757e60683
--- /dev/null
+++ b/changelogs/unreleased/20035-pause-resume-runners.yml
@@ -0,0 +1,5 @@
+---
+title: Add pause/resume button to project runners
+merge_request: 16032
+author: Mario de la Ossa
+type: added
diff --git a/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml b/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml
deleted file mode 100644
index d4c209aaf2b..00000000000
--- a/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add an ability to use a custom branch name on creation from issues
-merge_request: 13884
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml b/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
deleted file mode 100644
index 6d7f8655282..00000000000
--- a/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Limit autocomplete menu to applied labels
-merge_request: 11110
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/24347-dont-post-system-note-when-branch-creation-fails.yml b/changelogs/unreleased/24347-dont-post-system-note-when-branch-creation-fails.yml
new file mode 100644
index 00000000000..61153ad4f1a
--- /dev/null
+++ b/changelogs/unreleased/24347-dont-post-system-note-when-branch-creation-fails.yml
@@ -0,0 +1,5 @@
+---
+title: Fix when branch creation fails don't post system note
+merge_request:
+author: Mateusz Bajorski
+type: fixed
diff --git a/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml b/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml
new file mode 100644
index 00000000000..a5f6d316a7d
--- /dev/null
+++ b/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Show authored date rather than committed date on the commit list
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml b/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml
new file mode 100644
index 00000000000..0e91d4ae403
--- /dev/null
+++ b/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor member view using a Presenter
+merge_request: 9645
+author: TM Lee
diff --git a/changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml b/changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml
deleted file mode 100644
index b6646379b8d..00000000000
--- a/changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add edit button to mobile file view
-merge_request: 15199
-author: Travis Miller
-type: added
diff --git a/changelogs/unreleased/31995-project-limit-default-fix.yml b/changelogs/unreleased/31995-project-limit-default-fix.yml
new file mode 100644
index 00000000000..4f25eb34b45
--- /dev/null
+++ b/changelogs/unreleased/31995-project-limit-default-fix.yml
@@ -0,0 +1,5 @@
+---
+title: User#projects_limit remove DB default and added NOT NULL constraint
+merge_request: 16165
+author: Mario de la Ossa
+type: fixed
diff --git a/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml b/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml
deleted file mode 100644
index cd19d24e035..00000000000
--- a/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: added support for ordering and sorting in notes api
-merge_request: 15342
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/32098-pipelines-navigation.yml b/changelogs/unreleased/32098-pipelines-navigation.yml
deleted file mode 100644
index 925c92b6be8..00000000000
--- a/changelogs/unreleased/32098-pipelines-navigation.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Stop reloading the page when using pagination and tabs - use API calls - in
- Pipelines table
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/32364-updating-slack-notification-not-working-by-api.yml b/changelogs/unreleased/32364-updating-slack-notification-not-working-by-api.yml
new file mode 100644
index 00000000000..e3fae55c6f0
--- /dev/null
+++ b/changelogs/unreleased/32364-updating-slack-notification-not-working-by-api.yml
@@ -0,0 +1,5 @@
+---
+title: Support new chat notifications parameters in Services API
+merge_request: 11435
+author:
+type: added
diff --git a/changelogs/unreleased/32878-merge-request-from-email.yml b/changelogs/unreleased/32878-merge-request-from-email.yml
deleted file mode 100644
index 2df148d5a81..00000000000
--- a/changelogs/unreleased/32878-merge-request-from-email.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow creation of merge request from email
-merge_request: 13817
-author: janp
-type: added
diff --git a/changelogs/unreleased/33028-event-tag-links.yml b/changelogs/unreleased/33028-event-tag-links.yml
new file mode 100644
index 00000000000..1d674200dcd
--- /dev/null
+++ b/changelogs/unreleased/33028-event-tag-links.yml
@@ -0,0 +1,5 @@
+---
+title: Fix tags in the Activity tab not being clickable
+merge_request: 15996
+author: Mario de la Ossa
+type: fixed
diff --git a/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml b/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml
deleted file mode 100644
index d61bbf2e355..00000000000
--- a/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add internationalization support for the prometheus integration
-merge_request: 33338
-author:
-type: other
diff --git a/changelogs/unreleased/33609-hide-pagination.yml b/changelogs/unreleased/33609-hide-pagination.yml
new file mode 100644
index 00000000000..3586b091cb1
--- /dev/null
+++ b/changelogs/unreleased/33609-hide-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Disable Vue pagination when only one page of content is available
+merge_request: 15999
+author: Mario de la Ossa
+type: fixed
diff --git a/changelogs/unreleased/33926-update-issuable-icons.yml b/changelogs/unreleased/33926-update-issuable-icons.yml
new file mode 100644
index 00000000000..87076dde545
--- /dev/null
+++ b/changelogs/unreleased/33926-update-issuable-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Update issuable status icons
+merge_request: 15898
+author:
+type: changed
diff --git a/changelogs/unreleased/34534-switch-to-axios.yml b/changelogs/unreleased/34534-switch-to-axios.yml
new file mode 100644
index 00000000000..1200272c9eb
--- /dev/null
+++ b/changelogs/unreleased/34534-switch-to-axios.yml
@@ -0,0 +1,5 @@
+---
+title: Fix some POST/DELETE requests in IE by switching some bundles to Axios for Ajax requests
+merge_request: 15951
+author:
+type: fixed
diff --git a/changelogs/unreleased/34600-performance-wiki-pages.yml b/changelogs/unreleased/34600-performance-wiki-pages.yml
deleted file mode 100644
index 541ae8f8e60..00000000000
--- a/changelogs/unreleased/34600-performance-wiki-pages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Performance issues when loading large number of wiki pages
-merge_request: 15276
-author:
-type: performance
diff --git a/changelogs/unreleased/35616-move-k8-to-cluster-page.yml b/changelogs/unreleased/35616-move-k8-to-cluster-page.yml
deleted file mode 100644
index 032a39608ce..00000000000
--- a/changelogs/unreleased/35616-move-k8-to-cluster-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create a new form to add Existing Kubernetes Cluster
-merge_request: 14805
-author:
-type: added
diff --git a/changelogs/unreleased/36020-private-npm-modules.yml b/changelogs/unreleased/36020-private-npm-modules.yml
new file mode 100644
index 00000000000..5c2585a602e
--- /dev/null
+++ b/changelogs/unreleased/36020-private-npm-modules.yml
@@ -0,0 +1,5 @@
+---
+title: Do not generate NPM links for private NPM modules in blob view
+merge_request: 16002
+author: Mario de la Ossa
+type: added
diff --git a/changelogs/unreleased/36400-trigger-job.yml b/changelogs/unreleased/36400-trigger-job.yml
deleted file mode 100644
index 27243813dc8..00000000000
--- a/changelogs/unreleased/36400-trigger-job.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent 500 error when inspecting job after trigger was removed
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36782-replace-team-user-role-with-add_role-user-in-specs.yml b/changelogs/unreleased/36782-replace-team-user-role-with-add_role-user-in-specs.yml
new file mode 100644
index 00000000000..8773ac73a75
--- /dev/null
+++ b/changelogs/unreleased/36782-replace-team-user-role-with-add_role-user-in-specs.yml
@@ -0,0 +1,5 @@
+---
+title: Replace '.team << [user, role]' with 'add_role(user)' in specs
+merge_request: 16069
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml b/changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml
new file mode 100644
index 00000000000..8348e3e8ceb
--- /dev/null
+++ b/changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml
@@ -0,0 +1,5 @@
+---
+title: Enable ordering of groups and their children by name
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/37354-pipelines-update.yml b/changelogs/unreleased/37354-pipelines-update.yml
deleted file mode 100644
index 2b6ddfe95ed..00000000000
--- a/changelogs/unreleased/37354-pipelines-update.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make sure head pippeline always corresponds with the head sha of an MR
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37843-ci-trace-ansi-colours-256-bold-have-no-css-due-wrongly-ansi2html-light-color-variant-conversion-feature.yml b/changelogs/unreleased/37843-ci-trace-ansi-colours-256-bold-have-no-css-due-wrongly-ansi2html-light-color-variant-conversion-feature.yml
new file mode 100644
index 00000000000..abf98cd2af4
--- /dev/null
+++ b/changelogs/unreleased/37843-ci-trace-ansi-colours-256-bold-have-no-css-due-wrongly-ansi2html-light-color-variant-conversion-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Fix ANSI 256 bold colors in pipelines job output
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38019-hide-runner-token.yml b/changelogs/unreleased/38019-hide-runner-token.yml
new file mode 100644
index 00000000000..11ae0a685ef
--- /dev/null
+++ b/changelogs/unreleased/38019-hide-runner-token.yml
@@ -0,0 +1,5 @@
+---
+title: Hide runner token in CI/CD settings page
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/38030-add-graph-value-to-hover.yml b/changelogs/unreleased/38030-add-graph-value-to-hover.yml
new file mode 100644
index 00000000000..233db2b19c9
--- /dev/null
+++ b/changelogs/unreleased/38030-add-graph-value-to-hover.yml
@@ -0,0 +1,5 @@
+---
+title: Display graph values on hover within monitoring page
+merge_request: 16261
+author:
+type: changed
diff --git a/changelogs/unreleased/38075_allow_refernce_integer_labels.yml b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
deleted file mode 100644
index b5342d4adf8..00000000000
--- a/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix errors when selecting numeric-only labels in the labels autocomplete selector
-merge_request: 14607
-author: haseebeqx
-type: fixed
diff --git a/changelogs/unreleased/38145_ux_issues_in_system_info_page.yml b/changelogs/unreleased/38145_ux_issues_in_system_info_page.yml
new file mode 100644
index 00000000000..d2358750518
--- /dev/null
+++ b/changelogs/unreleased/38145_ux_issues_in_system_info_page.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes the wording of headers in system info page
+merge_request: 15802
+author: Gilbert Roulot
+type: fixed
diff --git a/changelogs/unreleased/38239-update-toggle-design.yml b/changelogs/unreleased/38239-update-toggle-design.yml
new file mode 100644
index 00000000000..4d9034e8515
--- /dev/null
+++ b/changelogs/unreleased/38239-update-toggle-design.yml
@@ -0,0 +1,5 @@
+---
+title: Update feature toggle design to use icons and make it i18n friendly
+merge_request: 15904
+author:
+type: changed
diff --git a/changelogs/unreleased/38318-search-merge-requests-with-api.yml b/changelogs/unreleased/38318-search-merge-requests-with-api.yml
new file mode 100644
index 00000000000..d8b2f1f25c8
--- /dev/null
+++ b/changelogs/unreleased/38318-search-merge-requests-with-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add optional search param for Merge Requests API
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml
deleted file mode 100644
index c73cf8bf60b..00000000000
--- a/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changed validation error message on wrong milestone dates
-merge_request:
-author: Xurxo Méndez Pérez
-type: fixed
diff --git a/changelogs/unreleased/38541-cancel-alignment.yml b/changelogs/unreleased/38541-cancel-alignment.yml
new file mode 100644
index 00000000000..c6d5136dd57
--- /dev/null
+++ b/changelogs/unreleased/38541-cancel-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: fix button alignment on MWPS component
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38596-fix-backspace-visual-token-clearing.yml b/changelogs/unreleased/38596-fix-backspace-visual-token-clearing.yml
new file mode 100644
index 00000000000..4a9d0b66a8c
--- /dev/null
+++ b/changelogs/unreleased/38596-fix-backspace-visual-token-clearing.yml
@@ -0,0 +1,5 @@
+---
+title: Clears visual token on second backspace
+merge_request:
+author: Martin Wortschack
+type: fixed
diff --git a/changelogs/unreleased/38822-oauth-search-case-insensitive.yml b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml
deleted file mode 100644
index d84360b4c5c..00000000000
--- a/changelogs/unreleased/38822-oauth-search-case-insensitive.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: OAuth identity lookups case-insensitive
-merge_request: 15312
-author:
-type: fixed
diff --git a/changelogs/unreleased/38862-email-notifications-not-sent-as-expected.yml b/changelogs/unreleased/38862-email-notifications-not-sent-as-expected.yml
deleted file mode 100644
index 6b1b309ab14..00000000000
--- a/changelogs/unreleased/38862-email-notifications-not-sent-as-expected.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix sending notification emails to users with the mention level set who were
- mentioned in an issue or merge request description
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38869-templates.yml b/changelogs/unreleased/38869-templates.yml
deleted file mode 100644
index 957b5f27bd0..00000000000
--- a/changelogs/unreleased/38869-templates.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove template selector from global namespace
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml b/changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml
deleted file mode 100644
index 07439a860ec..00000000000
--- a/changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disables autocomplete in filtered searc
-merge_request: 15477
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/38893-banzai-upload-filter-relative-urls.yml b/changelogs/unreleased/38893-banzai-upload-filter-relative-urls.yml
new file mode 100644
index 00000000000..9ab0a0159e9
--- /dev/null
+++ b/changelogs/unreleased/38893-banzai-upload-filter-relative-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Use relative URLs when linking to uploaded files
+merge_request: 15751
+author:
+type: other
diff --git a/changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml b/changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml
deleted file mode 100644
index a4d703bc69f..00000000000
--- a/changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add the option to automatically run a pipeline after updating AutoDevOps settings
-merge_request: 15380
-author:
-type: changed
diff --git a/changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml b/changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml
new file mode 100644
index 00000000000..ce238a2c79f
--- /dev/null
+++ b/changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml
@@ -0,0 +1,5 @@
+---
+title: Only mark import and fork jobs as failed once all Sidekiq retries get exhausted
+merge_request: 15844
+author:
+type: changed
diff --git a/changelogs/unreleased/39298-list-of-avatars-2.yml b/changelogs/unreleased/39298-list-of-avatars-2.yml
new file mode 100644
index 00000000000..e2095561c0e
--- /dev/null
+++ b/changelogs/unreleased/39298-list-of-avatars-2.yml
@@ -0,0 +1,5 @@
+---
+title: List of avatars should never show +1
+merge_request: 15972
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/39335-add-time-spend-to-milestones.yml b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
deleted file mode 100644
index 41a43418cbf..00000000000
--- a/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add total time spent to milestones
-merge_request: 15116
-author: George Andrinopoulos
-type: added
diff --git a/changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml b/changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml
deleted file mode 100644
index 9793c6a8e9c..00000000000
--- a/changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change boards page boards_data absolute urls to paths
-merge_request: 15703
-author:
-type: fixed
diff --git a/changelogs/unreleased/39367-fix-new-email-session-path.yml b/changelogs/unreleased/39367-fix-new-email-session-path.yml
deleted file mode 100644
index 73485d9d1a9..00000000000
--- a/changelogs/unreleased/39367-fix-new-email-session-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Confirming email with invalid token should no longer generate an error
-merge_request: 15726
-author:
-type: fixed
diff --git a/changelogs/unreleased/39436-pages-api-administrative.yml b/changelogs/unreleased/39436-pages-api-administrative.yml
deleted file mode 100644
index f38bbbd479c..00000000000
--- a/changelogs/unreleased/39436-pages-api-administrative.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add administrative endpoint to list all pages domains
-merge_request: 15160
-author: Travis Miller
-type: added
diff --git a/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml b/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml
deleted file mode 100644
index cb522cb7611..00000000000
--- a/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed tooltip from clone dropdown
-merge_request: 15334
-author:
-type: other
diff --git a/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml
deleted file mode 100644
index fc7c024f95a..00000000000
--- a/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add inline editing to issues on mobile
-merge_request: 15438
-author:
-type: changed
diff --git a/changelogs/unreleased/39601-create-issuable-destroy-service.yml b/changelogs/unreleased/39601-create-issuable-destroy-service.yml
deleted file mode 100644
index b0463f02eba..00000000000
--- a/changelogs/unreleased/39601-create-issuable-destroy-service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create issuable destroy service
-merge_request: 15604
-author: George Andrinopoulos
-type: other
diff --git a/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
deleted file mode 100644
index 056afe43010..00000000000
--- a/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move update_project_counter_caches? out of issue and merge request
-merge_request: 15300
-author: George Andrinopoulos
-type: other
diff --git a/changelogs/unreleased/39608-comment-on-image-discussions-tab-alignment.yml b/changelogs/unreleased/39608-comment-on-image-discussions-tab-alignment.yml
new file mode 100644
index 00000000000..5021fe88caf
--- /dev/null
+++ b/changelogs/unreleased/39608-comment-on-image-discussions-tab-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Update comment on image cursor and icons
+merge_request: 15760
+author:
+type: fixed
diff --git a/changelogs/unreleased/3968-protected-branch-is-not-set-for-default-branch-on-import.yml b/changelogs/unreleased/3968-protected-branch-is-not-set-for-default-branch-on-import.yml
new file mode 100644
index 00000000000..e972ac6d54a
--- /dev/null
+++ b/changelogs/unreleased/3968-protected-branch-is-not-set-for-default-branch-on-import.yml
@@ -0,0 +1,5 @@
+---
+title: Protected branch is now created for default branch on import
+merge_request: 16198
+author:
+type: fixed
diff --git a/changelogs/unreleased/39720-group-milestone-sorting.yml b/changelogs/unreleased/39720-group-milestone-sorting.yml
deleted file mode 100644
index 15ef87fa567..00000000000
--- a/changelogs/unreleased/39720-group-milestone-sorting.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add dropdown sort to group milestones
-merge_request: 15230
-author: George Andrinopoulos
-type: added
diff --git a/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml
deleted file mode 100644
index 8b27c43d15b..00000000000
--- a/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix commits page throwing 500 when the multi-file editor was enabled
-merge_request: 15502
-author:
-type: fixed
diff --git a/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml b/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml
deleted file mode 100644
index ebd7b582e8a..00000000000
--- a/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix item name and namespace text overflow in Projects dropdown
-merge_request: 15451
-author:
-type: fixed
diff --git a/changelogs/unreleased/39869_show_closed_status_of_links_to_issues_on_wiki_pages.yml b/changelogs/unreleased/39869_show_closed_status_of_links_to_issues_on_wiki_pages.yml
deleted file mode 100644
index cd31ed463a2..00000000000
--- a/changelogs/unreleased/39869_show_closed_status_of_links_to_issues_on_wiki_pages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: show status of gitlab reference links in wiki
-merge_request: 15694
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
deleted file mode 100644
index 580b97241e7..00000000000
--- a/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix pipeline status transition for single manual job. This would also fix pipeline
- duration becuse it is depending on status transition
-merge_request: 15251
-author:
-type: fixed
diff --git a/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml b/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
deleted file mode 100644
index 358c007387e..00000000000
--- a/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix acceptance of username for Mattermost service update
-merge_request: 15275
-author:
-type: fixed
diff --git a/changelogs/unreleased/39957-redirect-to-gpc-page-if-users-try-to-create-a-cluster-but-the-account-is-not-enabled.yml b/changelogs/unreleased/39957-redirect-to-gpc-page-if-users-try-to-create-a-cluster-but-the-account-is-not-enabled.yml
new file mode 100644
index 00000000000..d8fd1f14bd4
--- /dev/null
+++ b/changelogs/unreleased/39957-redirect-to-gpc-page-if-users-try-to-create-a-cluster-but-the-account-is-not-enabled.yml
@@ -0,0 +1,5 @@
+---
+title: Implement checking GCP project billing status in cluster creation form.
+merge_request: 15665
+author:
+type: changed
diff --git a/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml b/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml
deleted file mode 100644
index b7a974fd8d9..00000000000
--- a/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Set the default gitlab-shell timeout to 3 hours
-merge_request: 15292
-author:
-type: fixed
diff --git a/changelogs/unreleased/40016-log-header.yml b/changelogs/unreleased/40016-log-header.yml
deleted file mode 100644
index f52c2d2a0d5..00000000000
--- a/changelogs/unreleased/40016-log-header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide log size for mobile screens
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/40031-include-assset_sync-gem.yml b/changelogs/unreleased/40031-include-assset_sync-gem.yml
new file mode 100644
index 00000000000..93ce565b32c
--- /dev/null
+++ b/changelogs/unreleased/40031-include-assset_sync-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Add assets_sync gem to Gemfile
+merge_request: 15734
+author:
+type: added
diff --git a/changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml b/changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml
new file mode 100644
index 00000000000..e2fade2bfd9
--- /dev/null
+++ b/changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml
@@ -0,0 +1,5 @@
+---
+title: Adds the multi file editor as a new beta feature
+merge_request: 15430
+author:
+type: feature
diff --git a/changelogs/unreleased/40063-markdown-editor-improvements.yml b/changelogs/unreleased/40063-markdown-editor-improvements.yml
new file mode 100644
index 00000000000..fa2f09408b4
--- /dev/null
+++ b/changelogs/unreleased/40063-markdown-editor-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Hide markdown toolbar in preview mode
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml b/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml
deleted file mode 100644
index a2ae2059c47..00000000000
--- a/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add total_time_spent to the `changes` hash in issuable Webhook payloads
-merge_request: 15381
-author:
-type: changed
diff --git a/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml b/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml
deleted file mode 100644
index 00f7dd7c0f0..00000000000
--- a/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix search results when a filename would contain a special character.
-merge_request: 15606
-author: haseebeqx
-type: fixed
diff --git a/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml b/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml
deleted file mode 100644
index fdaa90f0d5d..00000000000
--- a/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove extra margin from wordmark in header
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/40190-fix-slash-commands-dropdown-description-mis-alignement-on-firefox.yml b/changelogs/unreleased/40190-fix-slash-commands-dropdown-description-mis-alignement-on-firefox.yml
new file mode 100644
index 00000000000..71a606ff607
--- /dev/null
+++ b/changelogs/unreleased/40190-fix-slash-commands-dropdown-description-mis-alignement-on-firefox.yml
@@ -0,0 +1,5 @@
+---
+title: "Fix slash commands dropdown description mis-alignment on Firefox"
+merge_request: 16125
+author: Maurizio De Santis
+type: fixed
diff --git a/changelogs/unreleased/40228-verify-integrity-of-repositories.yml b/changelogs/unreleased/40228-verify-integrity-of-repositories.yml
new file mode 100644
index 00000000000..261d48652db
--- /dev/null
+++ b/changelogs/unreleased/40228-verify-integrity-of-repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Fix gitlab-rake gitlab:import:repos import schedule
+merge_request: 15931
+author:
+type: fixed
diff --git a/changelogs/unreleased/40274-user-settings-breadcrumbs.yml b/changelogs/unreleased/40274-user-settings-breadcrumbs.yml
new file mode 100644
index 00000000000..1f381668aca
--- /dev/null
+++ b/changelogs/unreleased/40274-user-settings-breadcrumbs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix breadcrumbs in User Settings
+merge_request: 16172
+author: rfwatson
+type: fixed
diff --git a/changelogs/unreleased/40286-hide-full-namespace-groups-tree.yml b/changelogs/unreleased/40286-hide-full-namespace-groups-tree.yml
deleted file mode 100644
index cae02d0c2f6..00000000000
--- a/changelogs/unreleased/40286-hide-full-namespace-groups-tree.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Show only group name by default and put full namespace in tooltip in Groups
- tree
-merge_request: 15650
-author:
-type: changed
diff --git a/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml
deleted file mode 100644
index 9c308321a19..00000000000
--- a/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive'
-merge_request: 15493
-author:
-type: fixed
diff --git a/changelogs/unreleased/40301-rebase.yml b/changelogs/unreleased/40301-rebase.yml
new file mode 100644
index 00000000000..1c0fc0cd8ae
--- /dev/null
+++ b/changelogs/unreleased/40301-rebase.yml
@@ -0,0 +1,5 @@
+---
+title: Allow user to rebase merge requests.
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml b/changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml
deleted file mode 100644
index e683e60397e..00000000000
--- a/changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix Issue comment submit button being disabled when pasting content from another
- GFM note
-merge_request: 15530
-author:
-type: fixed
diff --git a/changelogs/unreleased/40418-migrate-existing-data-from-kubernetesservice-to-clusters-platforms-kubernetes.yml b/changelogs/unreleased/40418-migrate-existing-data-from-kubernetesservice-to-clusters-platforms-kubernetes.yml
new file mode 100644
index 00000000000..5e158d831a6
--- /dev/null
+++ b/changelogs/unreleased/40418-migrate-existing-data-from-kubernetesservice-to-clusters-platforms-kubernetes.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate existing data from KubernetesService to Clusters::Platforms::Kubernetes
+merge_request: 15589
+author:
+type: changed
diff --git a/changelogs/unreleased/40453-fix-api-endpoints-to-edit-wiki-pages-where-project-belongs-to-a-group.yml b/changelogs/unreleased/40453-fix-api-endpoints-to-edit-wiki-pages-where-project-belongs-to-a-group.yml
new file mode 100644
index 00000000000..30917098a95
--- /dev/null
+++ b/changelogs/unreleased/40453-fix-api-endpoints-to-edit-wiki-pages-where-project-belongs-to-a-group.yml
@@ -0,0 +1,5 @@
+---
+title: Fix API endpoints to edit wiki pages where project belongs to a group
+merge_request: 16170
+author:
+type: fixed
diff --git a/changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml b/changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml
deleted file mode 100644
index e275c65e8c8..00000000000
--- a/changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade jQuery to 2.2.4
-merge_request: 15570
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/40509_sorting_tags_api.yml b/changelogs/unreleased/40509_sorting_tags_api.yml
new file mode 100644
index 00000000000..38b198d0fe3
--- /dev/null
+++ b/changelogs/unreleased/40509_sorting_tags_api.yml
@@ -0,0 +1,5 @@
+---
+title: add support for sorting in tags api
+merge_request: 15772
+author: haseebeqx
+type: added
diff --git a/changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml b/changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml
deleted file mode 100644
index e9fae6fe0d7..00000000000
--- a/changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix merge requests where the source or target branch name matches a tag name
-merge_request: 15591
-author:
-type: fixed
diff --git a/changelogs/unreleased/40533-groups-tree-updates.yml b/changelogs/unreleased/40533-groups-tree-updates.yml
new file mode 100644
index 00000000000..1bc0aa90f9e
--- /dev/null
+++ b/changelogs/unreleased/40533-groups-tree-updates.yml
@@ -0,0 +1,6 @@
+---
+title: Update groups tree to use GitLab SVG icons, add last updated at information
+ for projects
+merge_request: 15980
+author:
+type: changed
diff --git a/changelogs/unreleased/40549-render-emoj-in-groups-overview.yml b/changelogs/unreleased/40549-render-emoj-in-groups-overview.yml
new file mode 100644
index 00000000000..9b2f58df440
--- /dev/null
+++ b/changelogs/unreleased/40549-render-emoj-in-groups-overview.yml
@@ -0,0 +1,5 @@
+---
+title: Rendering of emoji's in Group-Overview
+merge_request: 16098
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml b/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml
deleted file mode 100644
index 4f0eaf8472f..00000000000
--- a/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix related branches/Merge requests failing to load when the hostname setting
- is changed
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml b/changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml
deleted file mode 100644
index e0e3ddbdaa8..00000000000
--- a/changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Strip leading & trailing whitespaces in CI/CD secret variable keys
-merge_request: 15615
-author:
-type: fixed
diff --git a/changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml b/changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml
deleted file mode 100644
index 708269d5c83..00000000000
--- a/changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade seed-fu to 2.3.7
-merge_request: 15607
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml b/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml
deleted file mode 100644
index afbb869bdbb..00000000000
--- a/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename GKE as Kubernetes Engine
-merge_request: 15608
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/40622-use-left-right-and-max-count.yml b/changelogs/unreleased/40622-use-left-right-and-max-count.yml
new file mode 100644
index 00000000000..c4c8f271cbe
--- /dev/null
+++ b/changelogs/unreleased/40622-use-left-right-and-max-count.yml
@@ -0,0 +1,6 @@
+---
+title: Improve the performance for counting diverging commits. Show 999+
+ if it is more than 1000 commits
+merge_request: 15963
+author:
+type: performance
diff --git a/changelogs/unreleased/40711-fix-forking-hashed-projects.yml b/changelogs/unreleased/40711-fix-forking-hashed-projects.yml
deleted file mode 100644
index 116d7d4e9cf..00000000000
--- a/changelogs/unreleased/40711-fix-forking-hashed-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the fork project functionality for projects with hashed storage
-merge_request: 15671
-author:
-type: fixed
diff --git a/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml b/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml
deleted file mode 100644
index 0328a693354..00000000000
--- a/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix updateEndpoint undefined error for issue_show app root
-merge_request: 15698
-author:
-type: fixed
diff --git a/changelogs/unreleased/40770-doc-elasticsearch.yml b/changelogs/unreleased/40770-doc-elasticsearch.yml
deleted file mode 100644
index b770fb7548e..00000000000
--- a/changelogs/unreleased/40770-doc-elasticsearch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix typo in docs about Elasticsearch
-merge_request: 15699
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/40780-choose-file.yml b/changelogs/unreleased/40780-choose-file.yml
new file mode 100644
index 00000000000..73e59dfcce8
--- /dev/null
+++ b/changelogs/unreleased/40780-choose-file.yml
@@ -0,0 +1,5 @@
+---
+title: Update Browse file to Choose file in all occurences
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/4080-align-retry-btn.yml b/changelogs/unreleased/4080-align-retry-btn.yml
deleted file mode 100644
index c7d3997839c..00000000000
--- a/changelogs/unreleased/4080-align-retry-btn.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Align retry button with job title with new grid size
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/40871-todo-notification-count-shows-notification-without-having-a-todo.yml b/changelogs/unreleased/40871-todo-notification-count-shows-notification-without-having-a-todo.yml
new file mode 100644
index 00000000000..ee196629def
--- /dev/null
+++ b/changelogs/unreleased/40871-todo-notification-count-shows-notification-without-having-a-todo.yml
@@ -0,0 +1,5 @@
+---
+title: Reset todo counters when the target is deleted
+merge_request: 15807
+author:
+type: fixed
diff --git a/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml b/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml
new file mode 100644
index 00000000000..485133b46a7
--- /dev/null
+++ b/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml
@@ -0,0 +1,5 @@
+---
+title: Use relative URL for projects to avoid storing domains
+merge_request: 15876
+author:
+type: fixed
diff --git a/changelogs/unreleased/41016-import-gitlab-shell-projects.yml b/changelogs/unreleased/41016-import-gitlab-shell-projects.yml
new file mode 100644
index 00000000000..47a9e9c3eec
--- /dev/null
+++ b/changelogs/unreleased/41016-import-gitlab-shell-projects.yml
@@ -0,0 +1,6 @@
+---
+title: Import some code and functionality from gitlab-shell to improve subprocess
+ handling
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/41053-extend-cluster-applications-to-allow-install-to-prometheus.yml b/changelogs/unreleased/41053-extend-cluster-applications-to-allow-install-to-prometheus.yml
new file mode 100644
index 00000000000..ffb79d7d79f
--- /dev/null
+++ b/changelogs/unreleased/41053-extend-cluster-applications-to-allow-install-to-prometheus.yml
@@ -0,0 +1,5 @@
+---
+title: Add Prometheus to available Cluster applications
+merge_request: 15895
+author:
+type: added
diff --git a/changelogs/unreleased/41054-disable-creation-of-new-kubernetes-integrations.yml b/changelogs/unreleased/41054-disable-creation-of-new-kubernetes-integrations.yml
new file mode 100644
index 00000000000..b960b14624c
--- /dev/null
+++ b/changelogs/unreleased/41054-disable-creation-of-new-kubernetes-integrations.yml
@@ -0,0 +1,6 @@
+---
+title: Disable creation of new Kubernetes Integrations unless they're active or created
+ from template
+merge_request: 41054
+author:
+type: added
diff --git a/changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml b/changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml
new file mode 100644
index 00000000000..2dd6fc5f1b5
--- /dev/null
+++ b/changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml
@@ -0,0 +1,5 @@
+---
+title: Allow automatic creation of Kubernetes Integration from template
+merge_request: 16104
+author:
+type: added
diff --git a/changelogs/unreleased/41244-issue-board-shortcut-working-while-no-issues.yml b/changelogs/unreleased/41244-issue-board-shortcut-working-while-no-issues.yml
new file mode 100644
index 00000000000..b2c3a86551b
--- /dev/null
+++ b/changelogs/unreleased/41244-issue-board-shortcut-working-while-no-issues.yml
@@ -0,0 +1,5 @@
+---
+title: disables shortcut to issue boards when issues are not enabled
+merge_request: 16020
+author: Christiaan Van den Poel
+type: fixed
diff --git a/changelogs/unreleased/41249-clearing-the-cache.yml b/changelogs/unreleased/41249-clearing-the-cache.yml
new file mode 100644
index 00000000000..221589a1239
--- /dev/null
+++ b/changelogs/unreleased/41249-clearing-the-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Implement project jobs cache reset
+merge_request: 16067
+author:
+type: added
diff --git a/changelogs/unreleased/41268-bump-ruby-to-2-3-6.yml b/changelogs/unreleased/41268-bump-ruby-to-2-3-6.yml
new file mode 100644
index 00000000000..188a854ebee
--- /dev/null
+++ b/changelogs/unreleased/41268-bump-ruby-to-2-3-6.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Ruby to 2.3.6 to include security patches
+merge_request: 16016
+author:
+type: security
diff --git a/changelogs/unreleased/41424-gitlab-rake-gitlab-import-repos-schedules-an-import.yml b/changelogs/unreleased/41424-gitlab-rake-gitlab-import-repos-schedules-an-import.yml
new file mode 100644
index 00000000000..b495754a5a8
--- /dev/null
+++ b/changelogs/unreleased/41424-gitlab-rake-gitlab-import-repos-schedules-an-import.yml
@@ -0,0 +1,5 @@
+---
+title: Fix gitlab-rake gitlab:import:repos import schedule
+merge_request: 16115
+author:
+type: fixed
diff --git a/changelogs/unreleased/41468-error-500-trying-to-view-a-merge-request-json-undefined-method-binary-for-nil-nilclass.yml b/changelogs/unreleased/41468-error-500-trying-to-view-a-merge-request-json-undefined-method-binary-for-nil-nilclass.yml
new file mode 100644
index 00000000000..f69116382f0
--- /dev/null
+++ b/changelogs/unreleased/41468-error-500-trying-to-view-a-merge-request-json-undefined-method-binary-for-nil-nilclass.yml
@@ -0,0 +1,5 @@
+---
+title: Fix viewing merge request diffs where the underlying blobs are unavailable
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/41754-update-scss-lint-to-0-56-0.yml b/changelogs/unreleased/41754-update-scss-lint-to-0-56-0.yml
new file mode 100644
index 00000000000..b96dd376cec
--- /dev/null
+++ b/changelogs/unreleased/41754-update-scss-lint-to-0-56-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update scss-lint to 0.56.0
+merge_request: 16278
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml b/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml
new file mode 100644
index 00000000000..0ceeb7ccee1
--- /dev/null
+++ b/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml
@@ -0,0 +1,5 @@
+---
+title: Force Auto DevOps kubectl version to 1.8.6
+merge_request: 16218
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-tcp-check-rake-task.yml b/changelogs/unreleased/add-tcp-check-rake-task.yml
new file mode 100644
index 00000000000..a7c04bd0d55
--- /dev/null
+++ b/changelogs/unreleased/add-tcp-check-rake-task.yml
@@ -0,0 +1,5 @@
+---
+title: Add a gitlab:tcp_check rake task
+merge_request: 15759
+author:
+type: added
diff --git a/changelogs/unreleased/add_project_ci_config_path_leading_slash_validation.yml b/changelogs/unreleased/add_project_ci_config_path_leading_slash_validation.yml
deleted file mode 100644
index 1c96bd66ed4..00000000000
--- a/changelogs/unreleased/add_project_ci_config_path_leading_slash_validation.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Prefer ci_config_path validation for leading slashes instead of sanitizing
- the input
-merge_request: 15672
-author: Christiaan Van den Poel
-type: other
diff --git a/changelogs/unreleased/admin-welcome-new-group-link.yml b/changelogs/unreleased/admin-welcome-new-group-link.yml
deleted file mode 100644
index 9c939a0a3ad..00000000000
--- a/changelogs/unreleased/admin-welcome-new-group-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed admin welcome screen new group path
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/an-gitaly-timeouts.yml b/changelogs/unreleased/an-gitaly-timeouts.yml
deleted file mode 100644
index e18d82b2704..00000000000
--- a/changelogs/unreleased/an-gitaly-timeouts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add timeouts for Gitaly calls
-merge_request: 15047
-author:
-type: performance
diff --git a/changelogs/unreleased/anchor-issue-references.yml b/changelogs/unreleased/anchor-issue-references.yml
new file mode 100644
index 00000000000..78896427417
--- /dev/null
+++ b/changelogs/unreleased/anchor-issue-references.yml
@@ -0,0 +1,6 @@
+---
+title: Fix false positive issue references in merge requests caused by header anchor
+ links.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/api-domains-expose-project_id.yml b/changelogs/unreleased/api-domains-expose-project_id.yml
new file mode 100644
index 00000000000..22617ffe9b5
--- /dev/null
+++ b/changelogs/unreleased/api-domains-expose-project_id.yml
@@ -0,0 +1,5 @@
+---
+title: Expose project_id on /api/v4/pages/domains
+merge_request: 16200
+author: Luc Didry
+type: changed
diff --git a/changelogs/unreleased/brand_header_change.yml b/changelogs/unreleased/brand_header_change.yml
deleted file mode 100644
index 6ea6e8192a4..00000000000
--- a/changelogs/unreleased/brand_header_change.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: When a custom header logo is present, don't show GitLab type logo
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/bump_mysql_gem.yml b/changelogs/unreleased/bump_mysql_gem.yml
new file mode 100644
index 00000000000..58166949d72
--- /dev/null
+++ b/changelogs/unreleased/bump_mysql_gem.yml
@@ -0,0 +1,5 @@
+---
+title: Bump mysql2 gem version from 0.4.5 to 0.4.10
+merge_request:
+author: asaparov
+type: other
diff --git a/changelogs/unreleased/bvl-circuitbreaker-keys-set.yml b/changelogs/unreleased/bvl-circuitbreaker-keys-set.yml
deleted file mode 100644
index a56456240df..00000000000
--- a/changelogs/unreleased/bvl-circuitbreaker-keys-set.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Keep track of all circuitbreaker keys in a set
-merge_request: 15613
-author:
-type: performance
diff --git a/changelogs/unreleased/bvl-circuitbreaker-process.yml b/changelogs/unreleased/bvl-circuitbreaker-process.yml
deleted file mode 100644
index 595dd13f724..00000000000
--- a/changelogs/unreleased/bvl-circuitbreaker-process.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Monitor NFS shards for circuitbreaker in a separate process
-merge_request: 15426
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-delete-empty-fork-networks.yml b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml
deleted file mode 100644
index 3bbb4cf6e3c..00000000000
--- a/changelogs/unreleased/bvl-delete-empty-fork-networks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clean up empty fork networks
-merge_request: 15373
-author:
-type: other
diff --git a/changelogs/unreleased/bvl-double-fork.yml b/changelogs/unreleased/bvl-double-fork.yml
deleted file mode 100644
index 50bc1adde26..00000000000
--- a/changelogs/unreleased/bvl-double-fork.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Correctly link to a forked project from the new fork page.
-merge_request: 15653
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml b/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml
deleted file mode 100644
index 2acb98db785..00000000000
--- a/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create a fork network for forks with a deleted source
-merge_request: 15595
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-fork-public-project-to-private-namespace.yml b/changelogs/unreleased/bvl-fork-public-project-to-private-namespace.yml
new file mode 100644
index 00000000000..b802625943d
--- /dev/null
+++ b/changelogs/unreleased/bvl-fork-public-project-to-private-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Allow forking a public project to a private group
+merge_request: 16050
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml b/changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml
deleted file mode 100644
index 299d9bf6b9c..00000000000
--- a/changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reduce requests for project forks on show page of projects that have forks
-merge_request: 15663
-author:
-type: performance
diff --git a/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml b/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml
deleted file mode 100644
index 1114d429dec..00000000000
--- a/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make sure a user can add projects to subgroups they have access to
-merge_request: 15294
-author:
-type: fixed
diff --git a/changelogs/unreleased/change-issues-closed-at-background-migration.yml b/changelogs/unreleased/change-issues-closed-at-background-migration.yml
new file mode 100644
index 00000000000..1c81c6a889e
--- /dev/null
+++ b/changelogs/unreleased/change-issues-closed-at-background-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Use a background migration for issues.closed_at
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/cleanup-issues-schema.yml b/changelogs/unreleased/cleanup-issues-schema.yml
deleted file mode 100644
index 9f5fb0bdf82..00000000000
--- a/changelogs/unreleased/cleanup-issues-schema.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clean up schema of the "issues" table
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/commit-title-wrapping.yml b/changelogs/unreleased/commit-title-wrapping.yml
deleted file mode 100644
index 65c28b82b8b..00000000000
--- a/changelogs/unreleased/commit-title-wrapping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed long commit links not wrapping correctly
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/conditionally-eager-load-event-target-authors.yml b/changelogs/unreleased/conditionally-eager-load-event-target-authors.yml
new file mode 100644
index 00000000000..a5f1a958fa8
--- /dev/null
+++ b/changelogs/unreleased/conditionally-eager-load-event-target-authors.yml
@@ -0,0 +1,5 @@
+---
+title: Eager load event target authors whenever possible
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/da-handle-hashed-storage-repos-using-repo-import-task.yml b/changelogs/unreleased/da-handle-hashed-storage-repos-using-repo-import-task.yml
new file mode 100644
index 00000000000..74a00d49ab3
--- /dev/null
+++ b/changelogs/unreleased/da-handle-hashed-storage-repos-using-repo-import-task.yml
@@ -0,0 +1,5 @@
+---
+title: Handle GitLab hashed storage repositories using the repo import task
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/delay-background-migrations.yml b/changelogs/unreleased/delay-background-migrations.yml
new file mode 100644
index 00000000000..aa12591e7d2
--- /dev/null
+++ b/changelogs/unreleased/delay-background-migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Run background migrations with a minimum interval
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/deploy-keys-loading-icon.yml b/changelogs/unreleased/deploy-keys-loading-icon.yml
deleted file mode 100644
index e3cb5bc6924..00000000000
--- a/changelogs/unreleased/deploy-keys-loading-icon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed deploy keys remove button loading state not resetting
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml b/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml
deleted file mode 100644
index 1f8b42ea21f..00000000000
--- a/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make diff notes created on a commit in a merge request to persist a rebase.
-merge_request: 12148
-author:
-type: added
diff --git a/changelogs/unreleased/dm-diff-note-for-line-performance.yml b/changelogs/unreleased/dm-diff-note-for-line-performance.yml
new file mode 100644
index 00000000000..cbc418ab103
--- /dev/null
+++ b/changelogs/unreleased/dm-diff-note-for-line-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of MR discussions on large diffs
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/dm-search-pattern.yml b/changelogs/unreleased/dm-search-pattern.yml
deleted file mode 100644
index 1670d8c4b9a..00000000000
--- a/changelogs/unreleased/dm-search-pattern.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use fuzzy search with minimum length of 3 characters where appropriate
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml b/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml
new file mode 100644
index 00000000000..bc245880ed0
--- /dev/null
+++ b/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml
@@ -0,0 +1,5 @@
+---
+title: Add docs for why you might be signed out when using the Remember me token
+merge_request: 15756
+author:
+type: other
diff --git a/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml
deleted file mode 100644
index 1049e94f312..00000000000
--- a/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable UnnecessaryMantissa in scss-lint
-merge_request: 15255
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/feature-40842-provide-oracles-webgate-cookies-to-jira-requests.yml b/changelogs/unreleased/feature-40842-provide-oracles-webgate-cookies-to-jira-requests.yml
new file mode 100644
index 00000000000..d5ff5bc4627
--- /dev/null
+++ b/changelogs/unreleased/feature-40842-provide-oracles-webgate-cookies-to-jira-requests.yml
@@ -0,0 +1,6 @@
+---
+title: Provide additional cookies to JIRA service requests to allow Oracle WebGates
+ Basic Auth
+merge_request:
+author: Stanislaw Wozniak
+type: changed
diff --git a/changelogs/unreleased/feature-api_runners_online.yml b/changelogs/unreleased/feature-api_runners_online.yml
new file mode 100644
index 00000000000..08f4dd16f28
--- /dev/null
+++ b/changelogs/unreleased/feature-api_runners_online.yml
@@ -0,0 +1,5 @@
+---
+title: Add online and status attribute to runner api entity
+merge_request: 11750
+author:
+type: added
diff --git a/changelogs/unreleased/feature-custom-text-for-new-projects.yml b/changelogs/unreleased/feature-custom-text-for-new-projects.yml
deleted file mode 100644
index 905d76dab33..00000000000
--- a/changelogs/unreleased/feature-custom-text-for-new-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add custom brand text on new project pages
-merge_request: 15541
-author: Markus Koller
-type: changed
diff --git a/changelogs/unreleased/feature-disable-password-authentication.yml b/changelogs/unreleased/feature-disable-password-authentication.yml
deleted file mode 100644
index 999203f12ce..00000000000
--- a/changelogs/unreleased/feature-disable-password-authentication.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow password authentication to be disabled entirely
-merge_request: 15223
-author: Markus Koller
-type: changed
diff --git a/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml b/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml
deleted file mode 100644
index ab85b8ee515..00000000000
--- a/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fail jobs if its dependency is missing
-merge_request: 14009
-author:
-type: fixed
diff --git a/changelogs/unreleased/feature_add_mermaid.yml b/changelogs/unreleased/feature_add_mermaid.yml
deleted file mode 100644
index caeb5d3470d..00000000000
--- a/changelogs/unreleased/feature_add_mermaid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Add support of Mermaid (generation of diagrams and flowcharts from text)'
-merge_request: 15107
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/fix-abuse-reports-link-url.yml b/changelogs/unreleased/fix-abuse-reports-link-url.yml
new file mode 100644
index 00000000000..44c26f35984
--- /dev/null
+++ b/changelogs/unreleased/fix-abuse-reports-link-url.yml
@@ -0,0 +1,5 @@
+---
+title: Fix abuse reports link url in admin area navbar
+merge_request: 16068
+author: megos
+type: fixed
diff --git a/changelogs/unreleased/fix-activity-inline-event-line-height.yml b/changelogs/unreleased/fix-activity-inline-event-line-height.yml
new file mode 100644
index 00000000000..85e69567499
--- /dev/null
+++ b/changelogs/unreleased/fix-activity-inline-event-line-height.yml
@@ -0,0 +1,5 @@
+---
+title: Fix activity inline event line height on mobile
+merge_request: 16121
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml b/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml
new file mode 100644
index 00000000000..8668aa18669
--- /dev/null
+++ b/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml
@@ -0,0 +1,5 @@
+---
+title: Execute quick actions (if present) when creating MR from issue
+merge_request: 15810
+author:
+type: fixed
diff --git a/changelogs/unreleased/40508-snippets-zen-mode.yml b/changelogs/unreleased/fix-docs-help-shortcut.yml
index c205218575b..8c172e44160 100644
--- a/changelogs/unreleased/40508-snippets-zen-mode.yml
+++ b/changelogs/unreleased/fix-docs-help-shortcut.yml
@@ -1,5 +1,5 @@
---
-title: Init zen mode in snippets pages
+title: Fix shortcut links on help page
merge_request:
author:
type: fixed
diff --git a/changelogs/unreleased/fix-filter-by-my-reaction.yml b/changelogs/unreleased/fix-filter-by-my-reaction.yml
deleted file mode 100644
index 8bf91ddf893..00000000000
--- a/changelogs/unreleased/fix-filter-by-my-reaction.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix filter by my reaction is not working
-merge_request: 15345
-author: Hiroyuki Sato
-type: fixed
diff --git a/changelogs/unreleased/fix-gb-fix-import-export-restoring-associations.yml b/changelogs/unreleased/fix-gb-fix-import-export-restoring-associations.yml
new file mode 100644
index 00000000000..58df0024d61
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-import-export-restoring-associations.yml
@@ -0,0 +1,6 @@
+---
+title: Fix missing references to pipeline objects when restoring project with import/export
+ feature
+merge_request: 16221
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-last-push-event-widget-layout.yml b/changelogs/unreleased/fix-last-push-event-widget-layout.yml
new file mode 100644
index 00000000000..ba5b115ca19
--- /dev/null
+++ b/changelogs/unreleased/fix-last-push-event-widget-layout.yml
@@ -0,0 +1,5 @@
+---
+title: Last push event widget width for fixed layout
+merge_request: 15862
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-move-2fa-disable-button.yml b/changelogs/unreleased/fix-move-2fa-disable-button.yml
new file mode 100644
index 00000000000..bac98ad5148
--- /dev/null
+++ b/changelogs/unreleased/fix-move-2fa-disable-button.yml
@@ -0,0 +1,5 @@
+---
+title: Move 2FA disable button
+merge_request: 16177
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-new-project-guidelines-styling.yml b/changelogs/unreleased/fix-new-project-guidelines-styling.yml
deleted file mode 100644
index a97f5c485d4..00000000000
--- a/changelogs/unreleased/fix-new-project-guidelines-styling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use Markdown styling for new project guidelines
-merge_request: 15785
-author: Markus Koller
-type: fixed
diff --git a/changelogs/unreleased/fix-onion-skin-reenter.yml b/changelogs/unreleased/fix-onion-skin-reenter.yml
new file mode 100644
index 00000000000..66b12c037b0
--- /dev/null
+++ b/changelogs/unreleased/fix-onion-skin-reenter.yml
@@ -0,0 +1,5 @@
+---
+title: Fix onion-skin re-entering state
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-profile-settings-content-width.yml b/changelogs/unreleased/fix-profile-settings-content-width.yml
new file mode 100644
index 00000000000..bf164dc587d
--- /dev/null
+++ b/changelogs/unreleased/fix-profile-settings-content-width.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust content width for User Settings, GPG Keys
+merge_request: 16093
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-profile-settings-sidebar-heading.yml b/changelogs/unreleased/fix-profile-settings-sidebar-heading.yml
new file mode 100644
index 00000000000..75e0ea5612f
--- /dev/null
+++ b/changelogs/unreleased/fix-profile-settings-sidebar-heading.yml
@@ -0,0 +1,5 @@
+---
+title: Keep typographic hierarchy in User Settings
+merge_request: 16090
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-protected-branches-descriptions.yml b/changelogs/unreleased/fix-protected-branches-descriptions.yml
deleted file mode 100644
index 8e233d9defd..00000000000
--- a/changelogs/unreleased/fix-protected-branches-descriptions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clarify wording of protected branch settings for the default branch
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/fix-remove-unnecessary-sidebar-element-alignment.yml b/changelogs/unreleased/fix-remove-unnecessary-sidebar-element-alignment.yml
new file mode 100644
index 00000000000..24f6f62b934
--- /dev/null
+++ b/changelogs/unreleased/fix-remove-unnecessary-sidebar-element-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary sidebar element realignment
+merge_request: 16159
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
deleted file mode 100644
index 198116f34aa..00000000000
--- a/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only owner or master can erase jobs
-merge_request: 15216
-author:
-type: changed
diff --git a/changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml b/changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml
deleted file mode 100644
index 4e525c875ca..00000000000
--- a/changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Avoid deactivation when pipeline schedules execute a branch includes `[ci skip]`
- comment
-merge_request: 15405
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-todos-last-page.yml b/changelogs/unreleased/fix-todos-last-page.yml
deleted file mode 100644
index efcdbb75e6e..00000000000
--- a/changelogs/unreleased/fix-todos-last-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix access to the final page of todos
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix_build_count_in_pipeline_success_maild.yml b/changelogs/unreleased/fix_build_count_in_pipeline_success_maild.yml
new file mode 100644
index 00000000000..c39bba62271
--- /dev/null
+++ b/changelogs/unreleased/fix_build_count_in_pipeline_success_maild.yml
@@ -0,0 +1,5 @@
+---
+title: fix build count in pipeline success mail
+merge_request: 15827
+author: Christiaan Van den Poel
+type: fixed
diff --git a/changelogs/unreleased/fj-40053-error-500-members-list.yml b/changelogs/unreleased/fj-40053-error-500-members-list.yml
new file mode 100644
index 00000000000..8c82950bd41
--- /dev/null
+++ b/changelogs/unreleased/fj-40053-error-500-members-list.yml
@@ -0,0 +1,5 @@
+---
+title: Fixing error 500 when member exist but not the user
+merge_request: 15970
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml b/changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml
new file mode 100644
index 00000000000..3fd8b0eb988
--- /dev/null
+++ b/changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml
@@ -0,0 +1,5 @@
+---
+title: Normalizing Identity extern_uid when saving the record
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-40407-missing-order-paginate.yml b/changelogs/unreleased/fj-40407-missing-order-paginate.yml
deleted file mode 100644
index 27471dc2c52..00000000000
--- a/changelogs/unreleased/fj-40407-missing-order-paginate.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added default order to UsersFinder
-merge_request: 15679
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml b/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml
deleted file mode 100644
index cd7b87596e6..00000000000
--- a/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Using appropiate services in the API for managing forks
-merge_request: 15709
-author:
-type: fixed
diff --git a/changelogs/unreleased/group-new-miletone-breadcrumb.yml b/changelogs/unreleased/group-new-miletone-breadcrumb.yml
deleted file mode 100644
index b82c5b604e8..00000000000
--- a/changelogs/unreleased/group-new-miletone-breadcrumb.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed new group milestone breadcrumbs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/hashed-storage-attachments-migration-path.yml b/changelogs/unreleased/hashed-storage-attachments-migration-path.yml
deleted file mode 100644
index 32535437046..00000000000
--- a/changelogs/unreleased/hashed-storage-attachments-migration-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hashed Storage migration script now supports migrating project attachments
-merge_request: 15352
-author:
-type: added
diff --git a/changelogs/unreleased/improved-changes-dropdown.yml b/changelogs/unreleased/improved-changes-dropdown.yml
deleted file mode 100644
index f305cbe573b..00000000000
--- a/changelogs/unreleased/improved-changes-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improved diff changed files dropdown design
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/index-namespaces-lower-name.yml b/changelogs/unreleased/index-namespaces-lower-name.yml
new file mode 100644
index 00000000000..ef08b6d6755
--- /dev/null
+++ b/changelogs/unreleased/index-namespaces-lower-name.yml
@@ -0,0 +1,5 @@
+---
+title: Add index on namespaces lower(name) for UsersController#exists
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/issue-description-field-typo.yml b/changelogs/unreleased/issue-description-field-typo.yml
new file mode 100644
index 00000000000..9c4c179876d
--- /dev/null
+++ b/changelogs/unreleased/issue-description-field-typo.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed typo for issue description field declaration
+merge_request:
+author: Marcus Amargi
+type: fixed
diff --git a/changelogs/unreleased/issue_39238.yml b/changelogs/unreleased/issue_39238.yml
deleted file mode 100644
index 75a4969ca9e..00000000000
--- a/changelogs/unreleased/issue_39238.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix image diff notification email from showing wrong content
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/issue_40500.yml b/changelogs/unreleased/issue_40500.yml
new file mode 100644
index 00000000000..35e8938fdad
--- /dev/null
+++ b/changelogs/unreleased/issue_40500.yml
@@ -0,0 +1,5 @@
+---
+title: Fix timeout when filtering issues by label
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml b/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml
new file mode 100644
index 00000000000..4cac87b0cdb
--- /dev/null
+++ b/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'API: get participants from merge_requests & issues'
+merge_request: 16187
+author: Brent Greeff
+type: added
diff --git a/changelogs/unreleased/jivl-activate-repo-cookie-preferences.yml b/changelogs/unreleased/jivl-activate-repo-cookie-preferences.yml
new file mode 100644
index 00000000000..778eaa84381
--- /dev/null
+++ b/changelogs/unreleased/jivl-activate-repo-cookie-preferences.yml
@@ -0,0 +1,5 @@
+---
+title: Added option to user preferences to enable the multi file editor
+merge_request: 16056
+author:
+type: added
diff --git a/changelogs/unreleased/jivl-fix-import-project-url-bug.yml b/changelogs/unreleased/jivl-fix-import-project-url-bug.yml
new file mode 100644
index 00000000000..0d97b9c9a53
--- /dev/null
+++ b/changelogs/unreleased/jivl-fix-import-project-url-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix import project url not updating project name
+merge_request: 16120
+author:
+type: fixed
diff --git a/changelogs/unreleased/jramsay-4012-i18n-compare.yml b/changelogs/unreleased/jramsay-4012-i18n-compare.yml
new file mode 100644
index 00000000000..ff15724be39
--- /dev/null
+++ b/changelogs/unreleased/jramsay-4012-i18n-compare.yml
@@ -0,0 +1,5 @@
+---
+title: Add i18n helpers to branch comparison view
+merge_request: 16031
+author: James Ramsay
+type: added
diff --git a/changelogs/unreleased/jramsay-41590-add-readme-case.yml b/changelogs/unreleased/jramsay-41590-add-readme-case.yml
new file mode 100644
index 00000000000..37b2bd44e0e
--- /dev/null
+++ b/changelogs/unreleased/jramsay-41590-add-readme-case.yml
@@ -0,0 +1,5 @@
+---
+title: Fix inconsistent downcase of filenames in prefilled `Add` commit messages
+merge_request: 16232
+author: James Ramsay
+type: fixed
diff --git a/changelogs/unreleased/ldap_username_attributes.yml b/changelogs/unreleased/ldap_username_attributes.yml
new file mode 100644
index 00000000000..89bbca58fc9
--- /dev/null
+++ b/changelogs/unreleased/ldap_username_attributes.yml
@@ -0,0 +1,5 @@
+---
+title: Modify `LDAP::Person` to return username value based on attributes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/lfs-badge.yml b/changelogs/unreleased/lfs-badge.yml
new file mode 100644
index 00000000000..e4ed4d6741f
--- /dev/null
+++ b/changelogs/unreleased/lfs-badge.yml
@@ -0,0 +1,5 @@
+---
+title: Added badge to tree & blob views to indicate LFS tracked files
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/merge-request-lock-icon-size-fix.yml b/changelogs/unreleased/merge-request-lock-icon-size-fix.yml
deleted file mode 100644
index 09c059a3011..00000000000
--- a/changelogs/unreleased/merge-request-lock-icon-size-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed merge request lock icon size
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/merge-requests-schema-cleanup.yml b/changelogs/unreleased/merge-requests-schema-cleanup.yml
deleted file mode 100644
index ccce9b1436c..00000000000
--- a/changelogs/unreleased/merge-requests-schema-cleanup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clean up schema of the "merge_requests" table
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml b/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml
deleted file mode 100644
index 499543ef883..00000000000
--- a/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add untracked files to uploads table
-merge_request: 15270
-author:
-type: other
diff --git a/changelogs/unreleased/mk-add-user-rate-limits.yml b/changelogs/unreleased/mk-add-user-rate-limits.yml
deleted file mode 100644
index 512757da5fc..00000000000
--- a/changelogs/unreleased/mk-add-user-rate-limits.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Add anonymous rate limit per IP, and authenticated (web or API) rate limits
- per user
-merge_request: 14708
-author:
-type: added
diff --git a/changelogs/unreleased/mk-no-op-delete-conflicting-redirects.yml b/changelogs/unreleased/mk-no-op-delete-conflicting-redirects.yml
new file mode 100644
index 00000000000..37fdb1df6df
--- /dev/null
+++ b/changelogs/unreleased/mk-no-op-delete-conflicting-redirects.yml
@@ -0,0 +1,6 @@
+---
+title: Prevent excessive DB load due to faulty DeleteConflictingRedirectRoutes background
+ migration
+merge_request: 16205
+author:
+type: fixed
diff --git a/changelogs/unreleased/multiple-clusters-single-list.yml b/changelogs/unreleased/multiple-clusters-single-list.yml
new file mode 100644
index 00000000000..55743f3c00e
--- /dev/null
+++ b/changelogs/unreleased/multiple-clusters-single-list.yml
@@ -0,0 +1,5 @@
+---
+title: Present multiple clusters in a single list instead of a tabbed view
+merge_request: 15669
+author:
+type: changed
diff --git a/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml b/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml
new file mode 100644
index 00000000000..e0c3136be69
--- /dev/null
+++ b/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml
@@ -0,0 +1,6 @@
+---
+title: Treat empty markdown and html strings as valid cached text, not missing cache
+ that needs to be updated
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/osw-introduce-merge-request-statistics.yml b/changelogs/unreleased/osw-introduce-merge-request-statistics.yml
new file mode 100644
index 00000000000..fed7c2141fb
--- /dev/null
+++ b/changelogs/unreleased/osw-introduce-merge-request-statistics.yml
@@ -0,0 +1,5 @@
+---
+title: Cache merged and closed events data in merge_request_metrics table
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/outdated-browser-position-fix.yml b/changelogs/unreleased/outdated-browser-position-fix.yml
deleted file mode 100644
index 801e45a28b3..00000000000
--- a/changelogs/unreleased/outdated-browser-position-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed outdated browser flash positioning
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/patch-24.yml b/changelogs/unreleased/patch-24.yml
deleted file mode 100644
index a670eb3ab56..00000000000
--- a/changelogs/unreleased/patch-24.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix graph notes number duplication.
-merge_request: 15696
-author: Vladislav Kaverin
-type: fixed
diff --git a/changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml b/changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml
deleted file mode 100644
index a4133ae5cec..00000000000
--- a/changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reenable Prometheus metrics, add more control over Prometheus method instrumentation
-merge_request: 15558
-author:
-type: fixed
diff --git a/changelogs/unreleased/perform-sql-matching-of-tags.yml b/changelogs/unreleased/perform-sql-matching-of-tags.yml
deleted file mode 100644
index 39f8a867a4d..00000000000
--- a/changelogs/unreleased/perform-sql-matching-of-tags.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Perform SQL matching of Build&Runner tags to greatly speed-up job picking
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/protected-branches-names.yml b/changelogs/unreleased/protected-branches-names.yml
deleted file mode 100644
index 3c6767df571..00000000000
--- a/changelogs/unreleased/protected-branches-names.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only load branch names for protected branch checks
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/remove-incorrect-guidance.yml b/changelogs/unreleased/remove-incorrect-guidance.yml
new file mode 100644
index 00000000000..eeb5745698f
--- /dev/null
+++ b/changelogs/unreleased/remove-incorrect-guidance.yml
@@ -0,0 +1,6 @@
+---
+title: Removed incorrect guidance stating blocked users will be removed from groups
+ and project as members
+merge_request: 15947
+author: CesarApodaca
+type: fixed
diff --git a/changelogs/unreleased/remove-links-mr-empty-state.yml b/changelogs/unreleased/remove-links-mr-empty-state.yml
new file mode 100644
index 00000000000..c666bc2c81d
--- /dev/null
+++ b/changelogs/unreleased/remove-links-mr-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Remove related links in MR widget when empty state
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove-tabindexes-from-tag-form.yml b/changelogs/unreleased/remove-tabindexes-from-tag-form.yml
new file mode 100644
index 00000000000..a15bf2a7a4f
--- /dev/null
+++ b/changelogs/unreleased/remove-tabindexes-from-tag-form.yml
@@ -0,0 +1,5 @@
+---
+title: removed tabindexes from tag form
+merge_request:
+author: Marcus Amargi
+type: changed
diff --git a/changelogs/unreleased/sh-add-schedule-pipeline-run-now.yml b/changelogs/unreleased/sh-add-schedule-pipeline-run-now.yml
new file mode 100644
index 00000000000..6d06f695f10
--- /dev/null
+++ b/changelogs/unreleased/sh-add-schedule-pipeline-run-now.yml
@@ -0,0 +1,5 @@
+---
+title: Add button to run scheduled pipeline immediately
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/sh-catch-invalid-uri-markdown.yml b/changelogs/unreleased/sh-catch-invalid-uri-markdown.yml
new file mode 100644
index 00000000000..9b0233fe988
--- /dev/null
+++ b/changelogs/unreleased/sh-catch-invalid-uri-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Gracefully handle garbled URIs in Markdown
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-root-ref-repository.yml b/changelogs/unreleased/sh-fix-root-ref-repository.yml
deleted file mode 100644
index 0670db84fa6..00000000000
--- a/changelogs/unreleased/sh-fix-root-ref-repository.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Gracefully handle case when repository's root ref does not exist"
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-make-kib-human.yml b/changelogs/unreleased/sh-make-kib-human.yml
new file mode 100644
index 00000000000..c40bb34fa4a
--- /dev/null
+++ b/changelogs/unreleased/sh-make-kib-human.yml
@@ -0,0 +1,5 @@
+---
+title: Humanize the units of "Showing last X KiB of log" in job trace
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-optimize-commit-stats.yml b/changelogs/unreleased/sh-optimize-commit-stats.yml
new file mode 100644
index 00000000000..8c1be1252fb
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-commit-stats.yml
@@ -0,0 +1,5 @@
+---
+title: Speed up generation of commit stats by using Rugged native methods
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-optimize-groups-api.yml b/changelogs/unreleased/sh-optimize-groups-api.yml
deleted file mode 100644
index 37b74715a81..00000000000
--- a/changelogs/unreleased/sh-optimize-groups-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimize API /groups/:id/projects by preloading associations
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-validate-path-project-import.yml b/changelogs/unreleased/sh-validate-path-project-import.yml
new file mode 100644
index 00000000000..acad66c0ab2
--- /dev/null
+++ b/changelogs/unreleased/sh-validate-path-project-import.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid leaving a push event empty if payload cannot be created
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/show-inline-edit-btn.yml b/changelogs/unreleased/show-inline-edit-btn.yml
new file mode 100644
index 00000000000..8cfe9b7d75a
--- /dev/null
+++ b/changelogs/unreleased/show-inline-edit-btn.yml
@@ -0,0 +1,5 @@
+---
+title: Move edit button to second row on issue page (and change it to a pencil icon)
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/show_proper_labels_in_board_issue_sidebar_when_issue_is_closed.yml b/changelogs/unreleased/show_proper_labels_in_board_issue_sidebar_when_issue_is_closed.yml
new file mode 100644
index 00000000000..c2ab34b20a5
--- /dev/null
+++ b/changelogs/unreleased/show_proper_labels_in_board_issue_sidebar_when_issue_is_closed.yml
@@ -0,0 +1,5 @@
+---
+title: show None when issue is in closed list and no labels assigned
+merge_request: 15976
+author: Christiaan Van den Poel
+type: fixed
diff --git a/changelogs/unreleased/skip_confirmation_user_API.yml b/changelogs/unreleased/skip_confirmation_user_API.yml
deleted file mode 100644
index 144ccd69e68..00000000000
--- a/changelogs/unreleased/skip_confirmation_user_API.yml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Add email confirmation parameters for user creation and update via API
-merge_request:
-author: Daniel Juarez
-type: added
-
-
diff --git a/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml b/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml
new file mode 100644
index 00000000000..b5e3210c737
--- /dev/null
+++ b/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml
@@ -0,0 +1,5 @@
+---
+title: Hide link to issues/MRs from labels list if issues/MRs are disabled.
+merge_request: 15863
+author: Sophie Herold
+type: fixed
diff --git a/changelogs/unreleased/tc-correct-email-in-reply-to.yml b/changelogs/unreleased/tc-correct-email-in-reply-to.yml
new file mode 100644
index 00000000000..1c8043f6a5c
--- /dev/null
+++ b/changelogs/unreleased/tc-correct-email-in-reply-to.yml
@@ -0,0 +1,5 @@
+---
+title: Make mail notifications of discussion notes In-Reply-To of each other
+merge_request: 14289
+author:
+type: changed
diff --git a/changelogs/unreleased/text-utils.yml b/changelogs/unreleased/text-utils.yml
deleted file mode 100644
index b95bb82fe01..00000000000
--- a/changelogs/unreleased/text-utils.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Export text utils functions as es6 module and add tests
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/throttle-touching-of-objects.yml b/changelogs/unreleased/throttle-touching-of-objects.yml
deleted file mode 100644
index 0a57bea7c83..00000000000
--- a/changelogs/unreleased/throttle-touching-of-objects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Throttle the number of UPDATEs triggered by touch
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/tm-feature-list-runners-jobs-api.yml b/changelogs/unreleased/tm-feature-list-runners-jobs-api.yml
deleted file mode 100644
index d75a2b68c30..00000000000
--- a/changelogs/unreleased/tm-feature-list-runners-jobs-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: New API endpoint - list jobs for a specified runner
-merge_request: 15432
-author:
-type: added
diff --git a/changelogs/unreleased/tm-feature-namespace-by-id-api.yml b/changelogs/unreleased/tm-feature-namespace-by-id-api.yml
deleted file mode 100644
index bc4a8949d28..00000000000
--- a/changelogs/unreleased/tm-feature-namespace-by-id-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add new API endpoint - get a namespace by ID
-merge_request: 15442
-author:
-type: added
diff --git a/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml b/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml
deleted file mode 100644
index e509a8df6bc..00000000000
--- a/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: 'Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma
- in :cartwheel_tone4:'
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/update-merge-worker-metrics.yml b/changelogs/unreleased/update-merge-worker-metrics.yml
deleted file mode 100644
index c733675926a..00000000000
--- a/changelogs/unreleased/update-merge-worker-metrics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add performance logging to UpdateMergeRequestsWorker.
-merge_request: 15360
-author:
-type: performance
diff --git a/changelogs/unreleased/fl-upgrade-svg.yml b/changelogs/unreleased/update-redis-rack.yml
index caf73881d0f..6e2e6e203b8 100644
--- a/changelogs/unreleased/fl-upgrade-svg.yml
+++ b/changelogs/unreleased/update-redis-rack.yml
@@ -1,5 +1,5 @@
---
-title: Update svg external depencency
+title: Update redis-rack to 2.0.4
merge_request:
author:
type: other
diff --git a/changelogs/unreleased/update_mr_changes_empty_page.yml b/changelogs/unreleased/update_mr_changes_empty_page.yml
deleted file mode 100644
index bae73c21e8f..00000000000
--- a/changelogs/unreleased/update_mr_changes_empty_page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update empty state page of merge request 'changes' tab
-merge_request: 15611
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/use-count_commits-directly.yml b/changelogs/unreleased/use-count_commits-directly.yml
deleted file mode 100644
index 549e0744ea4..00000000000
--- a/changelogs/unreleased/use-count_commits-directly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve the performance for counting commits
-merge_request: 15628
-author:
-type: performance
diff --git a/changelogs/unreleased/use-merge-requests-diff-id-column.yml b/changelogs/unreleased/use-merge-requests-diff-id-column.yml
deleted file mode 100644
index da4106ec8cf..00000000000
--- a/changelogs/unreleased/use-merge-requests-diff-id-column.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make finding most recent merge request diffs more efficient
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/user-agent-gke-api.yml b/changelogs/unreleased/user-agent-gke-api.yml
deleted file mode 100644
index 1abdbadd53b..00000000000
--- a/changelogs/unreleased/user-agent-gke-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use custom user agent header in all GCP API requests.
-merge_request: 15705
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-modal-target-id.yml b/changelogs/unreleased/winh-modal-target-id.yml
new file mode 100644
index 00000000000..f8d5b72be50
--- /dev/null
+++ b/changelogs/unreleased/winh-modal-target-id.yml
@@ -0,0 +1,5 @@
+---
+title: Add id to modal.vue to support data-toggle="modal"
+merge_request: 16189
+author:
+type: other
diff --git a/changelogs/unreleased/winh-subgroups-api.yml b/changelogs/unreleased/winh-subgroups-api.yml
deleted file mode 100644
index c49e3621e9c..00000000000
--- a/changelogs/unreleased/winh-subgroups-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add /groups/:id/subgroups endpoint to API
-merge_request: 15142
-author: marbemac
-type: added
diff --git a/changelogs/unreleased/winh-translate-contributors-page-dates.yml b/changelogs/unreleased/winh-translate-contributors-page-dates.yml
new file mode 100644
index 00000000000..74801bbd86e
--- /dev/null
+++ b/changelogs/unreleased/winh-translate-contributors-page-dates.yml
@@ -0,0 +1,5 @@
+---
+title: Translate date ranges on contributors page
+merge_request: 15846
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-commit-show-n-1.yml b/changelogs/unreleased/zj-commit-show-n-1.yml
deleted file mode 100644
index e536434f74a..00000000000
--- a/changelogs/unreleased/zj-commit-show-n-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fetch blobs in bulk when generating diffs
-merge_request:
-author:
-type: performance
diff --git a/config.ru b/config.ru
index 065ce59932f..de0400f4f67 100644
--- a/config.ru
+++ b/config.ru
@@ -17,6 +17,11 @@ end
require ::File.expand_path('../config/environment', __FILE__)
+warmup do |app|
+ client = Rack::MockRequest.new(app)
+ client.get('/')
+end
+
map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do
run Gitlab::Application
end
diff --git a/config/application.rb b/config/application.rb
index 6436f887d14..1110199b888 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -34,6 +34,10 @@ module Gitlab
config.generators.templates.push("#{config.root}/generator_templates")
+ # Rake tasks ignore the eager loading settings, so we need to set the
+ # autoload paths explicitly
+ config.autoload_paths = config.eager_load_paths.dup
+
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
@@ -159,7 +163,7 @@ module Gitlab
config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly'
config.generators do |g|
- g.factory_girl false
+ g.factory_bot false
end
config.after_initialize do
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index d8fa3138184..cab72032d22 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -405,6 +405,7 @@ production: &base
# Sync user's profile from the specified Omniauth providers every time the user logs in (default: empty).
# Define the allowed providers using an array, e.g. ["cas3", "saml", "twitter"],
# or as true/false to allow all providers or none.
+ # When authenticating using LDAP, the user's email is always synced.
# sync_profile_from_provider: []
# Select which info to sync from the providers above. (default: email).
diff --git a/config/initializers/active_record_data_types.rb b/config/initializers/active_record_data_types.rb
index fef591c397d..0359e14b232 100644
--- a/config/initializers/active_record_data_types.rb
+++ b/config/initializers/active_record_data_types.rb
@@ -79,3 +79,8 @@ elsif Gitlab::Database.mysql?
NATIVE_DATABASE_TYPES[:datetime_with_timezone] = { name: 'timestamp' }
end
end
+
+# Ensure `datetime_with_timezone` columns are correctly written to schema.rb
+if (ActiveRecord::Base.connection.active? rescue false)
+ ActiveRecord::Base.connection.send :reload_type_map
+end
diff --git a/config/initializers/active_record_schema_ignore_tables.rb b/config/initializers/active_record_schema_ignore_tables.rb
new file mode 100644
index 00000000000..661135f8ade
--- /dev/null
+++ b/config/initializers/active_record_schema_ignore_tables.rb
@@ -0,0 +1,2 @@
+# Ignore table used temporarily in background migration
+ActiveRecord::SchemaDumper.ignore_tables = ["untracked_files_for_uploads"]
diff --git a/config/initializers/asset_sync.rb b/config/initializers/asset_sync.rb
new file mode 100644
index 00000000000..7f3934853fa
--- /dev/null
+++ b/config/initializers/asset_sync.rb
@@ -0,0 +1,31 @@
+AssetSync.configure do |config|
+ # Disable the asset_sync gem by default. If it is enabled, but not configured,
+ # asset_sync will cause the build to fail.
+ config.enabled = if ENV.has_key?('ASSET_SYNC_ENABLED')
+ ENV['ASSET_SYNC_ENABLED'] == 'true'
+ else
+ false
+ end
+
+ # Pulled from https://github.com/AssetSync/asset_sync/blob/v2.2.0/lib/asset_sync/engine.rb#L15-L40
+ # This allows us to disable asset_sync by default and configure through environment variables
+ # Updates to asset_sync gem should be checked
+ config.fog_provider = ENV['FOG_PROVIDER'] if ENV.has_key?('FOG_PROVIDER')
+ config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY')
+ config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION')
+
+ config.aws_access_key_id = ENV['ASSETS_AWS_ACCESS_KEY_ID'] if ENV.has_key?('ASSETS_AWS_ACCESS_KEY_ID')
+ config.aws_secret_access_key = ENV['ASSETS_AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('ASSETS_AWS_SECRET_ACCESS_KEY')
+ config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY')
+
+ config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME')
+ config.rackspace_api_key = ENV['RACKSPACE_API_KEY'] if ENV.has_key?('RACKSPACE_API_KEY')
+
+ config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] if ENV.has_key?('GOOGLE_STORAGE_ACCESS_KEY_ID')
+ config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] if ENV.has_key?('GOOGLE_STORAGE_SECRET_ACCESS_KEY')
+
+ config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
+
+ config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION')
+ config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST')
+end
diff --git a/config/initializers/fix_local_cache_middleware.rb b/config/initializers/fix_local_cache_middleware.rb
index cb37f9ed22c..2644ee6a7d3 100644
--- a/config/initializers/fix_local_cache_middleware.rb
+++ b/config/initializers/fix_local_cache_middleware.rb
@@ -6,7 +6,7 @@ module LocalCacheRegistryCleanupWithEnsure
def call(env)
LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
- response = @app.call(env)
+ response = @app.call(env) # rubocop:disable Gitlab/ModuleWithInstanceVariables
response[2] = ::Rack::BodyProxy.new(response[2]) do
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
end
diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb
index bfab8c77a4b..cc9167d29b9 100644
--- a/config/initializers/flipper.rb
+++ b/config/initializers/flipper.rb
@@ -1,8 +1,22 @@
-require 'flipper/middleware/memoizer'
+require 'flipper/adapters/active_record'
+require 'flipper/adapters/active_support_cache_store'
-unless Rails.env.test?
- Rails.application.config.middleware.use Flipper::Middleware::Memoizer,
- lambda { Feature.flipper }
+Flipper.configure do |config|
+ config.default do
+ adapter = Flipper::Adapters::ActiveRecord.new(
+ feature_class: Feature::FlipperFeature, gate_class: Feature::FlipperGate)
+ cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new(
+ adapter,
+ Rails.cache,
+ expires_in: 10.seconds)
+
+ Flipper.new(cached_adapter)
+ end
+end
- Feature.register_feature_groups
+Feature.register_feature_groups
+
+unless Rails.env.test?
+ require 'flipper/middleware/memoizer'
+ Rails.application.config.middleware.use Flipper::Middleware::Memoizer
end
diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb
index bedd57ede04..cb611aa21df 100644
--- a/config/initializers/forbid_sidekiq_in_transactions.rb
+++ b/config/initializers/forbid_sidekiq_in_transactions.rb
@@ -1,5 +1,7 @@
module Sidekiq
module Worker
+ EnqueueFromTransactionError = Class.new(StandardError)
+
mattr_accessor :skip_transaction_check
self.skip_transaction_check = false
@@ -12,11 +14,11 @@ module Sidekiq
end
module ClassMethods
- module NoSchedulingFromTransactions
+ module NoEnqueueingFromTransactions
%i(perform_async perform_at perform_in).each do |name|
define_method(name) do |*args|
if !Sidekiq::Worker.skip_transaction_check && AfterCommitQueue.inside_transaction?
- raise <<-MSG.strip_heredoc
+ raise Sidekiq::Worker::EnqueueFromTransactionError, <<~MSG
`#{self}.#{name}` cannot be called inside a transaction as this can lead to
race conditions when the worker runs before the transaction is committed and
tries to access a model that has not been saved yet.
@@ -30,7 +32,7 @@ module Sidekiq
end
end
- prepend NoSchedulingFromTransactions
+ prepend NoEnqueueingFromTransactions
end
end
end
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index 1cff355346c..581397b26f8 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -18,7 +18,7 @@ Peek.into Peek::Views::Rblineprof
Peek.into Peek::Views::GC
Peek.into Peek::Views::Gitaly
-# rubocop:disable Style/ClassAndModuleCamelCase
+# rubocop:disable Naming/ClassAndModuleCamelCase
class PEEK_DB_CLIENT
class << self
attr_accessor :query_details
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index 16b9d5b15e5..2de310753a9 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -19,10 +19,10 @@ module RspecProfilingExt
def example_finished(*args)
super
rescue => err
- return if @already_logged_example_finished_error
+ return if @already_logged_example_finished_error # rubocop:disable Gitlab/ModuleWithInstanceVariables
$stderr.puts "rspec_profiling couldn't collect an example: #{err}. Further warnings suppressed."
- @already_logged_example_finished_error = true
+ @already_logged_example_finished_error = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
alias_method :example_passed, :example_finished
diff --git a/config/initializers/rugged_use_gitlab_git_attributes.rb b/config/initializers/rugged_use_gitlab_git_attributes.rb
index 7d652799786..1cfb3bcb4bd 100644
--- a/config/initializers/rugged_use_gitlab_git_attributes.rb
+++ b/config/initializers/rugged_use_gitlab_git_attributes.rb
@@ -15,8 +15,11 @@ module Rugged
class Repository
module UseGitlabGitAttributes
def fetch_attributes(name, *)
+ attributes.attributes(name)
+ end
+
+ def attributes
@attributes ||= Gitlab::Git::Attributes.new(path)
- @attributes.attributes(name)
end
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index ba4481ae602..0f164e628f9 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -42,6 +42,8 @@ Sidekiq.configure_server do |config|
Gitlab::SidekiqThrottler.execute!
+ Gitlab::SidekiqVersioning.install!
+
config = Gitlab::Database.config ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = Sidekiq.options[:concurrency]
@@ -60,19 +62,3 @@ Sidekiq.configure_client do |config|
chain.add Gitlab::SidekiqStatus::ClientMiddleware
end
end
-
-# The Sidekiq client API always adds the queue to the Sidekiq queue
-# list, but mail_room and gitlab-shell do not. This is only necessary
-# for monitoring.
-begin
- queues = Gitlab::SidekiqConfig.worker_queues
-
- Sidekiq.redis do |conn|
- conn.pipelined do
- queues.each do |queue|
- conn.sadd('queues', queue)
- end
- end
- end
-rescue Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED
-end
diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml
index 264a975b614..da721a9b6e6 100644
--- a/config/no_todos_messages.yml
+++ b/config/no_todos_messages.yml
@@ -3,9 +3,9 @@
#
# If you come up with a fun one, please feel free to contribute it to GitLab!
# https://about.gitlab.com/contributing/
----
-- Good job! Looks like you don't have any todos left.
+---
+- Good job! Looks like you don't have any todos left
- Isn't an empty todo list beautiful?
- Give yourself a pat on the back!
- Nothing left to do, high five!
-- Henceforth you shall be known as "Todo Destroyer".
+- Henceforth you shall be known as "Todo Destroyer"
diff --git a/config/routes.rb b/config/routes.rb
index 016140e0ede..f162043dd5e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,6 +43,8 @@ Rails.application.routes.draw do
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check'
+ get 'ide' => 'ide#index'
+ get 'ide/*vueroute' => 'ide#index', format: false
resources :metrics, only: [:index]
mount Peek::Railtie => '/peek'
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 093da10f57f..bdf4b199c0a 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -96,6 +96,7 @@ constraints(ProjectUrlConstrainer.new) do
post :toggle_subscription
post :remove_wip
post :assign_related_issues
+ post :rebase
scope constraints: { format: nil }, action: :show do
get :commits, defaults: { tab: 'commits' }
@@ -179,6 +180,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :pipeline_schedules, except: [:show] do
member do
+ post :play
post :take_ownership
end
end
@@ -382,8 +384,8 @@ constraints(ProjectUrlConstrainer.new) do
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
member do
- get :resume
- get :pause
+ post :resume
+ post :pause
end
collection do
@@ -406,7 +408,9 @@ constraints(ProjectUrlConstrainer.new) do
end
namespace :settings do
get :members, to: redirect("%{namespace_id}/%{project_id}/project_members")
- resource :ci_cd, only: [:show], controller: 'ci_cd'
+ resource :ci_cd, only: [:show], controller: 'ci_cd' do
+ post :reset_cache
+ end
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index e059d7c11e0..eb49869e031 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -25,8 +25,6 @@
- [new_note, 2]
- [new_issue, 2]
- [new_merge_request, 2]
- - [build, 2]
- - [pipeline, 2]
- [pipeline_processing, 5]
- [pipeline_creation, 4]
- [pipeline_default, 3]
@@ -38,11 +36,12 @@
- [mailers, 2]
- [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2]
+ - [rebase, 2]
- [upload_checksum, 1]
- [repository_fork, 1]
- [repository_import, 1]
- [github_importer, 1]
- - [github_importer_advance_stage, 1]
+ - [github_import_advance_stage, 1]
- [project_service, 1]
- [delete_user, 1]
- [delete_merged_branches, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 78ced4c3e8c..5f95255334c 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -32,10 +32,10 @@ var config = {
boards: './boards/boards_bundle.js',
common: './commons/index.js',
common_vue: './vue_shared/vue_resource_interceptor.js',
- common_d3: ['d3'],
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
deploy_keys: './deploy_keys/index.js',
+ docs: './docs/docs_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
@@ -70,7 +70,7 @@ var config = {
protected_branches: './protected_branches',
protected_tags: './protected_tags',
registry_list: './registry/index.js',
- repo: './repo/index.js',
+ ide: './ide/index.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
@@ -118,7 +118,10 @@ var config = {
},
{
test: /\_worker\.js$/,
- loader: 'worker-loader',
+ use: [
+ { loader: 'worker-loader' },
+ { loader: 'babel-loader' },
+ ],
},
{
test: /\.(worker(\.min)?\.js|pdf|bmpr)$/,
@@ -138,6 +141,7 @@ var config = {
],
noParse: [/monaco-editor\/\w+\/vs\//],
+ strictExportPresence: true,
},
plugins: [
@@ -176,8 +180,13 @@ var config = {
return chunk.name;
}
return chunk.mapModules((m) => {
- var chunkPath = m.request.split('!').pop();
- return path.relative(m.context, chunkPath);
+ const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages');
+ if (m.resource.indexOf(pagesBase) === 0) {
+ return path.relative(pagesBase, m.resource)
+ .replace(/\/index\.[a-z]+$/, '')
+ .replace(/\//g, '__');
+ }
+ return path.relative(m.context, m.resource);
}).join('_');
}),
@@ -204,7 +213,7 @@ var config = {
'pipelines',
'pipelines_details',
'registry_list',
- 'repo',
+ 'ide',
'schedule_form',
'schedules_index',
'sidebar',
@@ -224,6 +233,9 @@ var config = {
'monitoring',
'users',
],
+ minChunks: function (module, count) {
+ return module.resource && /d3-/.test(module.resource);
+ },
}),
// create cacheable common library bundles
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 1f8f5cfc82b..213c8bca639 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -63,7 +63,8 @@ Sidekiq::Testing.inline! do
namespace_id: group.id,
name: project_path.titleize,
description: FFaker::Lorem.sentence,
- visibility_level: Gitlab::VisibilityLevel.values.sample
+ visibility_level: Gitlab::VisibilityLevel.values.sample,
+ skip_disk_validation: true
}
project = Projects::CreateService.new(User.first, params).execute
diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb
index 86e0a38aae1..b218f4e71fd 100644
--- a/db/fixtures/development/06_teams.rb
+++ b/db/fixtures/development/06_teams.rb
@@ -14,7 +14,7 @@ Sidekiq::Testing.inline! do
Project.all.each do |project|
User.all.sample(4).each do |user|
- if project.team << [user, Gitlab::Access.values.sample]
+ if project.add_role(user, Gitlab::Access.sym_options.keys.sample)
print '.'
else
print 'F'
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 96c6d954ff7..d7be6f5950f 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -140,8 +140,8 @@ class Gitlab::Seeder::CycleAnalytics
issue.update(milestone: @project.milestones.sample)
else
label_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
- list_label = FactoryGirl.create(:label, title: label_name, project: issue.project)
- FactoryGirl.create(:list, board: FactoryGirl.create(:board, project: issue.project), label: list_label)
+ list_label = FactoryBot.create(:label, title: label_name, project: issue.project)
+ FactoryBot.create(:list, board: FactoryBot.create(:board, project: issue.project), label: list_label)
issue.update(labels: [list_label])
end
diff --git a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
index 477b2106dea..21b367711c3 100644
--- a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
+++ b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class RemoveDeprecatedIssuesTrackerColumnsFromProjects < ActiveRecord::Migration
def change
remove_column :projects, :issues_tracker, :string, default: 'gitlab', null: false
diff --git a/db/migrate/20160610301627_remove_notification_level_from_users.rb b/db/migrate/20160610301627_remove_notification_level_from_users.rb
index 8afb14df2cf..356e53b4b23 100644
--- a/db/migrate/20160610301627_remove_notification_level_from_users.rb
+++ b/db/migrate/20160610301627_remove_notification_level_from_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class RemoveNotificationLevelFromUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160621123729_add_rebase_commit_sha_to_merge_requests.rb b/db/migrate/20160621123729_add_rebase_commit_sha_to_merge_requests.rb
new file mode 100644
index 00000000000..1222dc640a8
--- /dev/null
+++ b/db/migrate/20160621123729_add_rebase_commit_sha_to_merge_requests.rb
@@ -0,0 +1,22 @@
+# This migration is a duplicate of 20171230123729_add_rebase_commit_sha_to_merge_requests_ce.rb
+#
+# We backported this feature from EE using the same migration, but with a new
+# timestamp, which caused an error when the backport was then to be merged back
+# into EE.
+#
+# See discussion at https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3932
+class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:merge_requests, :rebase_commit_sha)
+ add_column :merge_requests, :rebase_commit_sha, :string
+ end
+ end
+
+ def down
+ if column_exists?(:merge_requests, :rebase_commit_sha)
+ remove_column :merge_requests, :rebase_commit_sha
+ end
+ end
+end
diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
index 52a9819c628..058bd539e65 100644
--- a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
+++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
index 4a7bde7f9f3..d0e5da4d28b 100644
--- a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
+++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
index e28ab31d629..baf254c3bcc 100644
--- a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
+++ b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
index aec709aaf59..9eafd8b9477 100644
--- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
+++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
index df7d922b816..f32167037e0 100644
--- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20161018024550_remove_priority_from_labels.rb b/db/migrate/20161018024550_remove_priority_from_labels.rb
index b7416cca664..bc25a43526c 100644
--- a/db/migrate/20161018024550_remove_priority_from_labels.rb
+++ b/db/migrate/20161018024550_remove_priority_from_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class RemovePriorityFromLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb
index 82fbdf02444..a547409aaa5 100644
--- a/db/migrate/20161201160452_migrate_project_statistics.rb
+++ b/db/migrate/20161201160452_migrate_project_statistics.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
class MigrateProjectStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
index 268144a2552..9bed38a3444 100644
--- a/db/migrate/20170222143500_remove_old_project_id_columns.rb
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# rubocop:disable RemoveIndex
class RemoveOldProjectIdColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
index 1a77d5934a3..0535c2ddaf2 100644
--- a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# rubocop:disable Migration/Datetime
class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
index 807dfcb385d..9b9098d115d 100644
--- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# rubocop:disable Migration/UpdateLargeTable
class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20171019141859_fix_dev_timezone_schema.rb b/db/migrate/20171019141859_fix_dev_timezone_schema.rb
new file mode 100644
index 00000000000..fb7c17dd747
--- /dev/null
+++ b/db/migrate/20171019141859_fix_dev_timezone_schema.rb
@@ -0,0 +1,25 @@
+class FixDevTimezoneSchema < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # The this migrations tries to help solve unwanted changes to `schema.rb`
+ # while developing GitLab. Installations created before we started using
+ # `datetime_with_timezone` are likely to face this problem. Updating those
+ # columns to the new type should help fix this.
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ TIMEZONE_TABLES = %i(appearances ci_group_variables ci_pipeline_schedule_variables events gpg_keys gpg_signatures project_auto_devops)
+
+ def up
+ return unless Rails.env.development? || Rails.env.test?
+
+ TIMEZONE_TABLES.each do |table|
+ change_column table, :created_at, :datetime_with_timezone
+ change_column table, :updated_at, :datetime_with_timezone
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
index e6a780d0964..bfb3dcae511 100644
--- a/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
+++ b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
@@ -16,6 +16,7 @@ class IssuesMilestoneIdForeignKey < ActiveRecord::Migration
def self.with_orphaned_milestones
where('NOT EXISTS (SELECT true FROM milestones WHERE milestones.id = issues.milestone_id)')
+ .where('milestone_id IS NOT NULL')
end
end
diff --git a/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
index 8d2ceb8cc18..6395462384b 100644
--- a/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
+++ b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
@@ -15,8 +15,20 @@ class IssuesMovedToIdForeignKey < ActiveRecord::Migration
self.table_name = 'issues'
def self.with_orphaned_moved_to_issues
- where('NOT EXISTS (SELECT true FROM issues WHERE issues.id = issues.moved_to_id)')
- .where('moved_to_id IS NOT NULL')
+ if Gitlab::Database.postgresql?
+ # Be careful to use a second table here for comparison otherwise we'll null
+ # out all rows that don't have id == moved.to_id!
+ where('NOT EXISTS (SELECT true FROM issues B WHERE issues.moved_to_id = B.id)')
+ .where('moved_to_id IS NOT NULL')
+ else
+ # MySQL doesn't allow modification of the same table in a subquery,
+ # and using a temporary table isn't automatically guaranteed to work
+ # due to the MySQL query optimizer. See
+ # https://dev.mysql.com/doc/refman/5.7/en/update.html for more
+ # details.
+ joins('LEFT JOIN issues AS b ON issues.moved_to_id = b.id')
+ .where('issues.moved_to_id IS NOT NULL AND b.id IS NULL')
+ end
end
end
diff --git a/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb b/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb
index f141c442d97..328cc65a549 100644
--- a/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb
+++ b/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb
@@ -4,6 +4,12 @@ class AddNewProjectGuidelinesToAppearances < ActiveRecord::Migration
DOWNTIME = false
def change
+ # Clears the current Appearance cache otherwise it breaks since
+ # new_project_guidelines_html would be missing. See
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/41041
+ # We're not using Appearance#flush_redis_cache on purpose here.
+ Rails.cache.delete('current_appearance')
+
change_table :appearances do |t|
t.text :new_project_guidelines
t.text :new_project_guidelines_html
diff --git a/db/migrate/20171127151038_add_events_related_columns_to_merge_request_metrics.rb b/db/migrate/20171127151038_add_events_related_columns_to_merge_request_metrics.rb
new file mode 100644
index 00000000000..18af697cf88
--- /dev/null
+++ b/db/migrate/20171127151038_add_events_related_columns_to_merge_request_metrics.rb
@@ -0,0 +1,37 @@
+class AddEventsRelatedColumnsToMergeRequestMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ change_table :merge_request_metrics do |t|
+ t.references :merged_by, references: :users
+ t.references :latest_closed_by, references: :users
+ end
+
+ add_column :merge_request_metrics, :latest_closed_at, :datetime_with_timezone
+
+ add_concurrent_foreign_key :merge_request_metrics, :users,
+ column: :merged_by_id,
+ on_delete: :nullify
+
+ add_concurrent_foreign_key :merge_request_metrics, :users,
+ column: :latest_closed_by_id,
+ on_delete: :nullify
+ end
+
+ def down
+ if foreign_keys_for(:merge_request_metrics, :merged_by_id).any?
+ remove_foreign_key :merge_request_metrics, column: :merged_by_id
+ end
+
+ if foreign_keys_for(:merge_request_metrics, :latest_closed_by_id).any?
+ remove_foreign_key :merge_request_metrics, column: :latest_closed_by_id
+ end
+
+ remove_columns :merge_request_metrics,
+ :merged_by_id, :latest_closed_by_id, :latest_closed_at
+ end
+end
diff --git a/db/migrate/20171204204233_add_permanent_to_redirect_route.rb b/db/migrate/20171204204233_add_permanent_to_redirect_route.rb
new file mode 100644
index 00000000000..f3ae471201e
--- /dev/null
+++ b/db/migrate/20171204204233_add_permanent_to_redirect_route.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 AddPermanentToRedirectRoute < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column(:redirect_routes, :permanent, :boolean)
+ end
+
+ def down
+ remove_column(:redirect_routes, :permanent)
+ end
+end
diff --git a/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb b/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb
new file mode 100644
index 00000000000..33ce7e1aa68
--- /dev/null
+++ b/db/migrate/20171206221519_add_permanent_index_to_redirect_route.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 AddPermanentIndexToRedirectRoute < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:redirect_routes, :permanent)
+ end
+
+ def down
+ remove_concurrent_index(:redirect_routes, :permanent) if index_exists?(:redirect_routes, :permanent)
+ end
+end
diff --git a/db/migrate/20171212203433_create_clusters_applications_prometheus.rb b/db/migrate/20171212203433_create_clusters_applications_prometheus.rb
new file mode 100644
index 00000000000..dc2531d2691
--- /dev/null
+++ b/db/migrate/20171212203433_create_clusters_applications_prometheus.rb
@@ -0,0 +1,18 @@
+class CreateClustersApplicationsPrometheus < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_prometheus do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.integer :status, null: false
+ t.string :version, null: false
+
+ t.text :status_reason
+
+ t.timestamps_with_timezone null: false
+ end
+ end
+end
diff --git a/db/migrate/20171216111734_clean_up_for_members.rb b/db/migrate/20171216111734_clean_up_for_members.rb
new file mode 100644
index 00000000000..22e0997dce6
--- /dev/null
+++ b/db/migrate/20171216111734_clean_up_for_members.rb
@@ -0,0 +1,31 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanUpForMembers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Member < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'members'
+ end
+
+ def up
+ condition = <<~EOF.squish
+ invite_token IS NULL AND
+ NOT EXISTS (SELECT 1 FROM users WHERE users.id = members.user_id)
+ EOF
+
+ Member.each_batch(of: 10_000) do |batch|
+ batch.where(condition).delete_all
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20171216112339_add_foreign_key_for_members.rb b/db/migrate/20171216112339_add_foreign_key_for_members.rb
new file mode 100644
index 00000000000..be17769be6a
--- /dev/null
+++ b/db/migrate/20171216112339_add_foreign_key_for_members.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 AddForeignKeyForMembers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:members,
+ :users,
+ column: :user_id)
+ end
+
+ def down
+ remove_foreign_key(:members, column: :user_id)
+ end
+end
diff --git a/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb
new file mode 100644
index 00000000000..7cf1d0cec68
--- /dev/null
+++ b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb
@@ -0,0 +1,30 @@
+class AddIndexOnNamespacesLowerName < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+ INDEX_NAME = 'index_on_namespaces_lower_name'
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+ if Gitlab::Database.version.to_f >= 9.5
+ # Allow us to hot-patch the index manually ahead of the migration
+ execute "CREATE INDEX CONCURRENTLY IF NOT EXISTS #{INDEX_NAME} ON namespaces (lower(name));"
+ else
+ execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON namespaces (lower(name));"
+ end
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+ if Gitlab::Database.version.to_f >= 9.2
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};"
+ else
+ execute "DROP INDEX IF EXISTS #{INDEX_NAME};"
+ end
+ end
+end
diff --git a/db/migrate/20171222183504_add_jobs_cache_index_to_project.rb b/db/migrate/20171222183504_add_jobs_cache_index_to_project.rb
new file mode 100644
index 00000000000..607e9d027d7
--- /dev/null
+++ b/db/migrate/20171222183504_add_jobs_cache_index_to_project.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddJobsCacheIndexToProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :jobs_cache_index, :integer
+ end
+end
diff --git a/db/migrate/20171229225929_change_user_project_limit_not_null_and_remove_default.rb b/db/migrate/20171229225929_change_user_project_limit_not_null_and_remove_default.rb
new file mode 100644
index 00000000000..54fbbcf1a0d
--- /dev/null
+++ b/db/migrate/20171229225929_change_user_project_limit_not_null_and_remove_default.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ChangeUserProjectLimitNotNullAndRemoveDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def up
+ # Set Users#projects_limit to NOT NULL and remove the default value
+ change_column_null :users, :projects_limit, false
+ change_column_default :users, :projects_limit, nil
+ end
+
+ def down
+ change_column_null :users, :projects_limit, true
+ change_column_default :users, :projects_limit, 10
+ end
+end
diff --git a/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests_ce.rb b/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests_ce.rb
new file mode 100644
index 00000000000..94a7c1019d8
--- /dev/null
+++ b/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests_ce.rb
@@ -0,0 +1,15 @@
+class AddRebaseCommitShaToMergeRequestsCe < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:merge_requests, :rebase_commit_sha)
+ add_column :merge_requests, :rebase_commit_sha, :string
+ end
+ end
+
+ def down
+ if column_exists?(:merge_requests, :rebase_commit_sha)
+ remove_column :merge_requests, :rebase_commit_sha
+ end
+ end
+end
diff --git a/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb b/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb
new file mode 100644
index 00000000000..006d17b4d62
--- /dev/null
+++ b/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb
@@ -0,0 +1,48 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveAssigneeIdFromIssue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+
+ include ::EachBatch
+ end
+
+ def up
+ remove_column :issues, :assignee_id
+ end
+
+ def down
+ add_column :issues, :assignee_id, :integer
+ add_concurrent_index :issues, :assignee_id
+
+ update_value = Arel.sql('(SELECT user_id FROM issue_assignees WHERE issue_assignees.issue_id = issues.id LIMIT 1)')
+
+ # This is only used in the down step, so we can ignore the RuboCop warning
+ # about large tables, as this is very unlikely to be run on GitLab.com
+ update_column_in_batches(:issues, :assignee_id, update_value) # rubocop:disable Migration/UpdateLargeTable
+ end
+end
diff --git a/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
index 3e84b295be4..033019c398e 100644
--- a/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
+++ b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
@@ -2,36 +2,12 @@
# for more information on how to write migrations for GitLab.
class DeleteConflictingRedirectRoutes < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
- MIGRATION = 'DeleteConflictingRedirectRoutesRange'.freeze
- BATCH_SIZE = 200 # At 200, I expect under 20s per batch, which is under our query timeout of 60s.
- DELAY_INTERVAL = 12.seconds
-
- disable_ddl_transaction!
-
- class Route < ActiveRecord::Base
- include EachBatch
-
- self.table_name = 'routes'
- end
-
def up
- say opening_message
-
- queue_background_migration_jobs_by_range_at_intervals(Route, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ # No-op.
+ # See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252
end
def down
# nothing
end
-
- def opening_message
- <<~MSG
- Clean up redirect routes that conflict with regular routes.
- See initial bug fix:
- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13357
- MSG
- end
end
diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/post_migrate/20171106154015_remove_issues_branch_name.rb
index 3d08225c96d..162b6bafab4 100644
--- a/db/migrate/20171106154015_remove_issues_branch_name.rb
+++ b/db/post_migrate/20171106154015_remove_issues_branch_name.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/RemoveColumn
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb b/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb
new file mode 100644
index 00000000000..11b581e4b57
--- /dev/null
+++ b/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb
@@ -0,0 +1,151 @@
+class MigrateKubernetesServiceToNewClustersArchitectures < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME = 'KubernetesService'.freeze
+
+ disable_ddl_transaction!
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
+ has_many :clusters, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
+ has_many :services, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service'
+ has_one :kubernetes_service, -> { where(category: 'deployment', type: 'KubernetesService') }, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service', inverse_of: :project, foreign_key: :project_id
+ end
+
+ class Cluster < ActiveRecord::Base
+ self.table_name = 'clusters'
+
+ has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
+ has_many :projects, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
+ has_one :platform_kubernetes, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::PlatformsKubernetes'
+
+ accepts_nested_attributes_for :platform_kubernetes
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+ end
+
+ class ClustersProject < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
+ belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
+ end
+
+ class PlatformsKubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+
+ belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+ end
+
+ class Service < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'services'
+ self.inheritance_column = :_type_disabled # Disable STI, otherwise KubernetesModel will be looked up
+
+ belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project', foreign_key: :project_id
+
+ scope :unmanaged_kubernetes_service, -> do
+ joins('LEFT JOIN projects ON projects.id = services.project_id')
+ .joins('LEFT JOIN cluster_projects ON cluster_projects.project_id = projects.id')
+ .joins('LEFT JOIN cluster_platforms_kubernetes ON cluster_platforms_kubernetes.cluster_id = cluster_projects.cluster_id')
+ .where(category: 'deployment', type: 'KubernetesService', template: false)
+ .where("services.properties LIKE '%api_url%'")
+ .where("(services.properties NOT LIKE CONCAT('%', cluster_platforms_kubernetes.api_url, '%')) OR cluster_platforms_kubernetes.api_url IS NULL")
+ .group(:id)
+ .order(id: :asc)
+ end
+
+ scope :kubernetes_service_without_template, -> do
+ where(category: 'deployment', type: 'KubernetesService', template: false)
+ end
+
+ def api_url
+ parsed_properties['api_url']
+ end
+
+ def ca_pem
+ parsed_properties['ca_pem']
+ end
+
+ def namespace
+ parsed_properties['namespace']
+ end
+
+ def token
+ parsed_properties['token']
+ end
+
+ private
+
+ def parsed_properties
+ @parsed_properties ||= JSON.parse(self.properties)
+ end
+ end
+
+ def find_dedicated_environement_scope(project)
+ environment_scopes = project.clusters.map(&:environment_scope)
+
+ return '*' if environment_scopes.exclude?('*') # KubernetesService should be added as a default cluster (environment_scope: '*') at first place
+ return 'migrated/*' if environment_scopes.exclude?('migrated/*') # If it's conflicted, the KubernetesService added as a migrated cluster
+
+ unique_iid = 0
+
+ # If it's still conflicted, finding an unique environment scope incrementaly
+ loop do
+ candidate = "migrated#{unique_iid}/*"
+ return candidate if environment_scopes.exclude?(candidate)
+
+ unique_iid += 1
+ end
+ end
+
+ def up
+ ActiveRecord::Base.transaction do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service
+ .unmanaged_kubernetes_service.find_each(batch_size: 1) do |kubernetes_service|
+ MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create(
+ enabled: kubernetes_service.active,
+ user_id: nil, # KubernetesService doesn't have
+ name: DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME,
+ provider_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.provider_types[:user],
+ platform_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.platform_types[:kubernetes],
+ projects: [kubernetes_service.project],
+ environment_scope: find_dedicated_environement_scope(kubernetes_service.project),
+ platform_kubernetes_attributes: {
+ api_url: kubernetes_service.api_url,
+ ca_cert: kubernetes_service.ca_pem,
+ namespace: kubernetes_service.namespace,
+ username: nil, # KubernetesService doesn't have
+ encrypted_password: nil, # KubernetesService doesn't have
+ encrypted_password_iv: nil, # KubernetesService doesn't have
+ token: kubernetes_service.token # encrypted_token and encrypted_token_iv
+ } )
+ end
+ end
+
+ MigrateKubernetesServiceToNewClustersArchitectures::Service
+ .kubernetes_service_without_template.each_batch(of: 100) do |kubernetes_service|
+ kubernetes_service.update_all(active: false)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb b/db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb
new file mode 100644
index 00000000000..547cc68e10e
--- /dev/null
+++ b/db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+# rubocop:disable GitlabSecurity/SqlInjection
+
+class SchedulePopulateMergeRequestMetricsWithEventsData < ActiveRecord::Migration
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+ MIGRATION = 'PopulateMergeRequestMetricsWithEventsData'
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ def up
+ merge_requests = MergeRequest.where("id IN (#{updatable_merge_requests_union_sql})").reorder(:id)
+
+ say 'Scheduling `PopulateMergeRequestMetricsWithEventsData` jobs'
+ # It will update around 4_000_000 records in batches of 10_000 merge
+ # requests (running between 10 minutes) and should take around 66 hours to complete.
+ # Apparently, production PostgreSQL is able to vacuum 10k-20k dead_tuples by
+ # minute, and at maximum, each of these jobs should UPDATE 20k records.
+ #
+ # More information about the updates in `PopulateMergeRequestMetricsWithEventsData` class.
+ #
+ merge_requests.each_batch(of: BATCH_SIZE) do |relation, index|
+ range = relation.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * 10.minutes, MIGRATION, range)
+ end
+ end
+
+ def down
+ execute "update merge_request_metrics set latest_closed_at = null"
+ execute "update merge_request_metrics set latest_closed_by_id = null"
+ execute "update merge_request_metrics set merged_by_id = null"
+ end
+
+ private
+
+ # On staging:
+ # Planning time: 0.682 ms
+ # Execution time: 22033.158 ms
+ #
+ def updatable_merge_requests_union_sql
+ metrics_not_exists_clause =
+ 'NOT EXISTS (SELECT 1 FROM merge_request_metrics WHERE merge_request_metrics.merge_request_id = merge_requests.id)'
+
+ without_metrics_data = <<-SQL.strip_heredoc
+ merge_request_metrics.merged_by_id IS NULL OR
+ merge_request_metrics.latest_closed_by_id IS NULL OR
+ merge_request_metrics.latest_closed_at IS NULL
+ SQL
+
+ mrs_without_metrics_record = MergeRequest
+ .where(metrics_not_exists_clause)
+ .select(:id)
+
+ mrs_without_events_data = MergeRequest
+ .joins('INNER JOIN merge_request_metrics ON merge_requests.id = merge_request_metrics.merge_request_id')
+ .where(without_metrics_data)
+ .select(:id)
+
+ Gitlab::SQL::Union.new([mrs_without_metrics_record, mrs_without_events_data]).to_sql
+ end
+end
diff --git a/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb b/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb
new file mode 100644
index 00000000000..149c28f1946
--- /dev/null
+++ b/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateGithubImporterAdvanceStageSidekiqQueue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ sidekiq_queue_migrate 'github_importer_advance_stage', to: 'github_import_advance_stage'
+ end
+
+ def down
+ sidekiq_queue_migrate 'github_import_advance_stage', to: 'github_importer_advance_stage'
+ end
+end
diff --git a/db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb b/db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb
new file mode 100644
index 00000000000..286721a0894
--- /dev/null
+++ b/db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class NormalizeExternUidFromIdentities < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'NormalizeLdapExternUidsRange'.freeze
+ DELAY_INTERVAL = 10.seconds
+
+ disable_ddl_transaction!
+
+ class Identity < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'identities'
+ end
+
+ def up
+ ldap_identities = Identity.where("provider like 'ldap%'")
+
+ if ldap_identities.any?
+ queue_background_migration_jobs_by_range_at_intervals(Identity, MIGRATION, DELAY_INTERVAL)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb b/db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb
new file mode 100644
index 00000000000..eeecc7b1de0
--- /dev/null
+++ b/db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb
@@ -0,0 +1,45 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/Datetime
+class ScheduleIssuesClosedAtTypeChange < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ include EachBatch
+
+ def self.to_migrate
+ where('closed_at IS NOT NULL')
+ end
+ end
+
+ def up
+ return unless migrate_column_type?
+
+ change_column_type_using_background_migration(
+ Issue.to_migrate,
+ :closed_at,
+ :datetime_with_timezone
+ )
+ end
+
+ def down
+ return if migrate_column_type?
+
+ change_column_type_using_background_migration(
+ Issue.to_migrate,
+ :closed_at,
+ :datetime
+ )
+ 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
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8aa6a87657e..02c44bccc61 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: 20171205190711) do
+ActiveRecord::Schema.define(version: 20171230123729) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -571,6 +571,15 @@ ActiveRecord::Schema.define(version: 20171205190711) do
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
+ t.string "version", null: false
+ t.text "status_reason"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -651,8 +660,8 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "confirmation_token"
- t.datetime "confirmed_at"
- t.datetime "confirmation_sent_at"
+ t.datetime_with_timezone "confirmed_at"
+ t.datetime_with_timezone "confirmation_sent_at"
end
add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
@@ -844,7 +853,6 @@ ActiveRecord::Schema.define(version: 20171205190711) do
create_table "issues", force: :cascade do |t|
t.string "title"
- t.integer "assignee_id"
t.integer "author_id"
t.integer "project_id"
t.datetime "created_at"
@@ -870,7 +878,6 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.datetime_with_timezone "closed_at"
end
- add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
@@ -1053,6 +1060,9 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "pipeline_id"
+ t.integer "merged_by_id"
+ t.integer "latest_closed_by_id"
+ t.datetime_with_timezone "latest_closed_at"
end
add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
@@ -1093,6 +1103,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.string "merge_jid"
t.boolean "discussion_locked"
t.integer "latest_merge_request_diff_id"
+ t.string "rebase_commit_sha"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -1444,6 +1455,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.boolean "repository_read_only"
t.boolean "merge_requests_ff_only_enabled", default: false
t.boolean "merge_requests_rebase_enabled", default: false, null: false
+ t.integer "jobs_cache_index"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1530,10 +1542,12 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.string "path", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "permanent"
end
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
+ add_index "redirect_routes", ["permanent"], name: "index_redirect_routes_on_permanent", using: :btree
add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
create_table "releases", force: :cascade do |t|
@@ -1765,8 +1779,8 @@ ActiveRecord::Schema.define(version: 20171205190711) do
add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
create_table "user_custom_attributes", force: :cascade do |t|
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
t.integer "user_id", null: false
t.string "key", null: false
t.string "value", null: false
@@ -1800,7 +1814,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.datetime "updated_at"
t.string "name"
t.boolean "admin", default: false, null: false
- t.integer "projects_limit", default: 10
+ t.integer "projects_limit", null: false
t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
@@ -1984,11 +1998,14 @@ ActiveRecord::Schema.define(version: 20171205190711) do
add_foreign_key "labels", "projects", name: "fk_7de4989a69", on_delete: :cascade
add_foreign_key "lists", "boards", name: "fk_0d3f677137", on_delete: :cascade
add_foreign_key "lists", "labels", name: "fk_7a5553d60f", on_delete: :cascade
+ add_foreign_key "members", "users", name: "fk_2e88fb7ce9", on_delete: :cascade
add_foreign_key "merge_request_diff_commits", "merge_request_diffs", on_delete: :cascade
add_foreign_key "merge_request_diff_files", "merge_request_diffs", on_delete: :cascade
add_foreign_key "merge_request_diffs", "merge_requests", name: "fk_8483f3258f", on_delete: :cascade
add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
+ add_foreign_key "merge_request_metrics", "users", column: "latest_closed_by_id", name: "fk_ae440388cc", on_delete: :nullify
+ add_foreign_key "merge_request_metrics", "users", column: "merged_by_id", name: "fk_7f28d925f3", on_delete: :nullify
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify
add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index ee9b9a9466a..373d4239f71 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -14,3 +14,4 @@ providers.
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
+- [Authentiq](authentiq.md): Enable the Authentiq OmniAuth provider for passwordless authentication
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/img/gitlab_ou.png b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/gitlab_ou.png
index 11ce324f938..11ce324f938 100644
--- a/doc/articles/how_to_configure_ldap_gitlab_ce/img/gitlab_ou.png
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/gitlab_ou.png
Binary files differ
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/img/ldap_ou.gif b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/ldap_ou.gif
index a6727a3d85f..a6727a3d85f 100644
--- a/doc/articles/how_to_configure_ldap_gitlab_ce/img/ldap_ou.gif
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/ldap_ou.gif
Binary files differ
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/img/user_auth.gif b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/user_auth.gif
index 36e6085259f..36e6085259f 100644
--- a/doc/articles/how_to_configure_ldap_gitlab_ce/img/user_auth.gif
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/img/user_auth.gif
Binary files differ
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
new file mode 100644
index 00000000000..9b9b8ca89e5
--- /dev/null
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
@@ -0,0 +1,267 @@
+# How to configure LDAP with GitLab CE
+
+> **[Article Type](../../../development/writing_documentation.html#types-of-technical-articles):** admin guide ||
+> **Level:** intermediary ||
+> **Author:** [Chris Wilson](https://gitlab.com/MrChrisW) ||
+> **Publication date:** 2017-05-03
+
+## Introduction
+
+Managing a large number of users in GitLab can become a burden for system administrators. As an organization grows so do user accounts. Keeping these user accounts in sync across multiple enterprise applications often becomes a time consuming task.
+
+In this guide we will focus on configuring GitLab with Active Directory. [Active Directory](https://en.wikipedia.org/wiki/Active_Directory) is a popular LDAP compatible directory service provided by Microsoft, included in all modern Windows Server operating systems.
+
+GitLab has supported LDAP integration since [version 2.2](https://about.gitlab.com/2012/02/22/gitlab-version-2-2/). With GitLab LDAP [group syncing](#group-syncing-ee) being added to GitLab Enterprise Edition in [version 6.0](https://about.gitlab.com/2013/08/20/gitlab-6-dot-0-released/). LDAP integration has become one of the most popular features in GitLab.
+
+## Getting started
+
+### Choosing an LDAP Server
+
+The main reason organizations choose to utilize a LDAP server is to keep the entire organization's user base consolidated into a central repository. Users can access multiple applications and systems across the IT environment using a single login. Because LDAP is an open, vendor-neutral, industry standard application protocol, the number of applications using LDAP authentication continues to increase.
+
+There are many commercial and open source [directory servers](https://en.wikipedia.org/wiki/Directory_service#LDAP_implementations) that support the LDAP protocol. Deciding on the right directory server highly depends on the existing IT environment in which the server will be integrated with.
+
+For example, [Active Directory](https://technet.microsoft.com/en-us/library/hh831484(v=ws.11).aspx) is generally favored in a primarily Windows environment, as this allows quick integration with existing services. Other popular directory services include:
+
+- [Oracle Internet Directory](http://www.oracle.com/technetwork/middleware/id-mgmt/overview/index-082035.html)
+- [OpenLDAP](http://www.openldap.org/)
+- [389 Directory](http://directory.fedoraproject.org/)
+- [OpenDJ](https://forgerock.org/opendj/)
+- [ApacheDS](https://directory.apache.org/)
+
+> GitLab uses the [Net::LDAP](https://rubygems.org/gems/net-ldap) library under the hood. This means it supports all [IETF](https://tools.ietf.org/html/rfc2251) compliant LDAPv3 servers.
+
+### Active Directory (AD)
+
+We won't cover the installation and configuration of Windows Server or Active Directory Domain Services in this tutorial. There are a number of resources online to guide you through this process:
+
+- Install Windows Server 2012 - (_technet.microsoft.com_) - [Installing Windows Server 2012 ](https://technet.microsoft.com/en-us/library/jj134246(v=ws.11).aspx)
+
+- Install Active Directory Domain Services (AD DS) (_technet.microsoft.com_)- [Install Active Directory Domain Services](https://technet.microsoft.com/windows-server-docs/identity/ad-ds/deploy/install-active-directory-domain-services--level-100-#BKMK_PS)
+
+> **Shortcut:** You can quickly install AD DS via PowerShell using
+`Install-WindowsFeature AD-Domain-Services -IncludeManagementTools`
+
+### Creating an AD **OU** structure
+
+Configuring organizational units (**OU**s) is an important part of setting up Active Directory. **OU**s form the base for an entire organizational structure. Using GitLab as an example we have designed the **OU** structure below using the geographic **OU** model. In the Geographic Model we separate **OU**s for different geographic regions.
+
+| GitLab **OU** Design | GitLab AD Structure |
+| :----------------------------: | :------------------------------: |
+| ![GitLab OU Design][gitlab_ou] | ![GitLab AD Structure][ldap_ou] |
+
+[gitlab_ou]: img/gitlab_ou.png
+[ldap_ou]: img/ldap_ou.gif
+
+Using PowerShell you can output the **OU** structure as a table (_all names are examples only_):
+
+```ps
+Get-ADObject -LDAPFilter "(objectClass=*)" -SearchBase 'OU=GitLab INT,DC=GitLab,DC=org' -Properties CanonicalName | Format-Table Name,CanonicalName -A
+```
+
+```
+OU CanonicalName
+---- -------------
+GitLab INT GitLab.org/GitLab INT
+United States GitLab.org/GitLab INT/United States
+Developers GitLab.org/GitLab INT/United States/Developers
+Gary Johnson GitLab.org/GitLab INT/United States/Developers/Gary Johnson
+Ellis Matthews GitLab.org/GitLab INT/United States/Developers/Ellis Matthews
+William Collins GitLab.org/GitLab INT/United States/Developers/William Collins
+People Ops GitLab.org/GitLab INT/United States/People Ops
+Margaret Baker GitLab.org/GitLab INT/United States/People Ops/Margaret Baker
+Libby Hartzler GitLab.org/GitLab INT/United States/People Ops/Libby Hartzler
+Victoria Ryles GitLab.org/GitLab INT/United States/People Ops/Victoria Ryles
+The Netherlands GitLab.org/GitLab INT/The Netherlands
+Developers GitLab.org/GitLab INT/The Netherlands/Developers
+John Doe GitLab.org/GitLab INT/The Netherlands/Developers/John Doe
+Jon Mealy GitLab.org/GitLab INT/The Netherlands/Developers/Jon Mealy
+Jane Weingarten GitLab.org/GitLab INT/The Netherlands/Developers/Jane Weingarten
+Production GitLab.org/GitLab INT/The Netherlands/Production
+Sarah Konopka GitLab.org/GitLab INT/The Netherlands/Production/Sarah Konopka
+Cynthia Bruno GitLab.org/GitLab INT/The Netherlands/Production/Cynthia Bruno
+David George GitLab.org/GitLab INT/The Netherlands/Production/David George
+United Kingdom GitLab.org/GitLab INT/United Kingdom
+Developers GitLab.org/GitLab INT/United Kingdom/Developers
+Leroy Fox GitLab.org/GitLab INT/United Kingdom/Developers/Leroy Fox
+Christopher Alley GitLab.org/GitLab INT/United Kingdom/Developers/Christopher Alley
+Norris Morita GitLab.org/GitLab INT/United Kingdom/Developers/Norris Morita
+Support GitLab.org/GitLab INT/United Kingdom/Support
+Laura Stanley GitLab.org/GitLab INT/United Kingdom/Support/Laura Stanley
+Nikki Schuman GitLab.org/GitLab INT/United Kingdom/Support/Nikki Schuman
+Harriet Butcher GitLab.org/GitLab INT/United Kingdom/Support/Harriet Butcher
+Global Groups GitLab.org/GitLab INT/Global Groups
+DevelopersNL GitLab.org/GitLab INT/Global Groups/DevelopersNL
+DevelopersUK GitLab.org/GitLab INT/Global Groups/DevelopersUK
+DevelopersUS GitLab.org/GitLab INT/Global Groups/DevelopersUS
+ProductionNL GitLab.org/GitLab INT/Global Groups/ProductionNL
+SupportUK GitLab.org/GitLab INT/Global Groups/SupportUK
+People Ops US GitLab.org/GitLab INT/Global Groups/People Ops US
+Global Admins GitLab.org/GitLab INT/Global Groups/Global Admins
+```
+
+> See [more information](https://technet.microsoft.com/en-us/library/ff730967.aspx) on searching Active Directory with Windows PowerShell from [The Scripting Guys](https://technet.microsoft.com/en-us/scriptcenter/dd901334.aspx)
+
+## 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 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)
+
+> Both group_base and admin_group configuration options are only available in GitLab Enterprise Edition. See [GitLab EE - LDAP Features](#gitlab-enterprise-edition---ldap-features)
+
+### Example `gitlab.rb` LDAP
+
+```
+gitlab_rails['ldap_enabled'] = true
+gitlab_rails['ldap_servers'] = {
+'main' => {
+ 'label' => 'GitLab AD',
+ 'host' => 'ad.example.org',
+ 'port' => 636,
+ 'uid' => 'sAMAccountName',
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true,
+ 'bind_dn' => 'CN=GitLabSRV,CN=Users,DC=GitLab,DC=org',
+ 'password' => 'Password1',
+ 'active_directory' => true,
+ 'base' => 'OU=GitLab INT,DC=GitLab,DC=org',
+ 'group_base' => 'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org',
+ 'admin_group' => 'Global Admins'
+ }
+}
+```
+
+> **Note:** Remember to run `gitlab-ctl reconfigure` after modifying `gitlab.rb`
+
+## Security improvements (LDAPS)
+
+Security is an important aspect when deploying an LDAP server. By default, LDAP traffic is transmitted unsecured. LDAP can be secured using SSL/TLS called LDAPS, or commonly "LDAP over SSL".
+
+Securing LDAP (enabling LDAPS) on Windows Server 2012 involves installing a valid SSL certificate. For full details see Microsoft's guide [How to enable LDAP over SSL with a third-party certification authority](https://support.microsoft.com/en-us/help/321051/how-to-enable-ldap-over-ssl-with-a-third-party-certification-authority)
+
+> By default a LDAP service listens for connections on TCP and UDP port 389. LDAPS (LDAP over SSL) listens on port 636
+
+### Testing you AD server
+
+#### Using **AdFind** (Windows)
+
+You can use the [`AdFind`](https://social.technet.microsoft.com/wiki/contents/articles/7535.adfind-command-examples.aspx) utility (on Windows based systems) to test that your LDAP server is accessible and authentication is working correctly. This is a freeware utility built by [Joe Richards](http://www.joeware.net/freetools/tools/adfind/index.htm).
+
+**Return all objects**
+
+You can use the filter `objectclass=*` to return all directory objects.
+
+```sh
+adfind -h ad.example.org:636 -ssl -u "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -up Password1 -b "OU=GitLab INT,DC=GitLab,DC=org" -f (objectClass=*)
+```
+
+**Return single object using filter**
+
+You can also retrieve a single object by **specifying** the object name or full **DN**. In this example we specify the object name only `CN=Leroy Fox`.
+
+```sh
+adfind -h ad.example.org:636 -ssl -u "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -up Password1 -b "OU=GitLab INT,DC=GitLab,DC=org" -f (&(objectcategory=person)(CN=Leroy Fox))â€
+```
+
+#### Using **ldapsearch** (Unix)
+
+You can use the `ldapsearch` utility (on Unix based systems) to test that your LDAP server is accessible and authentication is working correctly. This utility is included in the [`ldap-utils`](https://wiki.debian.org/LDAP/LDAPUtils) package.
+
+**Return all objects**
+
+You can use the filter `objectclass=*` to return all directory objects.
+
+```sh
+ldapsearch -D "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" \
+-w Password1 -p 636 -h ad.example.org \
+-b "OU=GitLab INT,DC=GitLab,DC=org" -Z \
+-s sub "(objectclass=*)"
+```
+
+**Return single object using filter**
+
+You can also retrieve a single object by **specifying** the object name or full **DN**. In this example we specify the object name only `CN=Leroy Fox`.
+
+```sh
+ldapsearch -D "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -w Password1 -p 389 -h ad.example.org -b "OU=GitLab INT,DC=GitLab,DC=org" -Z -s sub "CN=Leroy Fox"
+```
+
+**Full output of `ldapsearch` command:** - Filtering for _CN=Leroy Fox_
+
+```
+# LDAPv3
+# base <OU=GitLab INT,DC=GitLab,DC=org> with scope subtree
+# filter: CN=Leroy Fox
+# requesting: ALL
+#
+
+# Leroy Fox, Developers, United Kingdom, GitLab INT, GitLab.org
+dn: CN=Leroy Fox,OU=Developers,OU=United Kingdom,OU=GitLab INT,DC=GitLab,DC=or
+ g
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: user
+cn: Leroy Fox
+sn: Fox
+givenName: Leroy
+distinguishedName: CN=Leroy Fox,OU=Developers,OU=United Kingdom,OU=GitLab INT,
+ DC=GitLab,DC=org
+instanceType: 4
+whenCreated: 20170210030500.0Z
+whenChanged: 20170213050128.0Z
+displayName: Leroy Fox
+uSNCreated: 16790
+memberOf: CN=DevelopersUK,OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org
+uSNChanged: 20812
+name: Leroy Fox
+objectGUID:: rBCAo6NR6E6vfSKgzcUILg==
+userAccountControl: 512
+badPwdCount: 0
+codePage: 0
+countryCode: 0
+badPasswordTime: 0
+lastLogoff: 0
+lastLogon: 0
+pwdLastSet: 131311695009850084
+primaryGroupID: 513
+objectSid:: AQUAAAAAAAUVAAAA9GMAb7tdJZvsATf7ZwQAAA==
+accountExpires: 9223372036854775807
+logonCount: 0
+sAMAccountName: Leroyf
+sAMAccountType: 805306368
+userPrincipalName: Leroyf@GitLab.org
+objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=GitLab,DC=org
+dSCorePropagationData: 16010101000000.0Z
+lastLogonTimestamp: 131314356887754250
+
+# search result
+search: 2
+result: 0 Success
+
+# numResponses: 2
+# numEntries: 1
+```
+
+## Basic user authentication
+
+After configuring LDAP, basic authentication will be available. Users can then login using their directory credentials. An extra tab is added to the GitLab login screen for the configured LDAP server (e.g "**GitLab AD**").
+
+![GitLab OU Structure](img/user_auth.gif)
+
+Users that are removed from the LDAP base group (e.g `OU=GitLab INT,DC=GitLab,DC=org`) will be **blocked** in GitLab. [More information](../ldap.md#security) on LDAP security.
+
+If `allow_username_or_email_login` is enabled in the LDAP configuration, GitLab will ignore everything after the first '@' in the LDAP username used on login. Example: The username `jon.doe@example.com` is converted to `jon.doe` when authenticating with the LDAP server. Disable this setting if you use `userPrincipalName` as the `uid`.
+
+## LDAP extended features on GitLab EE
+
+With [GitLab Enterprise Edition (EE)](https://about.gitlab.com/gitlab-ee/), besides everything we just described, you'll
+have extended functionalities with LDAP, such as:
+
+- Group sync
+- Group permissions
+- Updating user permissions
+- Multiple LDAP servers
+
+Read through the article on [LDAP for GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/) for an overview.
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 6b5a0f139c5..881b6a827f4 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -38,6 +38,9 @@ in the application settings.
## Configuration
+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`.
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index a88e67bfeb5..ea8077f0623 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -37,6 +37,7 @@ Follow the steps below to configure an active/active setup:
1. [Configure the database](database.md)
1. [Configure Redis](redis.md)
+ 1. [Configure Redis for GitLab source installations](redis_source.md)
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 42666357faf..b85a166089d 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -1,6 +1,6 @@
# Configuring GitLab for HA
-Assuming you have already configured a database, Redis, and NFS, you can
+Assuming you have already configured a [database](database.md), [Redis](redis.md), and [NFS](nfs.md), you can
configure the GitLab application server(s) now. Complete the steps below
for each GitLab application server in your environment.
@@ -48,34 +48,33 @@ for each GitLab application server in your environment.
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.
-
+
```ruby
external_url 'https://gitlab.example.com'
# Prevent GitLab from starting if NFS data mounts are not available
high_availability['mountpoint'] = '/var/opt/gitlab/git-data'
-
+
# Disable components that will not be on the GitLab application server
- postgresql['enable'] = false
- redis['enable'] = false
-
+ roles ['application_role']
+
# PostgreSQL connection details
gitlab_rails['db_adapter'] = 'postgresql'
gitlab_rails['db_encoding'] = 'unicode'
gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server
gitlab_rails['db_password'] = 'DB password'
-
+
# Redis connection details
gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server
gitlab_rails['redis_password'] = 'Redis Password'
```
-
- > **Note:** To maintain uniformity of links across HA clusters, the `external_url`
- on the first application server as well as the additional application
- servers should point to the external url that users will use to access GitLab.
+
+ > **Note:** To maintain uniformity of links across HA clusters, the `external_url`
+ on the first application server as well as the additional application
+ 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.
+ route traffic to all GitLab application servers in the HA cluster.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
diff --git a/doc/administration/index.md b/doc/administration/index.md
index c8d28d8485a..0b199eecefd 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -16,26 +16,32 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Install](../install/README.md): Requirements, directory structures, and installation methods.
- [High Availability](high_availability/README.md): Configure multiple servers for scaling or high availability.
+ - [High Availability on AWS](../university/high-availability/aws/README.md): Set up GitLab HA on Amazon AWS.
### Configuring GitLab
- [Adjust your instance's timezone](../workflow/timezone.md): Customize the default time zone of GitLab.
-- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers.
-- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
- [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
- [Security](../security/README.md): Learn what you can do to further secure your GitLab instance.
- [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
- [Polling](polling.md): Configure how often the GitLab UI polls for updates.
- [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages.
-- [GitLab Pages configuration for installations from the source](pages/source.md): Enable and configure GitLab Pages on
+- [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on
[source installations](../install/installation.md#installation-from-source).
- [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
+#### Customizing GitLab's appearance
+
+- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers.
+- [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.
+
### Maintaining GitLab
- [Raketasks](../raketasks/README.md): Perform various tasks for maintenance, backups, automatic webhooks setup, etc.
- [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance.
-- [Operations](operations.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq Job throttling, Sidekiq MemoryKiller, Unicorn).
+- [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq Job throttling, Sidekiq MemoryKiller, Unicorn).
- [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components.
#### Updating GitLab
@@ -74,6 +80,7 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email.
- [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages.
- [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service.
- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
+- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet.
### Repository settings
@@ -99,20 +106,22 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email.
## Monitoring GitLab
-- [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
- - [IP whitelist](monitoring/ip_whitelist.md): Monitor endpoints that provide health check information when probed.
-- [Monitoring GitHub imports](monitoring/github_imports.md): GitLab's GitHub Importer displays Prometheus metrics to monitor the health and progress of the importer.
+- [Monitoring GitLab](monitoring/index.md):
+ - [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
+ - [IP whitelist](monitoring/ip_whitelist.md): Monitor endpoints that provide health check information when probed.
+ - [Monitoring GitHub imports](monitoring/github_imports.md): GitLab's GitHub Importer displays Prometheus metrics to monitor the health and progress of the importer.
- [Conversational Development (ConvDev) Index](../user/admin_area/monitoring/convdev.md): Provides an overview of your entire instance's feature usage.
### Performance Monitoring
-- [GitLab Performance Monitoring](monitoring/performance/gitlab_configuration.md): Enable GitLab Performance Monitoring.
-- [GitLab performance monitoring with InfluxDB](monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics.
- - [InfluxDB Schema](monitoring/performance/influxdb_schema.md): Measurements stored in InfluxDB.
-- [GitLab performance monitoring with Prometheus](monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
-- [GitLab performance monitoring with Grafana](monitoring/prometheus/index.md): Configure GitLab to visualize time series metrics through graphs and dashboards.
-- [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
-- [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page.
+- [GitLab Performance Monitoring](monitoring/performance/index.md):
+ - [Enable Performance Monitoring](monitoring/performance/gitlab_configuration.md): Enable GitLab Performance Monitoring.
+ - [GitLab performance monitoring with InfluxDB](monitoring/performance/influxdb_configuration.md): Configure GitLab and InfluxDB for measuring performance metrics.
+ - [InfluxDB Schema](monitoring/performance/influxdb_schema.md): Measurements stored in InfluxDB.
+ - [GitLab performance monitoring with Prometheus](monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
+ - [GitLab performance monitoring with Grafana](monitoring/performance/grafana_configuration.md): Configure GitLab to visualize time series metrics through graphs and dashboards.
+ - [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
+ - [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page.
## Troubleshooting
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 93c3642a1f1..65f59b72690 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -58,30 +58,32 @@ our AsciiDoc snippets, wikis and repos using delimited blocks:
- **Markdown**
- ```plantuml
- Bob -> Alice : hello
- Alice -> Bob : Go Away
- ```
+ <pre>
+ ```plantuml
+ Bob -> Alice : hello
+ Alice -> Bob : Go Away
+ ```
+ </pre>
- **AsciiDoc**
- ```
+ <pre>
[plantuml, format="png", id="myDiagram", width="200px"]
--
Bob->Alice : hello
Alice -> Bob : Go Away
--
- ```
+ </pre>
- **reStructuredText**
- ```
+ <pre>
.. plantuml::
:caption: Caption with **bold** and *italic*
Bob -> Alice: hello
Alice -> Bob: Go Away
- ```
+ </pre>
You can also use the `uml::` directive for compatibility with [sphinxcontrib-plantuml](https://pypi.python.org/pypi/sphinxcontrib-plantuml), but please note that we currently only support the `caption` option.
diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md
new file mode 100644
index 00000000000..d6333ee62b4
--- /dev/null
+++ b/doc/administration/monitoring/index.md
@@ -0,0 +1,9 @@
+# Monitoring GitLab
+
+Explore our features to monitor your GitLab instance:
+
+- [Performance monitoring](performance/index.md): GitLab Performance Monitoring makes it possible to measure a wide variety of statistics of your instance.
+- [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring service, providing a flexible platform for monitoring GitLab and other software products.
+- [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics.
+- [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
+ - [IP whitelists](ip_whitelist.md): Configure GitLab for monitoring endpoints that provide health check information when probed.
diff --git a/doc/administration/monitoring/performance/index.md b/doc/administration/monitoring/performance/index.md
new file mode 100644
index 00000000000..f5f0363ed38
--- /dev/null
+++ b/doc/administration/monitoring/performance/index.md
@@ -0,0 +1,72 @@
+# GitLab Performance Monitoring
+
+GitLab comes with its own application performance measuring system as of GitLab
+8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
+Community and Enterprise editions.
+
+Apart from this introduction, you are advised to read through the following
+documents in order to understand and properly configure GitLab Performance Monitoring:
+
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Install/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
+- [Performance bar](performance_bar.md)
+- [Request profiling](request_profiling.md)
+
+>**Note:**
+Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect
+metrics. It will eventually replace InfluxDB when their metrics collection is
+on par. Read more in the [Prometheus documentation](../prometheus/index.md).
+
+## Introduction to GitLab Performance Monitoring
+
+GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
+including (but not limited to):
+
+- The time it took to complete a transaction (a web request or Sidekiq job).
+- The time spent in running SQL queries and rendering HAML views.
+- The time spent executing (instrumented) Ruby methods.
+- Ruby object allocations, and retained objects in particular.
+- System statistics such as the process' memory usage and open file descriptors.
+- Ruby garbage collection statistics.
+
+Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
+data can be visualized using [Grafana][grafana] or any other application that
+supports reading data from InfluxDB. Alternatively data can be queried using the
+InfluxDB CLI.
+
+## Metric Types
+
+Two types of metrics are collected:
+
+1. Transaction specific metrics.
+1. Sampled metrics, collected at a certain interval in a separate thread.
+
+### Transaction Metrics
+
+Transaction metrics are metrics that can be associated with a single
+transaction. This includes statistics such as the transaction duration, timings
+of any executed SQL queries, time spent rendering HAML views, etc. These metrics
+are collected for every Rack request and Sidekiq job processed.
+
+### Sampled Metrics
+
+Sampled metrics are metrics that can't be associated with a single transaction.
+Examples include garbage collection statistics and retained Ruby objects. These
+metrics are collected at a regular interval. This interval is made up out of two
+parts:
+
+1. A user defined interval.
+1. A randomly generated offset added on top of the interval, the same offset
+ can't be used twice in a row.
+
+The actual interval can be anywhere between a half of the defined interval and a
+half above the interval. For example, for a user defined interval of 15 seconds
+the actual interval can be anywhere between 7.5 and 22.5. The interval is
+re-generated for every sampling run instead of being generated once and re-used
+for the duration of the process' lifetime.
+
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[grafana]: http://grafana.org/
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
index 17c2b4b70d3..37a5388d2fc 100644
--- a/doc/administration/monitoring/performance/introduction.md
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -1,70 +1 @@
-# GitLab Performance Monitoring
-
-GitLab comes with its own application performance measuring system as of GitLab
-8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
-Community and Enterprise editions.
-
-Apart from this introduction, you are advised to read through the following
-documents in order to understand and properly configure GitLab Performance Monitoring:
-
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Install/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
-
->**Note:**
-Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect
-metrics. It will eventually replace InfluxDB when their metrics collection is
-on par. Read more in the [Prometheus documentation](../prometheus/index.md).
-
-## Introduction to GitLab Performance Monitoring
-
-GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
-including (but not limited to):
-
-- The time it took to complete a transaction (a web request or Sidekiq job).
-- The time spent in running SQL queries and rendering HAML views.
-- The time spent executing (instrumented) Ruby methods.
-- Ruby object allocations, and retained objects in particular.
-- System statistics such as the process' memory usage and open file descriptors.
-- Ruby garbage collection statistics.
-
-Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
-data can be visualized using [Grafana][grafana] or any other application that
-supports reading data from InfluxDB. Alternatively data can be queried using the
-InfluxDB CLI.
-
-## Metric Types
-
-Two types of metrics are collected:
-
-1. Transaction specific metrics.
-1. Sampled metrics, collected at a certain interval in a separate thread.
-
-### Transaction Metrics
-
-Transaction metrics are metrics that can be associated with a single
-transaction. This includes statistics such as the transaction duration, timings
-of any executed SQL queries, time spent rendering HAML views, etc. These metrics
-are collected for every Rack request and Sidekiq job processed.
-
-### Sampled Metrics
-
-Sampled metrics are metrics that can't be associated with a single transaction.
-Examples include garbage collection statistics and retained Ruby objects. These
-metrics are collected at a regular interval. This interval is made up out of two
-parts:
-
-1. A user defined interval.
-1. A randomly generated offset added on top of the interval, the same offset
- can't be used twice in a row.
-
-The actual interval can be anywhere between a half of the defined interval and a
-half above the interval. For example, for a user defined interval of 15 seconds
-the actual interval can be anywhere between 7.5 and 22.5. The interval is
-re-generated for every sampling run instead of being generated once and re-used
-for the duration of the process' lifetime.
-
-[influxdb]: https://influxdata.com/time-series-platform/influxdb/
-[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
-[grafana]: http://grafana.org/
+This document was moved to [another location](index.md).
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 11d5e077a36..f495990d9a4 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -45,8 +45,9 @@ In this experimental phase, only a few metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
-| filesystem_circuitbreaker_latency_seconds | Histogram | 9.5 | Latency of the stat check the circuitbreaker uses to probe a shard |
+| filesystem_circuitbreaker_latency_seconds | Gauge | 9.5 | Time spent validating if a storage is accessible |
| filesystem_circuitbreaker | Gauge | 9.5 | Wether 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 |
## Metrics shared directory
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
index 0daceb98d99..4797d2a3206 100644
--- a/doc/administration/operations.md
+++ b/doc/administration/operations.md
@@ -1,7 +1 @@
-# GitLab operations
-
-- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
-- [Sidekiq Job throttling](operations/sidekiq_job_throttling.md)
-- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
-- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
-- [Moving repositories to a new location](operations/moving_repositories.md)
+This document was moved to [another location](operations/index.md).
diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md
new file mode 100644
index 00000000000..320d71a9527
--- /dev/null
+++ b/doc/administration/operations/index.md
@@ -0,0 +1,16 @@
+# Performing Operations in GitLab
+
+Keep your GitLab instance up and running smoothly.
+
+- [Clean up Redis sessions](cleaning_up_redis_sessions.md): Prior to GitLab 7.3,
+user sessions did not automatically expire from Redis. If
+you have been running a large GitLab server (thousands of users) since before
+GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
+database after you upgrade to GitLab 7.3.
+- [Moving repositories](moving_repositories.md): Moving all repositories managed
+by GitLab to another file system or another server.
+- [Sidekiq job throttling](sidekiq_job_throttling.md): Throttle Sidekiq queues
+that to prioritize important jobs.
+- [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller
+to restart Sidekiq.
+- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. \ No newline at end of file
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
index c8b5434c068..c39cb49b1c6 100644
--- a/doc/administration/raketasks/check.md
+++ b/doc/administration/raketasks/check.md
@@ -28,19 +28,25 @@ exactly which repositories are causing the trouble.
### Check all GitLab repositories
+>**Note:**
+>
+> - `gitlab:repo:check` has been deprecated in favor of `gitlab:git:fsck`
+> - [Deprecated][ce-15931] in GitLab 10.4.
+> - `gitlab:repo:check` will be removed in the future. [Removal issue][ce-41699]
+
This task loops through all repositories on the GitLab server and runs the
3 integrity checks described previously.
**Omnibus Installation**
```
-sudo gitlab-rake gitlab:repo:check
+sudo gitlab-rake gitlab:git:fsck
```
**Source Installation**
```bash
-sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:git:fsck RAILS_ENV=production
```
### Check repositories for a specific user
@@ -76,3 +82,6 @@ The LDAP check Rake task will test the bind_dn and password credentials
(if configured) and will list a sample of LDAP users. This task is also
executed as part of the `gitlab:check` task, but can run independently.
See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details.
+
+[ce-15931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15931
+[ce-41699]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41699
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index 136192191f9..ecf92c379fd 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -221,3 +221,22 @@ sudo gitlab-rake gitlab:shell:create_hooks
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production
```
+
+## Check TCP connectivity to a remote site
+
+Sometimes you need to know if your GitLab installation can connect to a TCP
+service on another machine - perhaps a PostgreSQL or HTTPS server. A rake task
+is included to help you with this:
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:tcp_check[example.com,80]
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:tcp_check[example.com,80] RAILS_ENV=production
+```
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 1304476e678..3a2cced37bf 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -89,9 +89,11 @@ email address in order to sign up.
If you also host a public-facing GitLab instance at `hooli.com` and set your
incoming email domain to `hooli.com`, an attacker could abuse the "Create new
-issue by email" feature by using a project's unique address as the email when
-signing up for Slack, which would send a confirmation email, which would create
-a new issue on the project owned by the attacker, allowing them to click the
+issue by email" or
+"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)"
+features by using a project's unique address as the email when signing up for
+Slack, which would send a confirmation email, which would create a new issue or
+merge request on the project owned by the attacker, allowing them to click the
confirmation link and validate their account on your company's private Slack
instance.
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 69c47abc806..246de50323e 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -15,10 +15,10 @@ GET /projects/:id/boards
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards
```
Example response:
@@ -27,6 +27,19 @@ Example response:
[
{
"id" : 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
"lists" : [
{
"id" : 1,
@@ -60,6 +73,74 @@ Example response:
]
```
+## Single board
+
+Get a single board.
+
+```
+GET /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "name:": "project issue board",
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+```
+
## List board lists
Get a list of the board's lists.
@@ -71,8 +152,8 @@ GET /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists
@@ -122,9 +203,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `list_id`| integer | yes | The ID of a board's list |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id`| integer | yes | The ID of a board's list |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
@@ -154,9 +235,9 @@ POST /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `label_id` | integer | yes | The ID of a label |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `label_id` | integer | yes | The ID of a label |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
@@ -186,10 +267,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `list_id` | integer | yes | The ID of a board's list |
-| `position` | integer | yes | The position of the list |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+| `position` | integer | yes | The position of the list |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2
@@ -219,9 +300,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `list_id` | integer | yes | The ID of a board's list |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 5a4a8d888b3..c9b72d4a1dd 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -84,6 +84,7 @@ POST /projects/:id/repository/commits
| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb` |
| `content` | string | no | File content, required for all except `delete`. Optional for `move` |
| `encoding` | string | no | `text` or `base64`. `text` is default. |
+| `last_commit_id` | string | no | Last known file commit id. Will be only considered in update, move and delete actions. |
```bash
PAYLOAD=$(cat << 'JSON'
diff --git a/doc/api/groups.md b/doc/api/groups.md
index c1b5737c247..de730cdd869 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -365,13 +365,15 @@ POST /groups
Parameters:
-- `name` (required) - The name of the group
-- `path` (required) - The path of the group
-- `description` (optional) - The group's description
-- `visibility` (optional) - The group's visibility. Can be `private`, `internal`, or `public`.
-- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
-- `request_access_enabled` (optional) - Allow users to request member access.
-- `parent_id` (optional) - The parent group id for creating nested group.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `name` | string | yes | The name of the group |
+| `path` | string | yes | The path of the group |
+| `description` | string | no | The group's description |
+| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. |
+| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
+| `request_access_enabled` | boolean | no | Allow users to request member access. |
+| `parent_id` | integer | no | The parent group id for creating nested group. |
## Transfer project to group
@@ -383,8 +385,10 @@ POST /groups/:id/projects/:project_id
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `project_id` (required) - The ID or path of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Update group
diff --git a/doc/api/issues.md b/doc/api/issues.md
index ec8ff3cd3f3..da89db17cd9 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -514,9 +514,9 @@ PUT /projects/:id/issues/:issue_iid
| `title` | string | no | The title of an issue |
| `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 users to assign the issue to |
-| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
-| `labels` | string | no | Comma-separated label names for an issue |
+| `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.|
+| `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) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
@@ -1124,6 +1124,45 @@ Example response:
```
+## Participants on issues
+
+```
+GET /projects/:id/issues/:issue_iid/participants
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|--------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/participants
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "John Doe1",
+ "username": "user1",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
+ "web_url": "http://localhost/user1"
+ },
+ {
+ "id": 5,
+ "name": "John Doe5",
+ "username": "user5",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80&d=identicon",
+ "web_url": "http://localhost/user5"
+ }
+]
+```
+
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index b2e4b6d0955..24afcef9a31 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -47,6 +47,7 @@ Parameters:
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `search` | string | no | Search merge requests against their `title` and `description` |
```json
[
@@ -161,6 +162,7 @@ Parameters:
| `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)_ |
+| `search` | string | no | Search merge requests against their `title` and `description` |
```json
[
@@ -306,6 +308,41 @@ Parameters:
}
```
+## Get single MR participants
+
+Get a list of merge request participants.
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/participants
+```
+
+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
+
+
+```json
+[
+ {
+ "id": 1,
+ "name": "John Doe1",
+ "username": "user1",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
+ "web_url": "http://localhost/user1"
+ },
+ {
+ "id": 2,
+ "name": "John Doe2",
+ "username": "user2",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon",
+ "web_url": "http://localhost/user2"
+ },
+]
+```
+
## Get single MR commits
Get a list of merge request commits.
@@ -524,15 +561,15 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The ID of a merge request |
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
-| `assignee_id` | integer | no | Assignee user ID |
+| `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.|
+| `labels` | string | no | Comma-separated label names for an 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) |
-| `labels` | string | no | Labels for MR as a comma-separated list |
-| `milestone_id` | integer | no | The ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 50685f335f7..20275b902c6 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
+ "project_id": 1337,
"certificate": {
"expired": false,
"expiration": "2020-04-12T14:32:00.000Z"
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 594babc74be..03b32577872 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -182,6 +182,8 @@ GET /projects/:id/repository/contributors
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` fields. If not given contributors are ordered by commit date.
+- `sort` (optional) - Return contributors sorted in `asc` or `desc` order. Default is `asc`
Response:
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index c517a38a8ba..a1a0b1b756c 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -151,3 +151,4 @@ Parameters:
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
+- `last_commit_id` (optional) - Last known file commit id
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 015b09a745e..7495c6cdedb 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -30,14 +30,18 @@ Example response:
"description": "test-1-20150125",
"id": 6,
"is_shared": false,
- "name": null
+ "name": null,
+ "online": true,
+ "status": "online"
},
{
"active": true,
"description": "test-2-20150125",
"id": 8,
"is_shared": false,
- "name": null
+ "name": null,
+ "online": false,
+ "status": "offline"
}
]
```
@@ -69,28 +73,36 @@ Example response:
"description": "shared-runner-1",
"id": 1,
"is_shared": true,
- "name": null
+ "name": null,
+ "online": true,
+ "status": "online"
},
{
"active": true,
"description": "shared-runner-2",
"id": 3,
"is_shared": true,
- "name": null
+ "name": null,
+ "online": false
+ "status": "offline"
},
{
"active": true,
"description": "test-1-20150125",
"id": 6,
"is_shared": false,
- "name": null
+ "name": null,
+ "online": true
+ "status": "paused"
},
{
"active": true,
"description": "test-2-20150125",
"id": 8,
"is_shared": false,
- "name": null
+ "name": null,
+ "online": false,
+ "status": "offline"
}
]
```
@@ -122,6 +134,8 @@ Example response:
"is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z",
"name": null,
+ "online": true,
+ "status": "online",
"platform": null,
"projects": [
{
@@ -176,6 +190,8 @@ Example response:
"is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z",
"name": null,
+ "online": true,
+ "status": "online",
"platform": null,
"projects": [
{
@@ -327,14 +343,18 @@ Example response:
"description": "test-2-20150125",
"id": 8,
"is_shared": false,
- "name": null
+ "name": null,
+ "online": false,
+ "status": "offline"
},
{
"active": true,
"description": "development_runner",
"id": 5,
"is_shared": true,
- "name": null
+ "name": null,
+ "online": true
+ "status": "paused"
}
]
```
@@ -364,7 +384,9 @@ Example response:
"description": "test-2016-02-01",
"id": 9,
"is_shared": false,
- "name": null
+ "name": null,
+ "online": true,
+ "status": "online"
}
```
diff --git a/doc/api/services.md b/doc/api/services.md
index 08df26db3ec..2928ab6cc75 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1,5 +1,7 @@
# Services API
+>**Note:** This API requires an access token with Master or Owner permissions
+
## Asana
Asana - Teamwork without email
@@ -16,8 +18,10 @@ PUT /projects/:id/services/asana
Parameters:
-- `api_key` (**required**) - User API token. User must have access to task, all comments will be attributed to this user.
-- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `api_key` | string | true | User API token. User must have access to task, all comments will be attributed to this user. |
+| `restrict_to_branch` | string | false | Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches. |
### Delete Asana service
@@ -49,8 +53,10 @@ PUT /projects/:id/services/assembla
Parameters:
-- `token` (**required**)
-- `subdomain` (optional)
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | The authentication token
+| `subdomain` | string | false | The subdomain setting |
### Delete Assembla service
@@ -84,10 +90,12 @@ PUT /projects/:id/services/bamboo
Parameters:
-- `bamboo_url` (**required**) - Bamboo root URL like https://bamboo.example.com
-- `build_key` (**required**) - Bamboo build plan key like KEY
-- `username` (**required**) - A user with API access, if applicable
-- `password` (**required**)
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `bamboo_url` | string | true | Bamboo root URL like https://bamboo.example.com |
+| `build_key` | string | true | Bamboo build plan key like KEY |
+| `username` | string | true | A user with API access, if applicable |
+| `password` | string | true | Password of the user |
### Delete Atlassian Bamboo CI service
@@ -105,6 +113,44 @@ Get Atlassian Bamboo CI service settings for a project.
GET /projects/:id/services/bamboo
```
+## Bugzilla
+
+Bugzilla Issue Tracker
+
+### Create/Edit Buildkite service
+
+Set Bugzilla service for a project.
+
+```
+PUT /projects/:id/services/bugzilla
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `new_issue_url` | string | true | New Issue url |
+| `issues_url` | string | true | Issue url |
+| `project_url` | string | true | Project url |
+| `description` | string | false | Description |
+| `title` | string | false | Title |
+
+### Delete Bugzilla Service
+
+Delete Bugzilla service for a project.
+
+```
+DELETE /projects/:id/services/bugzilla
+```
+
+### Get Bugzilla Service Settings
+
+Get Bugzilla service settings for a project.
+
+```
+GET /projects/:id/services/bugzilla
+```
+
## Buildkite
Continuous integration and deployments
@@ -119,9 +165,11 @@ PUT /projects/:id/services/buildkite
Parameters:
-- `token` (**required**) - Buildkite project GitLab token
-- `project_url` (**required**) - https://buildkite.com/example/project
-- `enable_ssl_verification` (optional) - Enable SSL verification
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Buildkite project GitLab token |
+| `project_url` | string | true | https://buildkite.com/example/project |
+| `enable_ssl_verification` | boolean | false | Enable SSL verification |
### Delete Buildkite service
@@ -153,9 +201,11 @@ PUT /projects/:id/services/campfire
Parameters:
-- `token` (**required**)
-- `subdomain` (optional)
-- `room` (optional)
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Campfire token |
+| `subdomain` | string | false | Campfire subdomain |
+| `room` | string | false | Campfire room |
### Delete Campfire service
@@ -187,11 +237,13 @@ PUT /projects/:id/services/custom-issue-tracker
Parameters:
-- `new_issue_url` (**required**) - New Issue url
-- `issues_url` (**required**) - Issue url
-- `project_url` (**required**) - Project url
-- `description` (optional) - Custom issue tracker
-- `title` (optional) - Custom Issue Tracker
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `new_issue_url` | string | true | New Issue url
+| `issues_url` | string | true | Issue url
+| `project_url` | string | true | Project url
+| `description` | string | false | Description
+| `title` | string | false | Title
### Delete Custom Issue Tracker service
@@ -223,9 +275,11 @@ PUT /projects/:id/services/drone-ci
Parameters:
-- `token` (**required**) - Drone CI project specific token
-- `drone_url` (**required**) - http://drone.example.com
-- `enable_ssl_verification` (optional) - Enable SSL verification
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Drone CI project specific token |
+| `drone_url` | string | true | http://drone.example.com |
+| `enable_ssl_verification` | boolean | false | Enable SSL verification |
### Delete Drone CI service
@@ -257,9 +311,11 @@ PUT /projects/:id/services/emails-on-push
Parameters:
-- `recipients` (**required**) - Emails separated by whitespace
-- `disable_diffs` (optional) - Disable code diffs
-- `send_from_committer_email` (optional) - Send from committer
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | true | Emails separated by whitespace |
+| `disable_diffs` | boolean | false | Disable code diffs |
+| `send_from_committer_email` | boolean | false | Send from committer |
### Delete Emails on push service
@@ -291,7 +347,9 @@ PUT /projects/:id/services/external-wiki
Parameters:
-- `external_wiki_url` (**required**) - The URL of the external Wiki
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `external_wiki_url` | string | true | The URL of the external Wiki |
### Delete External Wiki service
@@ -323,7 +381,9 @@ PUT /projects/:id/services/flowdock
Parameters:
-- `token` (**required**) - Flowdock Git source token
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Flowdock Git source token |
### Delete Flowdock service
@@ -355,8 +415,10 @@ PUT /projects/:id/services/gemnasium
Parameters:
-- `api_key` (**required**) - Your personal API KEY on gemnasium.com
-- `token` (**required**) - The project's slug on gemnasium.com
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `api_key` | string | true | Your personal API KEY on gemnasium.com |
+| `token` | string | true | The project's slug on gemnasium.com |
### Delete Gemnasium service
@@ -388,12 +450,14 @@ PUT /projects/:id/services/hipchat
Parameters:
-- `token` (**required**) - Room token
-- `color` (optional)
-- `notify` (optional)
-- `room` (optional) - Room name or ID
-- `api_version` (optional) - Leave blank for default (v2)
-- `server` (optional) - Leave blank for default. https://hipchat.example.com
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Room token |
+| `color` | string | false | The room color |
+| `notify` | boolean | false | Enable notifications |
+| `room` | string | false |Room name or ID |
+| `api_version` | string | false | Leave blank for default (v2) |
+| `server` | string | false | Leave blank for default. https://hipchat.example.com |
### Delete HipChat service
@@ -427,11 +491,13 @@ PUT /projects/:id/services/irker
Parameters:
-- `recipients` (**required**) - Recipients/channels separated by whitespaces
-- `default_irc_uri` (optional) - irc://irc.network.net:6697/
-- `server_port` (optional) - 6659
-- `server_host` (optional) - localhost
-- `colorize_messages` (optional)
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | true | Recipients/channels separated by whitespaces |
+| `default_irc_uri` | string | false | irc://irc.network.net:6697/ |
+| `server_host` | string | false | localhost |
+| `server_port` | integer | false | 6659 |
+| `colorize_messages` | boolean | false | Colorize messages |
### Delete Irker (IRC gateway) service
@@ -474,7 +540,9 @@ Set JIRA service for a project.
PUT /projects/:id/services/jira
```
-| Attribute | Type | Required | Description |
+Parameters:
+
+| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
@@ -494,6 +562,9 @@ DELETE /projects/:id/services/jira
Kubernetes / Openshift integration
+CAUTION: **Warning:**
+Kubernetes service integration has been deprecated in GitLab 10.3. API service endpoints will continue to work as long as the Kubernetes service is active, however if the service is inactive API endpoints will automatically return a `400 Bad Request`. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
+
### Create/Edit Kubernetes service
Set Kubernetes service for a project.
@@ -569,7 +640,7 @@ PUT /projects/:id/services/slack-slash-commands
Parameters:
-| Attribute | Type | Required | Description |
+| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Slack token |
@@ -604,7 +675,7 @@ PUT /projects/:id/services/mattermost-slash-commands
Parameters:
-| Attribute | Type | Required | Description |
+| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Mattermost token |
| `username` | string | no | The username to use to post the message |
@@ -665,7 +736,7 @@ PUT /projects/:id/services/pipelines-email
Parameters:
-| Attribute | Type | Required | Description |
+| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
@@ -701,8 +772,10 @@ PUT /projects/:id/services/pivotaltracker
Parameters:
-- `token` (**required**)
-- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | The PivotalTracker token |
+| `restrict_to_branch` | boolean | false | Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches. |
### Delete PivotalTracker service
@@ -720,6 +793,40 @@ Get PivotalTracker service settings for a project.
GET /projects/:id/services/pivotaltracker
```
+## Prometheus
+
+Prometheus is a powerful time-series monitoring service.
+
+### Create/Edit Prometheus service
+
+Set Prometheus service for a project.
+
+```
+PUT /projects/:id/services/prometheus
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `api_url` | string | true | Prometheus API Base URL, like http://prometheus.example.com/ |
+
+### Delete Prometheus service
+
+Delete Prometheus service for a project.
+
+```
+DELETE /projects/:id/services/prometheus
+```
+
+### Get Prometheus service settings
+
+Get Prometheus service settings for a project.
+
+```
+GET /projects/:id/services/prometheus
+```
+
## Pushover
Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.
@@ -734,11 +841,13 @@ PUT /projects/:id/services/pushover
Parameters:
-- `api_key` (**required**) - Your application key
-- `user_key` (**required**) - Your user key
-- `priority` (**required**)
-- `device` (optional) - Leave blank for all active devices
-- `sound` (optional)
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `api_key` | string | true | Your application key |
+| `user_key` | string | true | Your user key |
+| `priority` | string | true | The priority |
+| `device` | string | false | Leave blank for all active devices |
+| `sound` | string | false | The sound of the notification |
### Delete Pushover service
@@ -770,10 +879,12 @@ PUT /projects/:id/services/redmine
Parameters:
-- `new_issue_url` (**required**) - New Issue url
-- `project_url` (**required**) - Project url
-- `issues_url` (**required**) - Issue url
-- `description` (optional) - Redmine issue tracker
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `new_issue_url` | string | true | New Issue url |
+| `project_url` | string | true | Project url |
+| `issues_url` | string | true | Issue url |
+| `description` | string | false | Description |
### Delete Redmine service
@@ -803,11 +914,33 @@ Set Slack service for a project.
PUT /projects/:id/services/slack
```
+>**Note:** Specific event parameters (e.g. `push_events` flag and `push_channel`) were [introduced in v10.4][11435]
+
Parameters:
-- `webhook` (**required**) - https://hooks.slack.com/services/...
-- `username` (optional) - username
-- `channel` (optional) - #channel
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | https://hooks.slack.com/services/... |
+| `username` | string | false | username |
+| `channel` | string | false | Default channel to use if others are not configured |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `notify_only_default_branch` | boolean | false | Send notifications only for the default branch |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+| `push_channel` | string | false | The name of the channel to receive push events notifications |
+| `issue_channel` | string | false | The name of the channel to receive issues events notifications |
+| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
+| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
+| `note_channel` | string | false | The name of the channel to receive note events notifications |
+| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
+| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
+| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
### Delete Slack service
@@ -825,6 +958,40 @@ Get Slack service settings for a project.
GET /projects/:id/services/slack
```
+## Microsoft Teams
+
+Group Chat Software
+
+### Create/Edit Microsoft Teams service
+
+Set Microsoft Teams service for a project.
+
+```
+PUT /projects/:id/services/microsoft_teams
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/... |
+
+### Delete Microsoft Teams service
+
+Delete Microsoft Teams service for a project.
+
+```
+DELETE /projects/:id/services/microsoft_teams
+```
+
+### Get Microsoft Teams service settings
+
+Get Microsoft Teams service settings for a project.
+
+```
+GET /projects/:id/services/microsoft_teams
+```
+
## Mattermost notifications
Receive event notifications in Mattermost
@@ -837,11 +1004,33 @@ Set Mattermost service for a project.
PUT /projects/:id/services/mattermost
```
+>**Note:** Specific event parameters (e.g. `push_events` flag and `push_channel`) were [introduced in v10.4][11435]
+
Parameters:
-- `webhook` (**required**) - https://mattermost.example/hooks/1298aff...
-- `username` (optional) - username
-- `channel` (optional) - #channel
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Mattermost webhook. e.g. http://mattermost_host/hooks/... |
+| `username` | string | false | username |
+| `channel` | string | false | Default channel to use if others are not configured |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `notify_only_default_branch` | boolean | false | Send notifications only for the default branch |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+| `push_channel` | string | false | The name of the channel to receive push events notifications |
+| `issue_channel` | string | false | The name of the channel to receive issues events notifications |
+| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
+| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
+| `note_channel` | string | false | The name of the channel to receive note events notifications |
+| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
+| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
+| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
### Delete Mattermost notifications service
@@ -875,10 +1064,12 @@ PUT /projects/:id/services/teamcity
Parameters:
-- `teamcity_url` (**required**) - TeamCity root URL like https://teamcity.example.com
-- `build_type` (**required**) - Build configuration ID
-- `username` (**required**) - A user with permissions to trigger a manual build
-- `password` (**required**)
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `teamcity_url` | string | true | TeamCity root URL like https://teamcity.example.com |
+| `build_type` | string | true | Build configuration ID |
+| `username` | string | true | A user with permissions to trigger a manual build |
+| `password` | string | true | The password of the user |
### Delete JetBrains TeamCity CI service
@@ -916,7 +1107,9 @@ PUT /projects/:id/services/mock-ci
Parameters:
-- `mock_service_url` (**required**) - http://localhost:4004
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `mock_service_url` | string | true | http://localhost:4004 |
### Delete MockCI service
@@ -933,3 +1126,5 @@ Get MockCI service settings for a project.
```
GET /projects/:id/services/mock-ci
```
+
+[11435]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11435
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 0e4758cda2d..0b5b1f0c134 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -69,7 +69,7 @@ PUT /application/settings
| `after_sign_up_text` | string | no | Text shown to the user after signing up |
| `akismet_api_key` | string | no | API key for akismet spam protection |
| `akismet_enabled` | boolean | no | Enable or disable akismet spam protection |
-| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. |
+| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. |
| `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. |
| `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. |
| `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. |
diff --git a/doc/api/tags.md b/doc/api/tags.md
index bebe6536b6e..fa25dc76452 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -12,7 +12,11 @@ GET /projects/:id/repository/tags
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string| yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user|
+| `order_by` | string | no | Return tags ordered by `name` or `updated` fields. Default is `updated` |
+| `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc` |
```json
[
diff --git a/doc/articles/artifactory_and_gitlab/index.md b/doc/articles/artifactory_and_gitlab/index.md
index c64851bad2b..6a590b53727 100644
--- a/doc/articles/artifactory_and_gitlab/index.md
+++ b/doc/articles/artifactory_and_gitlab/index.md
@@ -1,278 +1 @@
-# How to deploy Maven projects to Artifactory with GitLab CI/CD
-
-> **Article [Type](../../development/writing_documentation.md#types-of-technical-articles):** tutorial ||
-> **Level:** intermediary ||
-> **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) ||
-> **Publication date:** 2017-08-15
-
-## Introduction
-
-In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/)
-to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/), and then use it from another Maven application as a dependency.
-
-You'll create two different projects:
-
-- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep)
-- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app)
-
-We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/).
-We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it.
-
-## Create the simple Maven dependency
-
-First of all, you need an application to work with: in this specific case we will
-use a simple one, but it could be any Maven application. This will be the
-dependency you want to package and deploy to Artifactory, in order to be
-available to other projects.
-
-### Prepare the dependency application
-
-For this article you'll use a Maven app that can be cloned from our example
-project:
-
-1. Log in to your GitLab account
-1. Create a new project by selecting **Import project from âž” Repo by URL**
-1. Add the following URL:
-
- ```
- https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git
- ```
-1. Click **Create project**
-
-This application is nothing more than a basic class with a stub for a JUnit based test suite.
-It exposes a method called `hello` that accepts a string as input, and prints a hello message on the screen.
-
-The project structure is really simple, and you should consider these two resources:
-
-- `pom.xml`: project object model (POM) configuration file
-- `src/main/java/com/example/dep/Dep.java`: source of our application
-
-### Configure the Artifactory deployment
-
-The application is ready to use, but you need some additional steps to deploy it to Artifactory:
-
-1. Log in to Artifactory with your user's credentials.
-1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel.
-1. Copy to clipboard the configuration snippet under the **Deploy** paragraph.
-1. Change the `url` value in order to have it configurable via secret variables.
-1. Copy the snippet in the `pom.xml` file for your project, just after the
- `dependencies` section. The snippet should look like this:
-
- ```xml
- <distributionManagement>
- <repository>
- <id>central</id>
- <name>83d43b5afeb5-releases</name>
- <url>${env.MAVEN_REPO_URL}/libs-release-local</url>
- </repository>
- </distributionManagement>
- ```
-
-Another step you need to do before you can deploy the dependency to Artifactory
-is to configure the authentication data. It is a simple task, but Maven requires
-it to stay in a file called `settings.xml` that has to be in the `.m2` subdirectory
-in the user's homedir.
-
-Since you want to use GitLab Runner to automatically deploy the application, you
-should create the file in the project's home directory and set a command line
-parameter in `.gitlab-ci.yml` to use the custom location instead of the default one:
-
-1. Create a folder called `.m2` in the root of your repository
-1. Create a file called `settings.xml` in the `.m2` folder
-1. Copy the following content into a `settings.xml` file:
-
- ```xml
- <settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"
- xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
- <servers>
- <server>
- <id>central</id>
- <username>${env.MAVEN_REPO_USER}</username>
- <password>${env.MAVEN_REPO_PASS}</password>
- </server>
- </servers>
- </settings>
- ```
-
- Username and password will be replaced by the correct values using secret variables.
-
-### Configure GitLab CI/CD for `simple-maven-dep`
-
-Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and deploy the dependency!
-
-GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs
-that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/).
-
-First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > CI/CD** page
-and add the following secret variables (replace them with your current values, of course):
-
-- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL)
-- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username)
-- **MAVEN_REPO_PASS**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory Encrypted Password)
-
-Now it's time to define jobs in `.gitlab-ci.yml` and push it to the repo:
-
-```yaml
-image: maven:latest
-
-variables:
- MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
- MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
-
-cache:
- paths:
- - .m2/repository/
- - target/
-
-build:
- stage: build
- script:
- - mvn $MAVEN_CLI_OPTS compile
-
-test:
- stage: test
- script:
- - mvn $MAVEN_CLI_OPTS test
-
-deploy:
- stage: deploy
- script:
- - mvn $MAVEN_CLI_OPTS deploy
- only:
- - master
-```
-
-GitLab Runner will use the latest [Maven Docker image](https://hub.docker.com/_/maven/), which already contains all the tools and the dependencies you need to manage the project,
-in order to run the jobs.
-
-Environment variables are set to instruct Maven to use the `homedir` of the repo instead of the user's home when searching for configuration and dependencies.
-
-Caching the `.m2/repository folder` (where all the Maven files are stored), and the `target` folder (where our application will be created), is useful for speeding up the process
-by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary.
-
-Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application.
-
-Deploy to Artifactory is done as defined by the secret variables we have just set up.
-The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published.
-
-Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening.
-If the deployment has been successful, the deploy job log will output:
-
-```
-[INFO] ------------------------------------------------------------------------
-[INFO] BUILD SUCCESS
-[INFO] ------------------------------------------------------------------------
-[INFO] Total time: 1.983 s
-```
-
->**Note**:
-the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log the first time you run it.
-
-Yay! You did it! Checking in Artifactory will confirm that you have a new artifact available in the `libs-release-local` repo.
-
-## Create the main Maven application
-
-Now that you have the dependency available on Artifactory, it's time to use it!
-Let's see how we can have it as a dependency to our main application.
-
-### Prepare the main application
-
-We'll use again a Maven app that can be cloned from our example project:
-
-1. Create a new project by selecting **Import project from âž” Repo by URL**
-1. Add the following URL:
-
- ```
- https://gitlab.com/gitlab-examples/maven/simple-maven-app.git
- ```
-1. Click **Create project**
-
-This one is a simple app as well. If you look at the `src/main/java/com/example/app/App.java`
-file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter.
-
-Since Maven doesn't know how to resolve the dependency, you need to modify the configuration:
-
-1. Go back to Artifactory
-1. Browse the `libs-release-local` repository
-1. Select the `simple-maven-dep-1.0.jar` file
-1. Find the configuration snippet from the **Dependency Declaration** section of the main panel
-1. Copy the snippet in the `dependencies` section of the `pom.xml` file.
- The snippet should look like this:
-
- ```xml
- <dependency>
- <groupId>com.example.dep</groupId>
- <artifactId>simple-maven-dep</artifactId>
- <version>1.0</version>
- </dependency>
- ```
-
-### Configure the Artifactory repository location
-
-At this point you defined the dependency for the application, but you still miss where you can find the required files.
-You need to create a `.m2/settings.xml` file as you did for the dependency project, and let Maven know the location using environment variables.
-
-Here is how you can get the content of the file directly from Artifactory:
-
-1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel
-1. Click on **Generate Maven Settings**
-1. Click on **Generate Settings**
-1. Copy to clipboard the configuration file
-1. Save the file as `.m2/settings.xml` in your repo
-
-Now you are ready to use the Artifactory repository to resolve dependencies and use `simple-maven-dep` in your main application!
-
-### Configure GitLab CI/CD for `simple-maven-app`
-
-You need a last step to have everything in place: configure the `.gitlab-ci.yml` file for this project, as you already did for `simple-maven-dep`.
-
-You want to leverage [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and run your awesome application,
-and see if you can get the greeting as expected!
-
-All you need to do is to add the following `.gitlab-ci.yml` to the repo:
-
-```yaml
-image: maven:latest
-
-stages:
- - build
- - test
- - run
-
-variables:
- MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
- MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
-
-cache:
- paths:
- - .m2/repository/
- - target/
-
-build:
- stage: build
- script:
- - mvn $MAVEN_CLI_OPTS compile
-
-test:
- stage: test
- script:
- - mvn $MAVEN_CLI_OPTS test
-
-run:
- stage: run
- script:
- - mvn $MAVEN_CLI_OPTS package
- - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App"
-```
-
-It is very similar to the configuration used for `simple-maven-dep`, but instead of the `deploy` job there is a `run` job.
-Probably something that you don't want to use in real projects, but here it is useful to see the application executed automatically.
-
-And that's it! In the `run` job output log you will find a friendly hello to GitLab!
-
-## Conclusion
-
-In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume artifacts.
-
-A similar approach could be used to interact with any other Maven compatible Binary Repository Manager.
-Obviously, you can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow.
+This document was moved to [another location](../../ci/examples/artifactory_and_gitlab/index.md)
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
index 25a24bc1d32..a8320c12e6b 100644
--- a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
@@ -1,267 +1 @@
-# How to configure LDAP with GitLab CE
-
-> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** admin guide ||
-> **Level:** intermediary ||
-> **Author:** [Chris Wilson](https://gitlab.com/MrChrisW) ||
-> **Publication date:** 2017-05-03
-
-## Introduction
-
-Managing a large number of users in GitLab can become a burden for system administrators. As an organization grows so do user accounts. Keeping these user accounts in sync across multiple enterprise applications often becomes a time consuming task.
-
-In this guide we will focus on configuring GitLab with Active Directory. [Active Directory](https://en.wikipedia.org/wiki/Active_Directory) is a popular LDAP compatible directory service provided by Microsoft, included in all modern Windows Server operating systems.
-
-GitLab has supported LDAP integration since [version 2.2](https://about.gitlab.com/2012/02/22/gitlab-version-2-2/). With GitLab LDAP [group syncing](#group-syncing-ee) being added to GitLab Enterprise Edition in [version 6.0](https://about.gitlab.com/2013/08/20/gitlab-6-dot-0-released/). LDAP integration has become one of the most popular features in GitLab.
-
-## Getting started
-
-### Choosing an LDAP Server
-
-The main reason organizations choose to utilize a LDAP server is to keep the entire organization's user base consolidated into a central repository. Users can access multiple applications and systems across the IT environment using a single login. Because LDAP is an open, vendor-neutral, industry standard application protocol, the number of applications using LDAP authentication continues to increase.
-
-There are many commercial and open source [directory servers](https://en.wikipedia.org/wiki/Directory_service#LDAP_implementations) that support the LDAP protocol. Deciding on the right directory server highly depends on the existing IT environment in which the server will be integrated with.
-
-For example, [Active Directory](https://technet.microsoft.com/en-us/library/hh831484(v=ws.11).aspx) is generally favored in a primarily Windows environment, as this allows quick integration with existing services. Other popular directory services include:
-
-- [Oracle Internet Directory](http://www.oracle.com/technetwork/middleware/id-mgmt/overview/index-082035.html)
-- [OpenLDAP](http://www.openldap.org/)
-- [389 Directory](http://directory.fedoraproject.org/)
-- [OpenDJ](https://forgerock.org/opendj/)
-- [ApacheDS](https://directory.apache.org/)
-
-> GitLab uses the [Net::LDAP](https://rubygems.org/gems/net-ldap) library under the hood. This means it supports all [IETF](https://tools.ietf.org/html/rfc2251) compliant LDAPv3 servers.
-
-### Active Directory (AD)
-
-We won't cover the installation and configuration of Windows Server or Active Directory Domain Services in this tutorial. There are a number of resources online to guide you through this process:
-
-- Install Windows Server 2012 - (_technet.microsoft.com_) - [Installing Windows Server 2012 ](https://technet.microsoft.com/en-us/library/jj134246(v=ws.11).aspx)
-
-- Install Active Directory Domain Services (AD DS) (_technet.microsoft.com_)- [Install Active Directory Domain Services](https://technet.microsoft.com/windows-server-docs/identity/ad-ds/deploy/install-active-directory-domain-services--level-100-#BKMK_PS)
-
-> **Shortcut:** You can quickly install AD DS via PowerShell using
-`Install-WindowsFeature AD-Domain-Services -IncludeManagementTools`
-
-### Creating an AD **OU** structure
-
-Configuring organizational units (**OU**s) is an important part of setting up Active Directory. **OU**s form the base for an entire organizational structure. Using GitLab as an example we have designed the **OU** structure below using the geographic **OU** model. In the Geographic Model we separate **OU**s for different geographic regions.
-
-| GitLab **OU** Design | GitLab AD Structure |
-| :----------------------------: | :------------------------------: |
-| ![GitLab OU Design][gitlab_ou] | ![GitLab AD Structure][ldap_ou] |
-
-[gitlab_ou]: img/gitlab_ou.png
-[ldap_ou]: img/ldap_ou.gif
-
-Using PowerShell you can output the **OU** structure as a table (_all names are examples only_):
-
-```ps
-Get-ADObject -LDAPFilter "(objectClass=*)" -SearchBase 'OU=GitLab INT,DC=GitLab,DC=org' -Properties CanonicalName | Format-Table Name,CanonicalName -A
-```
-
-```
-OU CanonicalName
----- -------------
-GitLab INT GitLab.org/GitLab INT
-United States GitLab.org/GitLab INT/United States
-Developers GitLab.org/GitLab INT/United States/Developers
-Gary Johnson GitLab.org/GitLab INT/United States/Developers/Gary Johnson
-Ellis Matthews GitLab.org/GitLab INT/United States/Developers/Ellis Matthews
-William Collins GitLab.org/GitLab INT/United States/Developers/William Collins
-People Ops GitLab.org/GitLab INT/United States/People Ops
-Margaret Baker GitLab.org/GitLab INT/United States/People Ops/Margaret Baker
-Libby Hartzler GitLab.org/GitLab INT/United States/People Ops/Libby Hartzler
-Victoria Ryles GitLab.org/GitLab INT/United States/People Ops/Victoria Ryles
-The Netherlands GitLab.org/GitLab INT/The Netherlands
-Developers GitLab.org/GitLab INT/The Netherlands/Developers
-John Doe GitLab.org/GitLab INT/The Netherlands/Developers/John Doe
-Jon Mealy GitLab.org/GitLab INT/The Netherlands/Developers/Jon Mealy
-Jane Weingarten GitLab.org/GitLab INT/The Netherlands/Developers/Jane Weingarten
-Production GitLab.org/GitLab INT/The Netherlands/Production
-Sarah Konopka GitLab.org/GitLab INT/The Netherlands/Production/Sarah Konopka
-Cynthia Bruno GitLab.org/GitLab INT/The Netherlands/Production/Cynthia Bruno
-David George GitLab.org/GitLab INT/The Netherlands/Production/David George
-United Kingdom GitLab.org/GitLab INT/United Kingdom
-Developers GitLab.org/GitLab INT/United Kingdom/Developers
-Leroy Fox GitLab.org/GitLab INT/United Kingdom/Developers/Leroy Fox
-Christopher Alley GitLab.org/GitLab INT/United Kingdom/Developers/Christopher Alley
-Norris Morita GitLab.org/GitLab INT/United Kingdom/Developers/Norris Morita
-Support GitLab.org/GitLab INT/United Kingdom/Support
-Laura Stanley GitLab.org/GitLab INT/United Kingdom/Support/Laura Stanley
-Nikki Schuman GitLab.org/GitLab INT/United Kingdom/Support/Nikki Schuman
-Harriet Butcher GitLab.org/GitLab INT/United Kingdom/Support/Harriet Butcher
-Global Groups GitLab.org/GitLab INT/Global Groups
-DevelopersNL GitLab.org/GitLab INT/Global Groups/DevelopersNL
-DevelopersUK GitLab.org/GitLab INT/Global Groups/DevelopersUK
-DevelopersUS GitLab.org/GitLab INT/Global Groups/DevelopersUS
-ProductionNL GitLab.org/GitLab INT/Global Groups/ProductionNL
-SupportUK GitLab.org/GitLab INT/Global Groups/SupportUK
-People Ops US GitLab.org/GitLab INT/Global Groups/People Ops US
-Global Admins GitLab.org/GitLab INT/Global Groups/Global Admins
-```
-
-> See [more information](https://technet.microsoft.com/en-us/library/ff730967.aspx) on searching Active Directory with Windows PowerShell from [The Scripting Guys](https://technet.microsoft.com/en-us/scriptcenter/dd901334.aspx)
-
-## 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 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)
-
-> Both group_base and admin_group configuration options are only available in GitLab Enterprise Edition. See [GitLab EE - LDAP Features](#gitlab-enterprise-edition---ldap-features)
-
-### Example `gitlab.rb` LDAP
-
-```
-gitlab_rails['ldap_enabled'] = true
-gitlab_rails['ldap_servers'] = {
-'main' => {
- 'label' => 'GitLab AD',
- 'host' => 'ad.example.org',
- 'port' => 636,
- 'uid' => 'sAMAccountName',
- 'encryption' => 'simple_tls',
- 'verify_certificates' => true,
- 'bind_dn' => 'CN=GitLabSRV,CN=Users,DC=GitLab,DC=org',
- 'password' => 'Password1',
- 'active_directory' => true,
- 'base' => 'OU=GitLab INT,DC=GitLab,DC=org',
- 'group_base' => 'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org',
- 'admin_group' => 'Global Admins'
- }
-}
-```
-
-> **Note:** Remember to run `gitlab-ctl reconfigure` after modifying `gitlab.rb`
-
-## Security improvements (LDAPS)
-
-Security is an important aspect when deploying an LDAP server. By default, LDAP traffic is transmitted unsecured. LDAP can be secured using SSL/TLS called LDAPS, or commonly "LDAP over SSL".
-
-Securing LDAP (enabling LDAPS) on Windows Server 2012 involves installing a valid SSL certificate. For full details see Microsoft's guide [How to enable LDAP over SSL with a third-party certification authority](https://support.microsoft.com/en-us/help/321051/how-to-enable-ldap-over-ssl-with-a-third-party-certification-authority)
-
-> By default a LDAP service listens for connections on TCP and UDP port 389. LDAPS (LDAP over SSL) listens on port 636
-
-### Testing you AD server
-
-#### Using **AdFind** (Windows)
-
-You can use the [`AdFind`](https://social.technet.microsoft.com/wiki/contents/articles/7535.adfind-command-examples.aspx) utility (on Windows based systems) to test that your LDAP server is accessible and authentication is working correctly. This is a freeware utility built by [Joe Richards](http://www.joeware.net/freetools/tools/adfind/index.htm).
-
-**Return all objects**
-
-You can use the filter `objectclass=*` to return all directory objects.
-
-```sh
-adfind -h ad.example.org:636 -ssl -u "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -up Password1 -b "OU=GitLab INT,DC=GitLab,DC=org" -f (objectClass=*)
-```
-
-**Return single object using filter**
-
-You can also retrieve a single object by **specifying** the object name or full **DN**. In this example we specify the object name only `CN=Leroy Fox`.
-
-```sh
-adfind -h ad.example.org:636 -ssl -u "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -up Password1 -b "OU=GitLab INT,DC=GitLab,DC=org" -f (&(objectcategory=person)(CN=Leroy Fox))â€
-```
-
-#### Using **ldapsearch** (Unix)
-
-You can use the `ldapsearch` utility (on Unix based systems) to test that your LDAP server is accessible and authentication is working correctly. This utility is included in the [`ldap-utils`](https://wiki.debian.org/LDAP/LDAPUtils) package.
-
-**Return all objects**
-
-You can use the filter `objectclass=*` to return all directory objects.
-
-```sh
-ldapsearch -D "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" \
--w Password1 -p 636 -h ad.example.org \
--b "OU=GitLab INT,DC=GitLab,DC=org" -Z \
--s sub "(objectclass=*)"
-```
-
-**Return single object using filter**
-
-You can also retrieve a single object by **specifying** the object name or full **DN**. In this example we specify the object name only `CN=Leroy Fox`.
-
-```sh
-ldapsearch -D "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -w Password1 -p 389 -h ad.example.org -b "OU=GitLab INT,DC=GitLab,DC=org" -Z -s sub "CN=Leroy Fox"
-```
-
-**Full output of `ldapsearch` command:** - Filtering for _CN=Leroy Fox_
-
-```
-# LDAPv3
-# base <OU=GitLab INT,DC=GitLab,DC=org> with scope subtree
-# filter: CN=Leroy Fox
-# requesting: ALL
-#
-
-# Leroy Fox, Developers, United Kingdom, GitLab INT, GitLab.org
-dn: CN=Leroy Fox,OU=Developers,OU=United Kingdom,OU=GitLab INT,DC=GitLab,DC=or
- g
-objectClass: top
-objectClass: person
-objectClass: organizationalPerson
-objectClass: user
-cn: Leroy Fox
-sn: Fox
-givenName: Leroy
-distinguishedName: CN=Leroy Fox,OU=Developers,OU=United Kingdom,OU=GitLab INT,
- DC=GitLab,DC=org
-instanceType: 4
-whenCreated: 20170210030500.0Z
-whenChanged: 20170213050128.0Z
-displayName: Leroy Fox
-uSNCreated: 16790
-memberOf: CN=DevelopersUK,OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org
-uSNChanged: 20812
-name: Leroy Fox
-objectGUID:: rBCAo6NR6E6vfSKgzcUILg==
-userAccountControl: 512
-badPwdCount: 0
-codePage: 0
-countryCode: 0
-badPasswordTime: 0
-lastLogoff: 0
-lastLogon: 0
-pwdLastSet: 131311695009850084
-primaryGroupID: 513
-objectSid:: AQUAAAAAAAUVAAAA9GMAb7tdJZvsATf7ZwQAAA==
-accountExpires: 9223372036854775807
-logonCount: 0
-sAMAccountName: Leroyf
-sAMAccountType: 805306368
-userPrincipalName: Leroyf@GitLab.org
-objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=GitLab,DC=org
-dSCorePropagationData: 16010101000000.0Z
-lastLogonTimestamp: 131314356887754250
-
-# search result
-search: 2
-result: 0 Success
-
-# numResponses: 2
-# numEntries: 1
-```
-
-## Basic user authentication
-
-After configuring LDAP, basic authentication will be available. Users can then login using their directory credentials. An extra tab is added to the GitLab login screen for the configured LDAP server (e.g "**GitLab AD**").
-
-![GitLab OU Structure](img/user_auth.gif)
-
-Users that are removed from the LDAP base group (e.g `OU=GitLab INT,DC=GitLab,DC=org`) will be **blocked** in GitLab. [More information](../../administration/auth/ldap.md#security) on LDAP security.
-
-If `allow_username_or_email_login` is enabled in the LDAP configuration, GitLab will ignore everything after the first '@' in the LDAP username used on login. Example: The username `jon.doe@example.com` is converted to `jon.doe` when authenticating with the LDAP server. Disable this setting if you use `userPrincipalName` as the `uid`.
-
-## LDAP extended features on GitLab EE
-
-With [GitLab Enterprise Edition (EE)](https://about.gitlab.com/gitlab-ee/), besides everything we just described, you'll
-have extended functionalities with LDAP, such as:
-
-- Group sync
-- Group permissions
-- Updating user permissions
-- Multiple LDAP servers
-
-Read through the article on [LDAP for GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/) for an overview.
+This document was moved to [another location](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md).
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 862fe0868a6..06675e15d76 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -10,16 +10,6 @@ They are written by members of the GitLab Team and by
Part of the articles listed below link to the [GitLab Blog](https://about.gitlab.com/blog/),
where they were originally published.
-## Authentication
-
-Explore GitLab's supported [authentications methods](../topics/authentication/index.md):
-
-| Article title | Category | Publishing date |
-| :------------ | :------: | --------------: |
-| **LDAP** |
-| [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md)| Admin guide | 2017-05-03 |
-| [How to configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/) | Admin guide | 2017-05-03 |
-
## Build, test, and deploy with GitLab CI/CD
Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/README.md):
@@ -28,7 +18,6 @@ Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/READM
| :------------ | :------: | --------------: |
| [Autoscaling GitLab Runners on AWS](runner_autoscale_aws/index.md) | Admin guide | 2017-11-24 |
| [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md) | Tutorial | 2017-08-31 |
-| [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md) | Tutorial | 2017-08-15 |
| [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017-07-13 |
| [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/) | Concepts | 2017-07-11 |
| [Continuous Integration: From Jenkins to GitLab Using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/) | Concepts | 2017-07-27 |
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/index.md b/doc/articles/laravel_with_gitlab_and_envoy/index.md
index e0d8fb8d081..b20bd8c247a 100644
--- a/doc/articles/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/articles/laravel_with_gitlab_and_envoy/index.md
@@ -502,8 +502,8 @@ stages:
unit_test:
stage: test
script:
- - composer install
- cp .env.example .env
+ - composer install
- php artisan key:generate
- php artisan migrate
- vendor/bin/phpunit
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 12404eddbe2..05d792dea0f 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -121,7 +121,7 @@ Here is an collection of tutorials and guides on setting up your CI pipeline.
- [Analyze code quality with the Code Climate CLI](examples/code_climate.md)
- **Articles**
- [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](../articles/laravel_with_gitlab_and_envoy/index.md)
- - [How to deploy Maven projects to Artifactory with GitLab CI/CD](../articles/artifactory_and_gitlab/index.md)
+ - [How to deploy Maven projects to Artifactory with GitLab CI/CD](examples/artifactory_and_gitlab/index.md)
- [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
- [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 0a2419b7ed2..22afcb9199d 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -164,6 +164,21 @@ not without its own challenges:
- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest
form offered. To use a different driver, see
[Using the overlayfs driver](#using-the-overlayfs-driver).
+- Since the `docker:dind` container and the runner container don't share their
+ root filesystem, the job's working directory can be used as a mount point for
+ children containers. For example, if you have files you want to share with a
+ child container, you may create a subdirectory under `/builds/$CI_PROJECT_PATH`
+ and use it as your mount point (for a more thorough explanation, check [issue
+ #41227](https://gitlab.com/gitlab-org/gitlab-ce/issues/41227)):
+
+ ```yaml
+ variables:
+ MOUNT_POINT: /builds/$CI_PROJECT_PATH/mnt
+
+ script:
+ - mkdir -p "$MOUNT_POINT"
+ - docker run -v "$MOUNT_POINT:/mnt" my-docker-image
+ ```
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index ecb8f15c851..fb5bfe26bb0 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -319,45 +319,62 @@ As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`][cmd].
> Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended
configuration options](#extended-docker-configuration-options).
+Before showing the available entrypoint override methods, let's describe shortly
+how the Runner starts and uses a Docker image for the containers used in the
+CI jobs:
+
+1. The Runner starts a Docker container using the defined entrypoint (default
+ from `Dockerfile` that may be overridden in `.gitlab-ci.yml`)
+1. The Runner attaches itself to a running container.
+1. The Runner prepares a script (the combination of
+ [`before_script`](../yaml/README.md#before_script),
+ [`script`](../yaml/README.md#script),
+ and [`after_script`](../yaml/README.md#after_script)).
+1. The Runner sends the script to the container's shell STDIN and receives the
+ output.
+
+To override the entrypoint of a Docker image, the recommended solution is to
+define an empty `entrypoint` in `.gitlab-ci.yml`, so the Runner doesn't start
+a useless shell layer. However, that will not work for all Docker versions, and
+you should check which one your Runner is using. Specifically:
+
+- If Docker 17.06 or later is used, the `entrypoint` can be set to an empty value.
+- If Docker 17.03 or previous versions are used, the `entrypoint` can be set to
+ `/bin/sh -c`, `/bin/bash -c` or an equivalent shell available in the image.
+
+The syntax of `image:entrypoint` is similar to [Dockerfile's `ENTRYPOINT`][entrypoint].
+
+----
+
Let's assume you have a `super/sql:experimental` image with some SQL database
inside it and you would like to use it as a base image for your job because you
want to execute some tests with this database binary. Let's also assume that
this image is configured with `/usr/bin/super-sql run` as an entrypoint. That
-means, that when starting the container without additional options, it will run
+means that when starting the container without additional options, it will run
the database's process, while Runner expects that the image will have no
-entrypoint or at least will start with a shell as its entrypoint.
-
-Before the new extended Docker configuration options, you would need to create
-your own image based on the `super/sql:experimental` image, set the entrypoint
-to a shell and then use it in job's configuration, like:
+entrypoint or that the entrypoint is prepared to start a shell command.
-```Dockerfile
-# my-super-sql:experimental image's Dockerfile
+With the extended Docker configuration options, instead of creating your
+own image based on `super/sql:experimental`, setting the `ENTRYPOINT`
+to a shell, and then using the new image in your CI job, you can now simply
+define an `entrypoint` in `.gitlab-ci.yml`.
-FROM super/sql:experimental
-ENTRYPOINT ["/bin/sh"]
-```
+**For Docker 17.06+:**
```yaml
-# .gitlab-ci.yml
-
-image: my-super-sql:experimental
+image:
+ name: super/sql:experimental
+ entrypoint: [""]
```
-After the new extended Docker configuration options, you can now simply
-set an `entrypoint` in `.gitlab-ci.yml`, like:
+**For Docker =< 17.03:**
```yaml
-# .gitlab-ci.yml
-
image:
name: super/sql:experimental
- entrypoint: ["/bin/sh"]
+ entrypoint: ["/bin/sh", "-c"]
```
-As you can see the syntax of `entrypoint` is similar to
-[Dockerfile's `ENTRYPOINT`][entrypoint].
-
## Define image and services in `config.toml`
Look for the `[runners.docker]` section:
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index d05b4db953a..25a0c5dcff5 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -27,8 +27,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
### Java
-- **Articles:**
- - [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/)
+- [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
@@ -41,18 +40,15 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
### Elixir
- [Test a Phoenix application](test-phoenix-application.md)
-- **Articles:**
- - [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
+- [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
### iOS
-- **Articles:**
- - [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
### Android
-- **Articles:**
- - [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
+- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
### Code quality analysis
@@ -63,20 +59,19 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Using `dpl` as deployment tool](deployment/README.md)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- **Articles:**
- - [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
+- [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
+- [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md)
-## GitLab CI for GitLab Pages
+## GitLab CI/CD for GitLab Pages
- [Example projects](https://gitlab.com/pages)
-- **Articles:**
- - [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](../../user/project/pages/getting_started_part_four.md)
- - [SSGs Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/):
- examples for Ruby-, NodeJS-, Python-, and GoLang-based SSGs
- - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
- - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
-
-See the topic [GitLab Pages](../../user/project/pages/index.md) for a complete overview.
+- [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](../../user/project/pages/getting_started_part_four.md)
+- [SSGs Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/):
+examples for Ruby-, NodeJS-, Python-, and GoLang-based SSGs
+- [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+- [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
+
+See the documentation on [GitLab Pages](../../user/project/pages/index.md) for a complete overview.
## More
diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md
new file mode 100644
index 00000000000..8e91cd05d8a
--- /dev/null
+++ b/doc/ci/examples/artifactory_and_gitlab/index.md
@@ -0,0 +1,282 @@
+---
+redirect_from: 'https://docs.gitlab.com/ee/articles/artifactory_and_gitlab/index.html'
+---
+
+# How to deploy Maven projects to Artifactory with GitLab CI/CD
+
+> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles):** tutorial ||
+> **Level:** intermediary ||
+> **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) ||
+> **Publication date:** 2017-08-15
+
+## Introduction
+
+In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/)
+to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/), and then use it from another Maven application as a dependency.
+
+You'll create two different projects:
+
+- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep)
+- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app)
+
+We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/).
+We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it.
+
+## Create the simple Maven dependency
+
+First of all, you need an application to work with: in this specific case we will
+use a simple one, but it could be any Maven application. This will be the
+dependency you want to package and deploy to Artifactory, in order to be
+available to other projects.
+
+### Prepare the dependency application
+
+For this article you'll use a Maven app that can be cloned from our example
+project:
+
+1. Log in to your GitLab account
+1. Create a new project by selecting **Import project from âž” Repo by URL**
+1. Add the following URL:
+
+ ```
+ https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git
+ ```
+1. Click **Create project**
+
+This application is nothing more than a basic class with a stub for a JUnit based test suite.
+It exposes a method called `hello` that accepts a string as input, and prints a hello message on the screen.
+
+The project structure is really simple, and you should consider these two resources:
+
+- `pom.xml`: project object model (POM) configuration file
+- `src/main/java/com/example/dep/Dep.java`: source of our application
+
+### Configure the Artifactory deployment
+
+The application is ready to use, but you need some additional steps to deploy it to Artifactory:
+
+1. Log in to Artifactory with your user's credentials.
+1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel.
+1. Copy to clipboard the configuration snippet under the **Deploy** paragraph.
+1. Change the `url` value in order to have it configurable via secret variables.
+1. Copy the snippet in the `pom.xml` file for your project, just after the
+ `dependencies` section. The snippet should look like this:
+
+ ```xml
+ <distributionManagement>
+ <repository>
+ <id>central</id>
+ <name>83d43b5afeb5-releases</name>
+ <url>${env.MAVEN_REPO_URL}/libs-release-local</url>
+ </repository>
+ </distributionManagement>
+ ```
+
+Another step you need to do before you can deploy the dependency to Artifactory
+is to configure the authentication data. It is a simple task, but Maven requires
+it to stay in a file called `settings.xml` that has to be in the `.m2` subdirectory
+in the user's homedir.
+
+Since you want to use GitLab Runner to automatically deploy the application, you
+should create the file in the project's home directory and set a command line
+parameter in `.gitlab-ci.yml` to use the custom location instead of the default one:
+
+1. Create a folder called `.m2` in the root of your repository
+1. Create a file called `settings.xml` in the `.m2` folder
+1. Copy the following content into a `settings.xml` file:
+
+ ```xml
+ <settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"
+ xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <servers>
+ <server>
+ <id>central</id>
+ <username>${env.MAVEN_REPO_USER}</username>
+ <password>${env.MAVEN_REPO_PASS}</password>
+ </server>
+ </servers>
+ </settings>
+ ```
+
+ Username and password will be replaced by the correct values using secret variables.
+
+### Configure GitLab CI/CD for `simple-maven-dep`
+
+Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and deploy the dependency!
+
+GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs
+that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/).
+
+First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > CI/CD** page
+and add the following secret variables (replace them with your current values, of course):
+
+- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL)
+- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username)
+- **MAVEN_REPO_PASS**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory Encrypted Password)
+
+Now it's time to define jobs in `.gitlab-ci.yml` and push it to the repo:
+
+```yaml
+image: maven:latest
+
+variables:
+ MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
+ MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
+
+cache:
+ paths:
+ - .m2/repository/
+ - target/
+
+build:
+ stage: build
+ script:
+ - mvn $MAVEN_CLI_OPTS compile
+
+test:
+ stage: test
+ script:
+ - mvn $MAVEN_CLI_OPTS test
+
+deploy:
+ stage: deploy
+ script:
+ - mvn $MAVEN_CLI_OPTS deploy
+ only:
+ - master
+```
+
+GitLab Runner will use the latest [Maven Docker image](https://hub.docker.com/_/maven/), which already contains all the tools and the dependencies you need to manage the project,
+in order to run the jobs.
+
+Environment variables are set to instruct Maven to use the `homedir` of the repo instead of the user's home when searching for configuration and dependencies.
+
+Caching the `.m2/repository folder` (where all the Maven files are stored), and the `target` folder (where our application will be created), is useful for speeding up the process
+by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary.
+
+Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application.
+
+Deploy to Artifactory is done as defined by the secret variables we have just set up.
+The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published.
+
+Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening.
+If the deployment has been successful, the deploy job log will output:
+
+```
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time: 1.983 s
+```
+
+>**Note**:
+the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log the first time you run it.
+
+Yay! You did it! Checking in Artifactory will confirm that you have a new artifact available in the `libs-release-local` repo.
+
+## Create the main Maven application
+
+Now that you have the dependency available on Artifactory, it's time to use it!
+Let's see how we can have it as a dependency to our main application.
+
+### Prepare the main application
+
+We'll use again a Maven app that can be cloned from our example project:
+
+1. Create a new project by selecting **Import project from âž” Repo by URL**
+1. Add the following URL:
+
+ ```
+ https://gitlab.com/gitlab-examples/maven/simple-maven-app.git
+ ```
+1. Click **Create project**
+
+This one is a simple app as well. If you look at the `src/main/java/com/example/app/App.java`
+file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter.
+
+Since Maven doesn't know how to resolve the dependency, you need to modify the configuration:
+
+1. Go back to Artifactory
+1. Browse the `libs-release-local` repository
+1. Select the `simple-maven-dep-1.0.jar` file
+1. Find the configuration snippet from the **Dependency Declaration** section of the main panel
+1. Copy the snippet in the `dependencies` section of the `pom.xml` file.
+ The snippet should look like this:
+
+ ```xml
+ <dependency>
+ <groupId>com.example.dep</groupId>
+ <artifactId>simple-maven-dep</artifactId>
+ <version>1.0</version>
+ </dependency>
+ ```
+
+### Configure the Artifactory repository location
+
+At this point you defined the dependency for the application, but you still miss where you can find the required files.
+You need to create a `.m2/settings.xml` file as you did for the dependency project, and let Maven know the location using environment variables.
+
+Here is how you can get the content of the file directly from Artifactory:
+
+1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel
+1. Click on **Generate Maven Settings**
+1. Click on **Generate Settings**
+1. Copy to clipboard the configuration file
+1. Save the file as `.m2/settings.xml` in your repo
+
+Now you are ready to use the Artifactory repository to resolve dependencies and use `simple-maven-dep` in your main application!
+
+### Configure GitLab CI/CD for `simple-maven-app`
+
+You need a last step to have everything in place: configure the `.gitlab-ci.yml` file for this project, as you already did for `simple-maven-dep`.
+
+You want to leverage [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and run your awesome application,
+and see if you can get the greeting as expected!
+
+All you need to do is to add the following `.gitlab-ci.yml` to the repo:
+
+```yaml
+image: maven:latest
+
+stages:
+ - build
+ - test
+ - run
+
+variables:
+ MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
+ MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
+
+cache:
+ paths:
+ - .m2/repository/
+ - target/
+
+build:
+ stage: build
+ script:
+ - mvn $MAVEN_CLI_OPTS compile
+
+test:
+ stage: test
+ script:
+ - mvn $MAVEN_CLI_OPTS test
+
+run:
+ stage: run
+ script:
+ - mvn $MAVEN_CLI_OPTS package
+ - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App"
+```
+
+It is very similar to the configuration used for `simple-maven-dep`, but instead of the `deploy` job there is a `run` job.
+Probably something that you don't want to use in real projects, but here it is useful to see the application executed automatically.
+
+And that's it! In the `run` job output log you will find a friendly hello to GitLab!
+
+## Conclusion
+
+In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume artifacts.
+
+A similar approach could be used to interact with any other Maven compatible Binary Repository Manager.
+Obviously, you can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow.
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
new file mode 100644
index 00000000000..a7945d05cd0
--- /dev/null
+++ b/doc/ci/examples/browser_performance.md
@@ -0,0 +1,50 @@
+# Browser Performance Testing with the Sitespeed.io container
+
+This example shows how to run the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on your code by using
+GitLab CI/CD and [Sitespeed.io](https://www.sitespeed.io) using Docker-in-Docker.
+
+First, you need a GitLab Runner with the [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
+
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `performance`:
+
+```yaml
+ stage: performance
+ image: docker:git
+ services:
+ - docker:dind
+ script:
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io --outputFolder sitespeed-results https://my.website.com
+ artifacts:
+ paths:
+ - sitespeed-results/
+```
+
+This will create a `performance` job in your CI/CD pipeline and will run Sitespeed.io against the webpage you define. The full HTML Sitespeed.io report will be saved as an artifact, and if you have Pages enabled it can be viewed directly in your browser. For further customization options of Sitespeed.io, including the ability to provide a list of URLs to test, please consult their [documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/).
+
+For GitLab [Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) users, a performance score can be automatically
+extracted and shown right in the merge request widget. Learn more about [Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
+
+## Performance testing on Review Apps
+
+The above CI YML is great for testing against static environments, and it can be extended for dynamic environments. There are a few extra steps to take to set this up:
+1. The `performance` job should run after the environment has started.
+1. In the `deploy` job, persist the hostname so it is available to the `performance` job. The same can be done for static environments like staging and production to unify the code path. Saving it as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`.
+1. In the `performance` job read the artifact into an environment variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test URL's.
+1. Now you can run the Sitespeed.io container against the desired hostname and paths.
+
+A simple `performance` job would look like:
+
+```yaml
+ stage: performance
+ image: docker:git
+ services:
+ - docker:dind
+ script:
+ - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io --outputFolder sitespeed-results $CI_ENVIRONMENT_URL
+ artifacts:
+ paths:
+ - sitespeed-results/
+```
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index 4d0ba8bfef3..6a5821762cc 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -16,8 +16,7 @@ codequality:
- docker:dind
script:
- docker pull codeclimate/codeclimate
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate init
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json || true
artifacts:
paths: [codeclimate.json]
```
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index e5a2bbd1773..df0e1521150 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -1,84 +1,106 @@
-# Using SSH keys
+---
+last_updated: 2017-12-13
+---
+
+# Using SSH keys with GitLab CI/CD
GitLab currently doesn't have built-in support for managing SSH keys in a build
-environment.
+environment (where the GitLab Runner runs).
The SSH keys can be useful when:
1. You want to checkout internal submodules
-2. You want to download private packages using your package manager (eg. bundler)
-3. You want to deploy your application to eg. Heroku or your own server
-4. You want to execute SSH commands from the build server to the remote server
-5. You want to rsync files from your build server to the remote server
+1. You want to download private packages using your package manager (e.g., Bundler)
+1. You want to deploy your application to your own server, or, for example, Heroku
+1. You want to execute SSH commands from the build environment to a remote server
+1. You want to rsync files from the build environment to a remote server
If anything of the above rings a bell, then you most likely need an SSH key.
-## Inject keys in your build server
-
The most widely supported method is to inject an SSH key into your build
-environment by extending your `.gitlab-ci.yml`.
-
-This is the universal solution which works with any type of executor
-(docker, shell, etc.).
-
-### How it works
-
-1. Create a new SSH key pair with [ssh-keygen][]
-2. Add the private key as a **Secret Variable** to the project
-3. Run the [ssh-agent][] during job to load the private key.
+environment by extending your `.gitlab-ci.yml`, and it's a solution which works
+with any type of [executor](https://docs.gitlab.com/runner/executors/)
+(Docker, shell, etc.).
+
+## 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
+ your project
+1. Run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during job to load
+ the private key.
+1. Copy the public key to the servers you want to have access to (usually in
+ `~/.ssh/authorized_keys`) or add it as a [deploy key](../../ssh/README.md#deploy-keys)
+ if you are accessing a private GitLab repository.
+
+NOTE: **Note:**
+The private key will not be displayed in the job trace, unless you enable
+[debug tracing](../variables/README.md#debug-tracing). You might also want to
+check the [visibility of your pipelines](../../user/project/pipelines/settings.md#visibility-of-pipelines).
## SSH keys when using the Docker executor
-You will first need to create an SSH key pair. For more information, follow the
-instructions to [generate an SSH key](../../ssh/README.md). Do not add a
-passphrase to the SSH key, or the `before_script` will prompt for it.
-
-Then, create a new **Secret Variable** in your project settings on GitLab
-following **Settings > CI/CD** and look for the "Secret Variables" section.
-As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the
-content of your _private_ key that you created earlier.
-
-It is also good practice to check the server's own public key to make sure you
-are not being targeted by a man-in-the-middle attack. To do this, add another
-variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run
-the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the
-server itself), and paste its output into the `SSH_SERVER_HOSTKEYS` variable. If
-you need to connect to multiple servers, concatenate all the server public keys
-that you collected into the **Value** of the variable. There must be one key per
-line.
-
-Next you need to modify your `.gitlab-ci.yml` with a `before_script` action.
-Add it to the top:
-
-```
-before_script:
- # Install ssh-agent if not already installed, it is required by Docker.
- # (change apt-get to yum if you use a CentOS-based image)
- - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
-
- # Run ssh-agent (inside the build environment)
- - eval $(ssh-agent -s)
-
- # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
- - ssh-add <(echo "$SSH_PRIVATE_KEY")
-
- # For Docker builds disable host key checking. Be aware that by adding that
- # you are suspectible to man-in-the-middle attacks.
- # WARNING: Use this only with the Docker executor, if you use it with shell
- # you will overwrite your user's SSH config.
- - mkdir -p ~/.ssh
- - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- # In order to properly check the server's host key, assuming you created the
- # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines
- # instead.
- # - mkdir -p ~/.ssh
- # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts'
-```
-
-As a final step, add the _public_ key from the one you created earlier to the
-services that you want to have an access to from within the build environment.
-If you are accessing a private GitLab repository you need to add it as a
-[deploy key](../../ssh/README.md#deploy-keys).
+When your CI/CD jobs run inside Docker containers (meaning the environment is
+contained) and you want to deploy your code in a private server, you need a way
+to access it. This is where an SSH key pair comes in handy.
+
+1. You will first need to create an SSH key pair. For more information, follow
+ the instructions to [generate an SSH key](../../ssh/README.md#generating-a-new-ssh-key-pair).
+ **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).
+ As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste
+ the content of your _private_ key that you created earlier.
+
+1. Modify your `.gitlab-ci.yml` with a `before_script` action. In the following
+ example, a Debian based image is assumed. Edit to your needs:
+
+ ```yaml
+ before_script:
+ ##
+ ## Install ssh-agent if not already installed, it is required by Docker.
+ ## (change apt-get to yum if you use an RPM-based image)
+ ##
+ - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+
+ ##
+ ## Run ssh-agent (inside the build environment)
+ ##
+ - eval $(ssh-agent -s)
+
+ ##
+ ## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
+ ## We're using tr to fix line endings which makes ed25519 keys work
+ ## without extra base64 encoding.
+ ## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
+ ##
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
+
+ ##
+ ## Create the SSH directory and give it the right permissions
+ ##
+ - mkdir -p ~/.ssh
+ - chmod 700 ~/.ssh
+
+ ##
+ ## Optionally, if you will be using any Git commands, set the user name and
+ ## and email.
+ ##
+ #- git config --global user.email "user@example.com"
+ #- git config --global user.name "User name"
+ ```
+
+ NOTE: **Note:**
+ The [`before_script`](../yaml/README.md#before-script) can be set globally
+ or per-job.
+
+1. Make sure the private server's [SSH host keys are verified](#verifying-the-ssh-host-keys).
+
+1. As a final step, add the _public_ key from the one you created in the first
+ step to the services that you want to have an access to from within the build
+ environment. If you are accessing a private GitLab repository you need to add
+ it as a [deploy key](../../ssh/README.md#deploy-keys).
That's it! You can now have access to private servers or repositories in your
build environment.
@@ -91,24 +113,93 @@ SSH key.
You can generate the SSH key from the machine that GitLab Runner is installed
on, and use that key for all projects that are run on this machine.
-First, you need to login to the server that runs your jobs.
+1. First, you need to login to the server that runs your jobs.
+
+1. Then from the terminal login as the `gitlab-runner` user:
-Then from the terminal login as the `gitlab-runner` user and generate the SSH
-key pair as described in the [SSH keys documentation](../../ssh/README.md).
+ ```
+ sudo su - gitlab-runner
+ ```
-As a final step, add the _public_ key from the one you created earlier to the
-services that you want to have an access to from within the build environment.
-If you are accessing a private GitLab repository you need to add it as a
-[deploy key](../../ssh/README.md#deploy-keys).
+1. Generate the SSH key pair as described in the instructions to
+ [generate an SSH key](../../ssh/README.md#generating-a-new-ssh-key-pair).
+ **Do not** add a passphrase to the SSH key, or the `before_script` will
+ prompt for it.
+
+1. As a final step, add the _public_ key from the one you created earlier to the
+ services that you want to have an access to from within the build environment.
+ If you are accessing a private GitLab repository you need to add it as a
+ [deploy key](../../ssh/README.md#deploy-keys).
Once done, try to login to the remote server in order to accept the fingerprint:
```bash
-ssh <address-of-my-server>
+ssh example.com
+```
+
+For accessing repositories on GitLab.com, you would use `git@gitlab.com`.
+
+## Verifying the SSH host keys
+
+It is a good practice to check the private server's own public key to make sure
+you are not being targeted by a man-in-the-middle attack. In case anything
+suspicious happens, you will notice it since the job would fail (the SSH
+connection would fail if the public keys would not match).
+
+To find out the host keys of your server, run the `ssh-keyscan` command from a
+trusted network (ideally, from the private server itself):
+
+```sh
+## Use the domain name
+ssh-keyscan example.com
+
+## Or use an IP
+ssh-keyscan 1.2.3.4
```
-For accessing repositories on GitLab.com, the `<address-of-my-server>` would be
-`git@gitlab.com`.
+Create a new [secret variable](../variables/README.md#secret-variables) with
+`SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`.
+
+NOTE: **Note:**
+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
+`.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,
+and you'll know there's something wrong with the server or the network.
+
+Now that the `SSH_KNOWN_HOSTS` variable is created, in addition to the
+[content of `.gitlab-ci.yml`](#ssh-keys-when-using-the-docker-executor)
+above, here's what more you need to add:
+
+ ```yaml
+before_script:
+ ##
+ ## Assuming you created the SSH_KNOWN_HOSTS variable, uncomment the
+ ## following two lines.
+ ##
+ - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts'
+ - chmod 644 ~/.ssh/known_hosts
+
+ ##
+ ## Alternatively, use ssh-keyscan to scan the keys of your private server.
+ ## Replace example.com with your private server's domain name. Repeat that
+ ## command if you have more than one server to connect to.
+ ##
+ #- ssh-keyscan example.com >> ~/.ssh/known_hosts
+ #- chmod 644 ~/.ssh/known_hosts
+
+ ##
+ ## You can optionally disable host key checking. Be aware that by adding that
+ ## you are suspectible to man-in-the-middle attacks.
+ ## WARNING: Use this only with the Docker executor, if you use it with shell
+ ## you will overwrite your user's SSH config.
+ ##
+ #- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+```
## Example project
@@ -119,6 +210,4 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available
Want to hack on it? Simply fork it, commit and push your changes. Within a few
moments the changes will be picked by a public runner and the job will begin.
-[ssh-keygen]: http://linux.die.net/man/1/ssh-keygen
-[ssh-agent]: http://linux.die.net/man/1/ssh-agent
[ssh-example-repo]: https://gitlab.com/gitlab-examples/ssh-private-key/
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index a9e6bda9916..b9d4a2098ed 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -213,14 +213,15 @@ An example project service that defines deployment variables is
## Debug tracing
> Introduced in GitLab Runner 1.7.
->
-> **WARNING:** Enabling debug tracing can have severe security implications. The
- output **will** contain the content of all your secret variables and any other
- secrets! The output **will** be uploaded to the GitLab server and made visible
- in job traces!
+
+CAUTION: **Warning:**
+Enabling debug tracing can have severe security implications. The
+output **will** contain the content of all your secret variables and any other
+secrets! The output **will** be uploaded to the GitLab server and made visible
+in job traces!
By default, GitLab Runner hides most of the details of what it is doing when
-processing a job. This behaviour keeps job traces short, and prevents secrets
+processing a job. This behavior keeps job traces short, and prevents secrets
from being leaked into the trace unless your script writes them to the screen.
If a job isn't working as expected, this can make the problem difficult to
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 31164ccd465..d14ba6ad522 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
This document was split into:
- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md).
diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md
index a0cb234bea0..0aef0bf5abb 100644
--- a/doc/customization/welcome_message.md
+++ b/doc/customization/welcome_message.md
@@ -8,5 +8,5 @@ It is possible to add a markdown-formatted welcome message to your GitLab
sign-in page. Users of GitLab Enterprise Edition should use the [branded login
page feature](branded_login_page.md) instead.
-The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI.
+The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI.
Admin area > Settings
diff --git a/doc/development/README.md b/doc/development/README.md
index 6892838be7f..12cca9f84b7 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -16,7 +16,8 @@ comments: false
- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md)
- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
-- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md)
+- [Automatic CE->EE merge](automatic_ce_ee_merge.md)
+- [Guidelines for implementing Enterprise Edition features](ee_features.md)
## UX and frontend guides
@@ -26,6 +27,7 @@ comments: false
## Backend guides
+- [GitLab utilities](utilities.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
@@ -36,6 +38,7 @@ comments: false
- [`Gemfile` guidelines](gemfile.md)
- [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid
+- [Avoid modules with instance variables](module_with_instance_variables.md) if possible
- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md)
- [Working with the GitHub importer](github_importer.md)
@@ -80,10 +83,9 @@ comments: false
## Documentation guides
-- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are
- contributing to the documentation.
- [Writing documentation](writing_documentation.md)
- - [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
+- [Documentation styleguide](doc_styleguide.md)
+- [Markdown](../user/markdown.md)
## Internationalization (i18n) guides
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
new file mode 100644
index 00000000000..4b9791c95bc
--- /dev/null
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -0,0 +1,93 @@
+# Automatic CE->EE merge
+
+GitLab Community Edition is merged automatically every 3 hours into the
+Enterprise Edition (look for the [`CE Upstream` merge requests]).
+
+This merge is done automatically in a
+[scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679).
+If a merge is already in progress, the job [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687).
+
+**If you are pinged in a `CE Upstream` merge request to resolve a conflict,
+please resolve the conflict as soon as possible or ask someone else to do it!**
+
+>**Note:**
+It's ok to resolve more conflicts than the one that you are asked to resolve. In
+that case, it's a good habit to ask for a double-check on your resolution by
+someone who is familiar with the code you touched.
+
+[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
+
+## Always merge EE merge requests before their CE counterparts
+
+**In order to avoid conflicts in the CE->EE merge, you should always merge the
+EE version of your CE merge request first, if present.**
+
+The rationale for this is that as CE->EE merges are done automatically every few
+hours, it can happen that:
+
+1. A CE merge request that needs EE-specific changes is merged
+1. The automatic CE->EE merge happens
+1. Conflicts due to the CE merge request occur since its EE merge request isn't
+ merged yet
+1. The automatic merge bot will ping someone to resolve the conflict **that are
+ already resolved in the EE merge request that isn't merged yet**
+
+That's a waste of time, and that's why you should merge EE merge request before
+their CE counterpart.
+
+## Avoiding CE->EE merge conflicts beforehand
+
+To avoid the conflicts beforehand, check out the
+[Guidelines for implementing Enterprise Edition features](ee_features.md).
+
+In any case, the CI `ee_compat_check` job will tell you if you need to open an
+EE version of your CE merge request.
+
+### Conflicts detection in CE merge requests
+
+For each commit (except on `master`), the `ee_compat_check` CI job tries to
+detect if the current branch's changes will conflict during the CE->EE merge.
+
+The job reports what files are conflicting and how to setup a merge request
+against EE.
+
+#### How the job works
+
+1. Generates the diff between your branch and current CE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds, otherwise...
+1. Detects a branch with the `ee-` prefix or `-ee` suffix in EE
+1. If it exists, generate the diff between this branch and current EE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds
+
+In the case where the job fails, it means you should create a `ee-<ce_branch>`
+or `<ce_branch>-ee` branch, push it to EE and open a merge request against EE
+`master`.
+At this point if you retry the failing job in your CE merge request, it should
+now pass.
+
+Notes:
+
+- This task is not a silver-bullet, its current goal is to bring awareness to
+ developers that their work needs to be ported to EE.
+- Community contributors shouldn't be required to submit merge requests against
+ EE, but reviewers should take actions by either creating such EE merge request
+ or asking a GitLab developer to do it **before the merge request is merged**.
+- If you branch is too far behind `master`, the job will fail. In that case you
+ should rebase your branch upon latest `master`.
+- Code reviews for merge requests often consist of multiple iterations of
+ feedback and fixes. There is no need to update your EE MR after each
+ iteration. Instead, create an EE MR as soon as you see the
+ `ee_compat_check` job failing. After you receive the final approval
+ from a Maintainer (but **before the CE MR is merged**) update the EE MR.
+ This helps to identify significant conflicts sooner, but also reduces the
+ number of times you have to resolve conflicts.
+- Please remember to
+ [always have your EE merge request merged before the CE version](#always-merge-ee-merge-requests-before-their-ce-counterparts).
+- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html)
+ to avoid resolving the same conflicts multiple times.
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index db13e0e6249..9cb1f708a6a 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -18,6 +18,10 @@ like `docs.gitlab.com/user/project/merge_requests.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 organise 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 |
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 932a44f65e4..1af839a27e1 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -1,4 +1,4 @@
-# Guidelines for implementing Enterprise Edition feature
+# Guidelines for implementing Enterprise Edition features
- **Write the code and the tests.**: As with any code, EE features should have
good test coverage to prevent regressions.
@@ -380,3 +380,9 @@ to avoid conflicts during CE to EE merge.
}
}
```
+
+## gitlab-svgs
+
+Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can
+be resolved simply by regenerating those assets with
+[`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md
index 962fe3dcec9..1daa6758171 100644
--- a/doc/development/fe_guide/axios.md
+++ b/doc/development/fe_guide/axios.md
@@ -11,7 +11,7 @@ This exported module should be used instead of directly using `axios` to ensure
## Usage
```javascript
- import axios from '~/lib/utils/axios_utils';
+ import axios from './lib/utils/axios_utils';
axios.get(url)
.then((response) => {
diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md
index e1660ac5caa..6314f8f38d2 100644
--- a/doc/development/fe_guide/dropdowns.md
+++ b/doc/development/fe_guide/dropdowns.md
@@ -4,15 +4,15 @@
## How to style a bootstrap dropdown
1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
1. Add a specific class to the top level `.dropdown` element
-
-
+
+
```Haml
.dropdown.my-dropdown
%button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
%span.dropdown-toggle-text
Toggle Dropdown
= icon('chevron-down')
-
+
%ul.dropdown-menu
%li
%a
@@ -29,10 +29,4 @@
item!
```
-1. Include the mixin in CSS
-
- ```SCSS
- @include new-style-dropdown('.my-dropdown ');
- ```
-
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 10f4c5a0902..1cd66f27492 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -86,34 +86,34 @@ followed by any global declarations, then a blank newline prior to any imports o
#### Modules, Imports, and Exports
1. Use ES module syntax to import modules
- ```javascript
- // bad
- const SomeClass = require('some_class');
+ ```javascript
+ // bad
+ const SomeClass = require('some_class');
- // good
- import SomeClass from 'some_class';
+ // good
+ import SomeClass from 'some_class';
- // bad
- module.exports = SomeClass;
+ // bad
+ module.exports = SomeClass;
- // good
- export default SomeClass;
- ```
-
- Import statements are following usual naming guidelines, for example object literals use camel case:
-
- ```javascript
- // some_object file
- export default {
- key: 'value',
- };
-
- // bad
- import ObjectLiteral from 'some_object';
+ // good
+ export default SomeClass;
+ ```
+
+ Import statements are following usual naming guidelines, for example object literals use camel case:
- // good
- import objectLiteral from 'some_object';
- ```
+ ```javascript
+ // some_object file
+ export default {
+ key: 'value',
+ };
+
+ // bad
+ import ObjectLiteral from 'some_object';
+
+ // good
+ import objectLiteral from 'some_object';
+ ```
1. Relative paths: when importing a module in the same directory, a child
directory, or an immediate parent directory prefer relative paths. When
@@ -334,33 +334,33 @@ A forEach will cause side effects, it will be mutating the array being iterated.
#### Alignment
1. Follow these alignment styles for the template method:
1. With more than one attribute, all attributes should be on a new line:
- ```javascript
- // bad
- <component v-if="bar"
- param="baz" />
+ ```javascript
+ // bad
+ <component v-if="bar"
+ param="baz" />
- <button class="btn">Click me</button>
+ <button class="btn">Click me</button>
- // good
- <component
- v-if="bar"
- param="baz"
- />
+ // good
+ <component
+ v-if="bar"
+ param="baz"
+ />
- <button class="btn">
- Click me
- </button>
- ```
+ <button class="btn">
+ Click me
+ </button>
+ ```
1. The tag can be inline if there is only one attribute:
- ```javascript
- // good
- <component bar="bar" />
+ ```javascript
+ // good
+ <component bar="bar" />
- // good
- <component
- bar="bar"
- />
- ```
+ // good
+ <component
+ bar="bar"
+ />
+ ```
#### Quotes
1. Always use double quotes `"` inside templates and single quotes `'` for all other JS.
@@ -414,7 +414,6 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. Default key should be provided if the prop is not required.
_Note:_ There are some scenarios where we need to check for the existence of the property.
On those a default key should not be provided.
-
```javascript
// good
props: {
@@ -494,21 +493,20 @@ On those a default key should not be provided.
#### Ordering
1. Tag order in `.vue` file
-
- ```
- <script>
- // ...
- </script>
-
- <template>
- // ...
- </template>
-
- // We don't use scoped styles but there are few instances of this
- <style>
- // ...
- </style>
- ```
+ ```
+ <script>
+ // ...
+ </script>
+
+ <template>
+ // ...
+ </template>
+
+ // We don't use scoped styles but there are few instances of this
+ <style>
+ // ...
+ </style>
+ ```
1. Properties in a Vue Component:
1. `name`
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index ca2048c7019..26abf967dcf 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -97,6 +97,29 @@ describe 'Gitaly Request count tests' do
end
```
+## Running tests with a locally modified version of Gitaly
+
+Normally, gitlab-ce/ee tests use a local clone of Gitaly in `tmp/tests/gitaly`
+pinned at the version specified in GITALY_SERVER_VERSION. If you want
+to run tests locally against a modified version of Gitaly you can
+replace `tmp/tests/gitaly` with a symlink.
+
+```shell
+rm -rf tmp/tests/gitaly
+ln -s /path/to/gitaly tmp/tests/gitaly
+```
+
+Make sure you run `make` in your local Gitaly directory before running
+tests. Otherwise, Gitaly will fail to boot.
+
+If you make changes to your local Gitaly in between test runs you need
+to manually run `make` again.
+
+Note that CI tests will not use your locally modified version of
+Gitaly. To use a custom Gitaly version in CI you need to update
+GITALY_SERVER_VERSION. You can use the format `=revision` to use a
+non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI.
+
---
[Return to Development documentation](README.md)
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index c2ca8966a3f..5786287d00c 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -8,7 +8,7 @@ might encounter or should avoid during development of GitLab CE and EE.
Consider the following factory:
```ruby
-FactoryGirl.define do
+FactoryBot.define do
factory :label do
sequence(:title) { |n| "label#{n}" }
end
@@ -53,7 +53,7 @@ When run, this spec doesn't do what we might expect:
(compared using ==)
```
-That's because FactoryGirl sequences are not reseted for each example.
+That's because FactoryBot sequences are not reseted for each example.
Please remember that sequence-generated values exist only to avoid having to
explicitly set attributes that have a uniqueness constraint when using a factory.
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 4b65a0f4a35..f493ad4ae66 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -215,6 +215,9 @@ There is also and alternative method to [translate messages from validation erro
sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe'
```
+The placeholders should match the code style of the respective source file.
+For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
+
### Plurals
- In Ruby/HAML:
@@ -259,6 +262,21 @@ Sometimes you need to add some context to the text that you want to translate
s__('OpenedNDaysAgo|Opened')
```
+### Dates / times
+
+- In JavaScript:
+
+```js
+import { createDateTimeFormat } from '.../locale';
+
+const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
+console.log(dateFormat.format(new Date('2063-04-05'))) // April 5, 2063
+```
+
+This makes use of [`Intl.DateTimeFormat`].
+
+[`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+
## Adding a new language
Let's suppose you want to add translations for a new language, let's say French.
diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md
index 4cb2624c098..8aa0462d213 100644
--- a/doc/development/i18n/index.md
+++ b/doc/development/i18n/index.md
@@ -59,6 +59,7 @@ Requests to become a proof reader will be considered on the merits of previous t
- French
- German
- Italian
+ - [Paolo Falomo](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Korean
- [Huang Tao](https://crowdin.com/profile/htve)
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
deleted file mode 100644
index ba82babb38a..00000000000
--- a/doc/development/limit_ee_conflicts.md
+++ /dev/null
@@ -1,347 +0,0 @@
-# Limit conflicts with EE when developing on CE
-
-This guide contains best-practices for avoiding conflicts between CE and EE.
-
-## Daily CE Upstream merge
-
-GitLab Community Edition is merged daily into the Enterprise Edition (look for
-the [`CE Upstream` merge requests]). The daily merge is currently done manually
-by four individuals.
-
-**If a developer pings you in a `CE Upstream` merge request for help with
-resolving conflicts, please help them because it means that you didn't do your
-job to reduce the conflicts nor to ease their resolution in the first place!**
-
-To avoid the conflicts beforehand when working on CE, there are a few tools and
-techniques that can help you:
-
-- know what are the usual types of conflicts and how to prevent them
-- the CI `rake ee_compat_check` job tells you if you need to open an EE-version
- of your CE merge request
-
-[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
-
-## Check the status of the CI `rake ee_compat_check` job
-
-For each commit (except on `master`), the `rake ee_compat_check` CI job tries to
-detect if the current branch's changes will conflict during the CE->EE merge.
-
-The job reports what files are conflicting and how to setup a merge request
-against EE. Here is roughly how it works:
-
-1. Generates the diff between your branch and current CE `master`
-1. Tries to apply it to current EE `master`
-1. If it applies cleanly, the job succeeds, otherwise...
-1. Detects a branch with the `-ee` suffix in EE
-1. If it exists, generate the diff between this branch and current EE `master`
-1. Tries to apply it to current EE `master`
-1. If it applies cleanly, the job succeeds
-
-In the case where the job fails, it means you should create a `<ce_branch>-ee`
-branch, push it to EE and open a merge request against EE `master`. At this
-point if you retry the failing job in your CE merge request, it should now pass.
-
-Notes:
-
-- This task is not a silver-bullet, its current goal is to bring awareness to
- developers that their work needs to be ported to EE.
-- Community contributors shouldn't submit merge requests against EE, but
- reviewers should take actions by either creating such EE merge request or
- asking a GitLab developer to do it once the merge request is merged.
-- If you branch is more than 500 commits behind `master`, the job will fail and
- you should rebase your branch upon latest `master`.
-- Code reviews for merge requests often consist of multiple iterations of
- feedback and fixes. There is no need to update your EE MR after each
- iteration. Instead, create an EE MR as soon as you see the
- `rake ee_compat_check` job failing. After you receive the final acceptance
- from a Maintainer (but before the CE MR is merged) update the EE MR.
- This helps to identify significant conflicts sooner, but also reduces the
- number of times you have to resolve conflicts.
-- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html)
- to avoid resolving the same conflicts multiple times.
-
-## Possible type of conflicts
-
-### Controllers
-
-#### List or arrays are augmented in EE
-
-In controllers, the most common type of conflict is with `before_action` that
-has a list of actions in CE but EE adds some actions to that list.
-
-The same problem often occurs for `params.require` / `params.permit` calls.
-
-##### Mitigations
-
-Separate CE and EE actions/keywords. For instance for `params.require` in
-`ProjectsController`:
-
-```ruby
-def project_params
- params.require(:project).permit(project_params_ce)
- # On EE, this is always:
- # params.require(:project).permit(project_params_ce << project_params_ee)
-end
-
-# Always returns an array of symbols, created however best fits the use case.
-# It _should_ be sorted alphabetically.
-def project_params_ce
- %i[
- description
- name
- path
- ]
-end
-
-# (On EE)
-def project_params_ee
- %i[
- approvals_before_merge
- approver_group_ids
- approver_ids
- ...
- ]
-end
-```
-
-#### Additional condition(s) in EE
-
-For instance for LDAP:
-
-```diff
- def destroy
- @key = current_user.keys.find(params[:id])
- - @key.destroy
- + @key.destroy unless @key.is_a? LDAPKey
-
- respond_to do |format|
-```
-
-Or for Geo:
-
-```diff
-def after_sign_out_path_for(resource)
-- current_application_settings.after_sign_out_path.presence || new_user_session_path
-+ if Gitlab::Geo.secondary?
-+ Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
-+ else
-+ current_application_settings.after_sign_out_path.presence || new_user_session_path
-+ end
-end
-```
-
-Or even for audit log:
-
-```diff
-def approve_access_request
-- Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
-+ member = Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
-+
-+ log_audit_event(member, action: :create)
-
- redirect_to polymorphic_url([membershipable, :members])
-end
-```
-
-### Views
-
-#### Additional view code in EE
-
-A block of code added in CE conflicts because there is already another block
-at the same place in EE
-
-##### Mitigations
-
-Blocks of code that are EE-specific should be moved to partials as much as
-possible to avoid conflicts with big chunks of HAML code that that are not fun
-to resolve when you add the indentation to the equation.
-
-For instance this kind of thing:
-
-```haml
-.form-group.detail-page-description
- = form.label :description, 'Description', class: 'control-label'
- .col-sm-10
- = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: form, attr: :description,
- classes: 'note-textarea',
- placeholder: "Write a comment or drag your files here...",
- supports_quick_actions: !issuable.persisted?
- = render 'projects/notes/hints', supports_quick_actions: !issuable.persisted?
- .clearfix
- .error-alert
-- if issuable.is_a?(Issue)
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = form.label :confidential do
- = form.check_box :confidential
- This issue is confidential and should only be visible to team members with at least Reporter access.
-- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
- - has_due_date = issuable.has_attribute?(:due_date)
- %hr
- .row
- %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
- .form-group.issue-assignee
- = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- - if issuable.assignee_id
- = form.hidden_field :assignee_id
- = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
- .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) }
- .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, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
- .form-group
- - has_labels = @labels && @labels.any?
- = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
- = 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}" }
- .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"
- - if issuable.respond_to?(:weight)
- - weight_options = Issue.weight_options
- - weight_options.delete(Issue::WEIGHT_ALL)
- - weight_options.delete(Issue::WEIGHT_ANY)
- .form-group
- = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
- Weight
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- - if issuable.weight
- = form.hidden_field :weight
- = dropdown_tag(issuable.weight || "Weight", options: { title: "Select weight", toggle_class: 'js-weight-select js-issuable-form-weight', dropdown_class: "dropdown-menu-selectable dropdown-menu-weight",
- placeholder: "Search weight", data: { field_name: "#{issuable.class.model_name.param_key}[weight]" , default_label: "Weight" } }) do
- %ul
- - weight_options.each do |weight|
- %li
- %a{href: "#", data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight)}
- = weight
- - if has_due_date
- .col-lg-6
- .form-group
- = form.label :due_date, "Due date", class: "control-label"
- .col-sm-10
- .issuable-form-select-holder
- = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
-```
-
-could be simplified by using partials:
-
-```haml
-= render 'shared/issuable/form/description', issuable: issuable, form: form
-
-- if issuable.respond_to?(:confidential)
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = form.label :confidential do
- = form.check_box :confidential
- 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
-```
-
-and then the `app/views/shared/issuable/form/_metadata.html.haml` could be as follows:
-
-```haml
-- issuable = local_assigns.fetch(:issuable)
-
-- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
-
-- has_due_date = issuable.has_attribute?(:due_date)
-- has_labels = @labels && @labels.any?
-- form = local_assigns.fetch(:form)
-
-%hr
-.row
- %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
- .form-group.issue-assignee
- = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- - if issuable.assignee_id
- = form.hidden_field :assignee_id
- = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
- .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) }
- .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, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
- .form-group
- - has_labels = @labels && @labels.any?
- = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
- = 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}" }
- .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/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
- .issuable-form-select-holder
- = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
-```
-
-and then the `app/views/shared/issuable/form/_weight.html.haml` could be as follows:
-
-```haml
-- issuable = local_assigns.fetch(:issuable)
-
-- return unless issuable.respond_to?(:weight)
-
-- has_due_date = issuable.has_attribute?(:due_date)
-- form = local_assigns.fetch(:form)
-
-.form-group
- = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
- Weight
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- - if issuable.weight
- = form.hidden_field :weight
-
- = weight_dropdown_tag(issuable, toggle_class: 'js-issuable-form-weight') do
- %ul
- - Issue.weight_options.each do |weight|
- %li
- %a{ href: '#', data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight) }
- = weight
-```
-
-Note:
-
-- The safeguards at the top allow to get rid of an unneccessary indentation level
-- Here we only moved the 'Weight' code to a partial since this is the only
- EE-specific code in that view, so it's the most likely to conflict, but you
- are encouraged to use partials even for code that's in CE to logically split
- big views into several smaller files.
-
-#### Indentation issue
-
-Sometimes a code block is indented more or less in EE because there's an
-additional condition.
-
-##### Mitigations
-
-Blocks of code that are EE-specific should be moved to partials as much as
-possible to avoid conflicts with big chunks of HAML code that that are not fun
-to resolve when you add the indentation in the equation.
-
-### Assets
-
-#### gitlab-svgs
-
-Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
-
----
-
-[Return to Development documentation](README.md)
diff --git a/doc/development/module_with_instance_variables.md b/doc/development/module_with_instance_variables.md
new file mode 100644
index 00000000000..48a1b7f847e
--- /dev/null
+++ b/doc/development/module_with_instance_variables.md
@@ -0,0 +1,242 @@
+## Modules with instance variables could be considered harmful
+
+### Background
+
+Rails somehow encourages people using modules and instance variables
+everywhere. For example, using instance variables in the controllers,
+helpers, and views. They're also encouraging the use of
+`ActiveSupport::Concern`, which further strengthens the idea of
+saving everything in a giant, single object, and people could access
+everything in that one giant object.
+
+### The problems
+
+Of course this is convenient to develop, because we just have everything
+within reach. However this has a number of downsides when that chosen object
+is growing, it would later become out of control for the same reason.
+
+There are just too many things in the same context, and we don't know if
+those things are tightly coupled or not, depending on each others or not.
+It's very hard to tell when the complexity grows to a point, and it makes
+tracking the code also extremely hard. For example, a class could be using
+3 different instance variables, and all of them could be initialized and
+manipulated from 3 different modules. It's hard to track when those variables
+start giving us troubles. We don't know which module would suddenly change
+one of the variables. Everything could touch anything.
+
+### Similar concerns
+
+People are saying multiple inheritance is bad. Mixing multiple modules with
+multiple instance variables scattering everywhere suffer from the same issue.
+The same applies to `ActiveSupport::Concern`. See:
+[Consider replacing concerns with dedicated classes & composition](
+https://gitlab.com/gitlab-org/gitlab-ce/issues/23786)
+
+There's also a similar idea:
+[Use decorators and interface segregation to solve overgrowing models problem](
+https://gitlab.com/gitlab-org/gitlab-ce/issues/13484)
+
+Note that `included` doesn't solve the whole issue. They define the
+dependencies, but they still allow each modules to talk implicitly via the
+instance variables in the final giant object, and that's where the problem is.
+
+### Solutions
+
+We should split the giant object into multiple objects, and they communicate
+with each other with the API, i.e. public methods. In short, composition over
+inheritance. This way, each smaller objects would have their own respective
+limited states, i.e. instance variables. If one instance variable goes wrong,
+we would be very clear that it's from that single small object, because
+no one else could be touching it.
+
+With clearly defined API, this would make things less coupled and much easier
+to debug and track, and much more extensible for other objects to use, because
+they communicate in a clear way, rather than implicit dependencies.
+
+### Acceptable use
+
+However, it's not always bad to use instance variables in a module,
+as long as it's contained in the same module; that is, no other modules or
+objects are touching them, then it would be an acceptable use.
+
+We especially allow the case where a single instance variable is used with
+`||=` to setup the value. This would look like:
+
+``` ruby
+module M
+ def f
+ @f ||= true
+ end
+end
+```
+
+Unfortunately it's not easy to code more complex rules into the cop, so
+we rely on people's best judgement. If we could find another good pattern
+we could easily add to the cop, we should do it.
+
+### How to rewrite and avoid disabling this cop
+
+Even if we could just disable the cop, we should avoid doing so. Some code
+could be easily rewritten in simple form. Consider this acceptable method:
+
+``` ruby
+module Gitlab
+ module Emoji
+ def emoji_unicode_version(name)
+ @emoji_unicode_versions_by_name ||=
+ JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
+ @emoji_unicode_versions_by_name[name]
+ end
+ end
+end
+```
+
+This method is totally fine because it's already self-contained. No other
+methods should be using `@emoji_unicode_versions_by_name` and we're good.
+However it's still offending the cop because it's not just `||=`, and the
+cop is not smart enough to judge that this is fine.
+
+On the other hand, we could split this method into two:
+
+``` ruby
+module Gitlab
+ module Emoji
+ def emoji_unicode_version(name)
+ emoji_unicode_versions_by_name[name]
+ end
+
+ private
+
+ def emoji_unicode_versions_by_name
+ @emoji_unicode_versions_by_name ||=
+ JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
+ end
+ end
+end
+```
+
+Now the cop won't complain. Here's a bad example which we could rewrite:
+
+``` ruby
+module SpamCheckService
+ def filter_spam_check_params
+ @request = params.delete(:request)
+ @api = params.delete(:api)
+ @recaptcha_verified = params.delete(:recaptcha_verified)
+ @spam_log_id = params.delete(:spam_log_id)
+ end
+
+ def spam_check(spammable, user)
+ spam_service = SpamService.new(spammable, @request)
+
+ spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do
+ user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
+ end
+ end
+end
+```
+
+There are several implicit dependencies here. First, `params` should be
+defined before use. Second, `filter_spam_check_params` should be called
+before `spam_check`. These are all implicit and the includer could be using
+those instance variables without awareness.
+
+This should be rewritten like:
+
+``` ruby
+class SpamCheckService
+ def initialize(request:, api:, recaptcha_verified:, spam_log_id:)
+ @request = request
+ @api = api
+ @recaptcha_verified = recaptcha_verified
+ @spam_log_id = spam_log_id
+ end
+
+ def spam_check(spammable, user)
+ spam_service = SpamService.new(spammable, @request)
+
+ spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do
+ user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
+ end
+ end
+end
+```
+
+And use it like:
+
+``` ruby
+class UpdateSnippetService < BaseService
+ def execute
+ # ...
+ spam = SpamCheckService.new(params.slice!(:request, :api, :recaptcha_verified, :spam_log_id))
+
+ spam.check(snippet, current_user)
+ # ...
+ end
+end
+```
+
+This way, all those instance variables are isolated in `SpamCheckService`
+rather than whatever includes the module, and those modules which were also
+included, making it much easier to track down any issues,
+and reducing the chance of having name conflicts.
+
+### How to disable this cop
+
+Put the disabling comment right after your code in the same line:
+
+``` ruby
+module M
+ def violating_method
+ @f + @g # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+end
+```
+
+If there are multiple lines, you could also enable and disable for a section:
+
+``` ruby
+module M
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def violating_method
+ @f = 0
+ @g = 1
+ @h = 2
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
+```
+
+Note that you need to enable it at some point, otherwise everything below
+won't be checked.
+
+### Things we might need to ignore right now
+
+Because of the way Rails helpers and mailers work, we might not be able to
+avoid the use of instance variables there. For those cases, we could ignore
+them at the moment. At least we're not going to share those modules with
+other random objects, so they're still somewhat isolated.
+
+### Instance variables in views
+
+They're bad because we can't easily tell who's using the instance variables
+(from controller's point of view) and where we set them up (from partials'
+point of view), making it extremely hard to track data dependency.
+
+We're trying to use something like this instead:
+
+``` haml
+= render 'projects/commits/commit', commit: commit, ref: ref, project: project
+```
+
+And in the partial:
+
+``` haml
+- ref = local_assigns.fetch(:ref)
+- commit = local_assigns.fetch(:commit)
+- project = local_assigns.fetch(:project)
+```
+
+This way it's clearer where those values were coming from, and we gain the
+benefit to have typo check over using instance variables. In the future,
+we should also forbid the use of instance variables in partials.
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 04419650b12..e7c5a6ca07a 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -37,7 +37,7 @@ graphs/dashboards.
GitLab provides built-in tools to aid the process of improving performance:
* [Sherlock](profiling.md#sherlock)
-* [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md)
+* [GitLab Performance Monitoring](../administration/monitoring/performance/index.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
* [QueryRecoder](query_recorder.md) for preventing `N+1` regressions
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
index 085fb8f902c..59ebf41e09f 100644
--- a/doc/development/sidekiq_style_guide.md
+++ b/doc/development/sidekiq_style_guide.md
@@ -9,25 +9,54 @@ All workers should include `ApplicationWorker` instead of `Sidekiq::Worker`,
which adds some convenience methods and automatically sets the queue based on
the worker's name.
-## Default Queue
+## Dedicated Queues
-Use of the "default" queue is not allowed. Every worker should use a queue that
-matches the worker's purpose the closest. For example, workers that are to be
-executed periodically should use the "cronjob" queue.
+All workers should use their own queue, which is automatically set based on the
+worker class name. For a worker named `ProcessSomethingWorker`, the queue name
+would be `process_something`. If you're not sure what queue a worker uses,
+you can find it using `SomeWorker.queue`. There is almost never a reason to
+manually override the queue name using `sidekiq_options queue: :some_queue`.
-A list of all available queues can be found in `config/sidekiq_queues.yml`.
+## Queue Namespaces
-## Dedicated Queues
+While different workers cannot share a queue, they can share a queue namespace.
-Most workers should use their own queue, which is automatically set based on the
-worker class name. For a worker named `ProcessSomethingWorker`, the queue name
-would be `process_something`. If you're not sure what a worker's queue name is,
-you can find it using `SomeWorker.queue`.
+Defining a queue namespace for a worker makes it possible to start a Sidekiq
+process that automatically handles jobs for all workers in that namespace,
+without needing to explicitly list all their queue names. If, for example, all
+workers that are managed by sidekiq-cron use the `cronjob` queue namespace, we
+can spin up a Sidekiq process specifically for these kinds of scheduled jobs.
+If a new worker using the `cronjob` namespace is added later on, the Sidekiq
+process will automatically pick up jobs for that worker too (after having been
+restarted), without the need to change any configuration.
+
+A queue namespace can be set using the `queue_namespace` DSL class method:
+
+```ruby
+class SomeScheduledTaskWorker
+ include ApplicationWorker
+
+ queue_namespace :cronjob
+
+ # ...
+end
+```
+
+Behind the scenes, this will set `SomeScheduledTaskWorker.queue` to
+`cronjob:some_scheduled_task`. Commonly used namespaces will have their own
+concern module that can easily be included into the worker class, and that may
+set other Sidekiq options besides the queue namespace. `CronjobQueue`, for
+example, sets the namespace, but also disables retries.
+
+`bundle exec sidekiq` is namespace-aware, and will automatically listen on all
+queues in a namespace (technically: all queues prefixed with the namespace name)
+when a namespace is provided instead of a simple queue name in the `--queue`
+(`-q`) option, or in the `:queues:` section in `config/sidekiq_queues.yml`.
-In some cases multiple workers do use the same queue. For example, the various
-workers for updating CI pipelines all use the `pipeline` queue. Adding workers
-to existing queues should be done with care, as adding more workers can lead to
-slow jobs blocking work (even for different jobs) on the shared queue.
+Note that adding a worker to an existing namespace should be done with care, as
+the extra jobs will take resources away from jobs from workers that were already
+there, if the resources available to the Sidekiq process handling the namespace
+are not adjusted appropriately.
## Tests
@@ -36,7 +65,7 @@ tests should be placed in `spec/workers`.
## Removing or renaming queues
-Try to avoid renaming or removing queues in minor and patch releases.
+Try to avoid renaming or removing workers and their queues in minor and patch releases.
During online update instance can have pending jobs and removing the queue can
lead to those jobs being stuck forever. If you can't write migration for those
Sidekiq jobs, please consider doing rename or remove queue in major release only.
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 8b7b015427f..edb8f372ea3 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -8,8 +8,8 @@ and effective _as well as_ fast.
Here are some things to keep in mind regarding test performance:
-- `double` and `spy` are faster than `FactoryGirl.build(...)`
-- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
+- `double` and `spy` are faster than `FactoryBot.build(...)`
+- `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
- Don't mark a feature as requiring JavaScript (through `@javascript` in
@@ -254,13 +254,13 @@ end
### Factories
-GitLab uses [factory_girl] as a test fixture replacement.
+GitLab uses [factory_bot] as a test fixture replacement.
- Factory definitions live in `spec/factories/`, named using the pluralization
of their corresponding model (`User` factories are defined in `users.rb`).
- There should be only one top-level factory definition per file.
-- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
- should) call `create(...)` instead of `FactoryGirl.create(...)`.
+- FactoryBot methods are mixed in to all RSpec groups. This means you can (and
+ should) call `create(...)` instead of `FactoryBot.create(...)`.
- Make use of [traits] to clean up definitions and usages.
- When defining a factory, don't define attributes that are not required for the
resulting record to pass validation.
@@ -269,8 +269,8 @@ GitLab uses [factory_girl] as a test fixture replacement.
- Factories don't have to be limited to `ActiveRecord` objects.
[See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
-[factory_girl]: https://github.com/thoughtbot/factory_girl
-[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
+[factory_bot]: https://github.com/thoughtbot/factory_bot
+[traits]: http://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md#Traits
### Fixtures
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md
new file mode 100644
index 00000000000..abe5b06e0f0
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end_tests.md
@@ -0,0 +1,80 @@
+# End-to-End Testing
+
+## What is End-to-End testing?
+
+End-to-End testing is a strategy used to check whether your application works
+as expected across entire software stack and architecture, including
+integration of all microservices and components that are supposed to work
+together.
+
+## How do we test GitLab?
+
+We use [Omnibus GitLab][omnibus-gitlab] to build GitLab packages and then we
+test these packages using [GitLab QA][gitlab-qa] project, which is entirely
+black-box, click-driven testing framework.
+
+### Testing nightly builds
+
+We run scheduled pipeline each night to test nightly builds created by Omnibus.
+You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pipelines].
+
+### Testing code in merge requests
+
+It is possible to run end-to-end tests (eventually being run within a
+[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering
+the `package-qa` manual action, that should be present in a merge request
+widget.
+
+Mmanual action that starts end-to-end tests is also available in merge requests
+in Omnibus GitLab project.
+
+Below you can read more about how to use it and how does it work.
+
+#### How does it work?
+
+Currently, we are using _multi-project pipeline_-like approach to run QA
+pipelines.
+
+1. Developer triggers a manual action, that can be found in CE and EE merge
+requests. This starts a chain of pipelines in multiple projects.
+
+1. The script being executed triggers a pipeline in GitLab Omnibus and waits
+for the resulting status. We call this a _status attribution_.
+
+1. GitLab packages are being built in Omnibus pipeline. Packages are going to be
+pushed to Container Registry.
+
+1. When packages are ready, and available in the registry, a final step in the
+pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab
+QA project. It also waits for a resulting status.
+
+1. GitLab QA pulls images from the registry, spins-up containers and runs tests
+against a test environment that has been just orchestrated by the `gitlab-qa`
+tool.
+
+1. The result of the GitLab QA pipeline is being propagated upstream, through
+Omnibus, back to CE / EE merge request.
+
+#### How do I write tests?
+
+In order to write new tests, you first need to learn more about GitLab QA
+architecture. See the [documentation about it][gitlab-qa-architecture] in
+GitLab QA project.
+
+Once you decided where to put test environment orchestration scenarios and
+instance specs, take a look at the [relevant documentation][instance-qa-readme]
+and examples in [the `qa/` directory][instance-qa-examples].
+
+## Where can I ask for help?
+
+You can ask question in the `#qa` channel on Slack (GitLab internal) or you can
+find an issue you would like to work on in [the issue tracker][gitlab-qa-issues]
+and start a new discussion there.
+
+[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
+[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
+[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines
+[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md
+[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues
+[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md
+[instance-qa-examples]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index 8045bbad7ba..74d09eb91ff 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -33,7 +33,7 @@ changes should be tested.
## [Testing best practices](best_practices.md)
-Everything you should know about how to write good tests: RSpec, FactoryGirl,
+Everything you should know about how to write good tests: RSpec, FactoryBot,
system tests, parameterized tests etc.
---
@@ -65,6 +65,13 @@ Everything you should know about how to test Rake tasks.
---
+## [End-to-end tests](end_to_end_tests.md)
+
+Everything you should know about how to run end-to-end tests using
+[GitLab QA][gitlab-qa] testing framework.
+
+---
+
## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
@@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead.
[Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/
[Jasmine]: https://jasmine.github.io/
+[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 1cbd4350284..4adf0dc7c7a 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -121,6 +121,9 @@ running feature tests (i.e. using Capybara) against it.
The actual test scenarios and steps are [part of GitLab Rails] so that they're
always in-sync with the codebase.
+Read a separate document about [end-to-end tests](end_to_end_tests.md) to
+learn more.
+
[multiple pieces]: ../architecture.md#components
[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
diff --git a/doc/development/utilities.md b/doc/development/utilities.md
new file mode 100644
index 00000000000..951c3ef85ce
--- /dev/null
+++ b/doc/development/utilities.md
@@ -0,0 +1,92 @@
+# GitLab utilities
+
+We developed a number of utilities to ease development.
+
+## [`MergeHash`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/utils/merge_hash.rb)
+
+* Deep merges an array of hashes:
+
+ ``` ruby
+ Gitlab::Utils::MergeHash.merge(
+ [{ hello: ["world"] },
+ { hello: "Everyone" },
+ { hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] } },
+ "Goodbye", "Hallo"]
+ )
+ ```
+
+ Gives:
+
+ ``` ruby
+ [
+ {
+ hello:
+ [
+ "world",
+ "Everyone",
+ { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] }
+ ]
+ },
+ "Goodbye"
+ ]
+ ```
+
+* Extracts all keys and values from a hash into an array:
+
+ ``` ruby
+ Gitlab::Utils::MergeHash.crush(
+ { hello: "world", this: { crushes: ["an entire", "hash"] } }
+ )
+ ```
+
+ Gives:
+
+ ``` ruby
+ [:hello, "world", :this, :crushes, "an entire", "hash"]
+ ```
+
+## [`StrongMemoize`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/utils/strong_memoize.rb)
+
+* Memoize the value even if it is `nil` or `false`.
+
+ We often do `@value ||= compute`, however this doesn't work well if
+ `compute` might eventually give `nil` and we don't want to compute again.
+ Instead we could use `defined?` to check if the value is set or not.
+ However it's tedious to write such pattern, and `StrongMemoize` would
+ help us use such pattern.
+
+ Instead of writing patterns like this:
+
+ ``` ruby
+ class Find
+ def result
+ return @result if defined?(@result)
+
+ @result = search
+ end
+ end
+ ```
+
+ We could write it like:
+
+ ``` ruby
+ class Find
+ include Gitlab::Utils::StrongMemoize
+
+ def result
+ strong_memoize(:result) do
+ search
+ end
+ end
+ end
+ ```
+
+* Clear memoization
+
+ ``` ruby
+ class Find
+ include Gitlab::Utils::StrongMemoize
+ end
+
+ Find.new.clear_memoization(:result)
+ ```
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 16a811dbc74..d396964e7c1 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -10,7 +10,7 @@
* [Tables](#tables)
* [Blocks](#blocks)
* [Panels](#panels)
-* [Dialog modals](#dialog-modals)
+* [Modals](#modals)
* [Alerts](#alerts)
* [Forms](#forms)
* [Search box](#search-box)
@@ -255,18 +255,18 @@ Skeleton loading can replace any existing UI elements for the period in which th
---
-## Dialog modals
+## Modals
-Dialog modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal.
+Modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal.
### Usage
-* When the action is irreversible, dialog modals provide the details and confirm with the user before they take an advanced action.
-* When the action will affect privacy or authorization, dialog modals provide advanced information and confirm with the user.
+* When the action is irreversible, modals provide the details and confirm with the user before they take an advanced action.
+* When the action will affect privacy or authorization, modals provide advanced information and confirm with the user.
### Style
-* Dialog modals contain the header, body, and actions.
+* Modals contain the header, body, and actions.
* **Header(1):** The header title is a question instead of a descriptive phrase.
* **Body(2):** The content in body should never be ambiguous and unclear. It provides specific information.
* **Actions(3):** Contains a affirmative action, a dismissive action, and an extra action. The order of actions from left to right: Dismissive action → Extra action → Affirmative action
@@ -277,13 +277,13 @@ Dialog modals are only used for having a conversation and confirmation with the
### Placement
-* Dialog modals should always be the center of the screen horizontally and be positioned **72px** from the top.
+* Modals should always be the center of the screen horizontally and be positioned **72px** from the top.
-| Dialog with 2 actions | Dialog with 3 actions | Special confirmation |
+| Modal with 2 actions | Modal with 3 actions | Special confirmation |
| --------------------- | --------------------- | -------------------- |
| ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![spcial-confirmation](img/modals-special-confimation-dialog.png) |
-> TODO: Special case for dialog modal.
+> TODO: Special case for modal.
---
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index 12e8d0a31bb..af842da7f62 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -46,11 +46,11 @@ Avoid using periods in solitary sentences in these elements:
* Labels
* Hover text
* Bulleted lists
-* Dialog body text
+* Modal body text
Periods should be used for:
-* Lists or dialogs with multiple sentences
+* Lists or modals with multiple sentences
* Any sentence followed by a link
| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence |
@@ -80,7 +80,7 @@ Omit punctuation after phrases and labels to create a cleaner and more readable
| Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description |
|---|---|---|---|---|---|---|
-| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
+| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and modal body text.<br><br>Use in lists or modals with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. |
| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. |
| Colon | **:** | `&#58;` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. |
@@ -88,7 +88,7 @@ Omit punctuation after phrases and labels to create a cleaner and more readable
| Quotation marks | **“**<br><br>**â€**<br><br>**‘**<br><br>**’** | `&ldquo;`<br><br>`&rdquo;`<br><br>`&lsquo;`<br><br>`&rsquo;` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. |
| Primes | **′**<br><br>**″** | `&prime;`<br><br>`&Prime;` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. |
| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `&quot;`<br><br>`&#39;`<br><br>`&#96;`<br><br>`&acute;` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. |
-| Ellipsis | **…** | `&hellip;` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…â€) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. |
+| Ellipsis | **…** | `&hellip;` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…â€) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a modal or start some other process. |
| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `&#171;`<br><br>`&#187;`<br><br>`&#8249;`<br><br>`&#8250;`<br><br>`&lt;`<br><br>`&gt;` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. |
| Em dash | **—** | `&mdash;` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. |
| En dash | **–** | `&ndash;` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kgâ€, “8:00 AM–12:30 PMâ€, “10–17 Jan†|
@@ -175,7 +175,7 @@ A **comment** is a written piece of text that users of GitLab can create. Commen
#### Discussion
A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
-## Confirmation dialogs
+## Modals
- Destruction buttons should be clear and always say what they are destroying.
E.g., `Delete page` instead of just `Delete`.
@@ -184,6 +184,8 @@ A **discussion** is a group of 1 or more comments. A discussion can include subd
- Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
confusing when you then see the `Cancel` button.
+see also: guidelines for [modal components](components.md#modals)
+
---
Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
diff --git a/doc/development/ux_guide/img/james-mackey.png b/doc/development/ux_guide/img/james-mackey.png
index 6db257c5b39..c8f9097f69f 100644
--- a/doc/development/ux_guide/img/james-mackey.png
+++ b/doc/development/ux_guide/img/james-mackey.png
Binary files differ
diff --git a/doc/development/ux_guide/img/karolina-plaskaty.png b/doc/development/ux_guide/img/karolina-plaskaty.png
index 2e356c99762..ae2e98b7bad 100644
--- a/doc/development/ux_guide/img/karolina-plaskaty.png
+++ b/doc/development/ux_guide/img/karolina-plaskaty.png
Binary files differ
diff --git a/doc/development/ux_guide/img/matthieu-poirier.png b/doc/development/ux_guide/img/matthieu-poirier.png
new file mode 100644
index 00000000000..dd21948ebe2
--- /dev/null
+++ b/doc/development/ux_guide/img/matthieu-poirier.png
Binary files differ
diff --git a/doc/development/ux_guide/img/nazim-ramesh.png b/doc/development/ux_guide/img/nazim-ramesh.png
index 01ba0391630..cc3e197679d 100644
--- a/doc/development/ux_guide/img/nazim-ramesh.png
+++ b/doc/development/ux_guide/img/nazim-ramesh.png
Binary files differ
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index fce882a45f1..6afb33cfc36 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -1,127 +1,252 @@
# UX Personas
* [Nazim Ramesh](#nazim-ramesh)
- - Small to medium size organisations using GitLab CE
+ - Small to medium size organizations using GitLab CE
+* [Matthieu Poirier](#matthieu-poirier)
+ - Responsible for managing and maintaining GitLab installation
+ - Any size organization
+ - Using CE or EE
* [James Mackey](#james-mackey)
- - Medium to large size organisations using CE or EE
- - Small organisations using EE
+ - Medium to large size organizations using CE or EE
+ - Small organizations using EE
* [Karolina Plaskaty](#karolina-plaskaty)
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
- - Working for a medium to large size organisation
+ - Working within a medium to large size organization
---
## Nazim Ramesh
-- Small to medium size organisations using GitLab CE
+- Small to medium size organizations using GitLab CE
-<img src="img/nazim-ramesh.png" width="300px">
+![nazim-ramesh](img/nazim-ramesh.png)
### Demographics
-- **Age**<br>32 years old
-- **Location**<br>Germany
-- **Education**<br>Bachelor of Science in Computer Science
-- **Occupation**<br>Full-stack web developer
-- **Programming experience**<br>Over 10 years
-- **Frequently used programming languages**<br>JavaScript, SQL, PHP
-- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
+**Age**
+
+32 years old
+
+**Location**
+
+Germany
+
+**Education**
+
+Bachelor of Science in Computer Science
+
+**Occupation**
+
+Full-stack web developer
+
+**Programming experience**
+
+Over 10 years
+
+**Frequently used programming languages**
+
+JavaScript, SQL, PHP
+
+**Hobbies / interests**
+
+Functional programming, open source, gaming, web development, and web security.
### Motivations
-Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management†returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
+Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wish list of features, Nazim began comparing source control tools. A search for "self-hosted Git server repository management" returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
->
-“I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.â€
->
+>"I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn't meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also, costs had to be low since we don't have a big budget for those things...the Community Edition was perfect in this regard."
In his role as a full-stack web developer, Nazim could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Nazim recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab.
->
-“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.â€
->
+>"The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use."
Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
->
-“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.â€
->
+>"Old SVN users couldn't see the benefits of Git at first. It took a month or two to convince them."
-Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
+Slowly, by showing his colleagues how easy it was to use Git, the majority of the team's projects were migrated to GitLab.
-The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
+The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company's decision to move to GitLab.
### Frustrations
#### Adoption to GitLab has been slow
-Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
+Not only has the engineering team had to get to grips with Git, they've also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilized. Nazim sold GitLab to his manager as an ‘all in one' tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn't had the time to integrate the legacy tools to GitLab and he's struggling to convince his peers to change their habits.
#### Missing Features
-Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
+Nazim's company want GitLab to be able to do everything. There isn't a large budget for software, so they're selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim's company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn't able to do what he or his colleagues need it to do.
#### Regressions and bugs
-Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks somethingâ€. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.†Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
+Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it "every new version adds something awesome, but breaks something". He feels that "old issues for minor annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs." Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it's also counterproductive to fix workflows every month.
#### Uses too much RAM and CPU
->
-“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHubâ€
->
+>"Memory usages mean that if we host it from a cloud-based host like AWS, we spend almost as much on the instance as what we would pay GitHub"
+
#### UI/UX
-GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.â€
+GitLab's interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab's interface daily has left him frustrated at the lack of personalization/control over his user experience. He's also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. "Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release."
### Goals
* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
-* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
+* To use a feature-rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc.
---
+## Matthieu Poirier
+- Responsible for managing and maintaining GitLab installation
+- Any size organization
+- Using CE or EE
+
+![matthieu-poirier](img/matthieu-poirier.png)
+
+### Demographics
+
+**Age**
+
+42 years old
+
+**Location**
+
+France
+
+**Education**
+
+Masters Degree in Computer Science
+
+**Occupation**
+
+DevOps Engineer
+
+**Programming experience**
+
+Over 10 years
+
+**Frequently used programming languages**
+
+JavaScript, SQL, PHP and Node.js
+
+**Hobbies / interests**
+
+Functional programming, data analysis, building apps, and tools.
+
+### Motivations
+Matthieu works in DevOps for a web services company which currently hires 90 staff. When Matthieu first joined the company, he was responsible for managing a custom built in-house bug tracking tool and release management system. Over time, as the company grew, his colleagues requested more features and tools to help them in their day-to-day work. To meet their needs, Matthieu was forced to "hack together" a solution. In his own words, Matthieu explains that it became:
+
+>"...a huge pain managing access to all the individual pieces. In addition, they didn't have any integration with each other, nobody ended up using them and we couldn't do any workflows with merge requests and the like. I was sick of managing all those separate parts and wanted to move to a single platform that would handle it all."
+
+
+He further explains that he wanted to introduce "better, easier, more formal code reviews" and to start using continuous integration and deployment.
+
+Matthieu tried to find a platform which would consolidate the company's existing toolset, and centralize code, documentation, and issues. He discovered GitHub, but the price was an issue:
+
+>"We needed to host our code on-site and wanted GitHub Enterprise functionality without the GitHub Enterprise costs."
+
+
+Not only was GitLab cheaper than GitHub, it was also more cost-effective than maintaining multiple tools. Subsequently, Matthieu found it easy to sell the merits of GitLab to his manager.
+
+Matthieu describes GitLab as:
+
+>"the only tool that offers the real feeling of having everything you need in one place."
+
+
+He credits himself as being entirely responsible for moving his company to GitLab.
+
+### Frustrations
+#### Updating to the latest release
+Matthieu introduced his company to GitLab. He is responsible for maintaining and managing the company's installation in addition to his day job. He feels updates are too frequent and he doesn't always have sufficient time to update GitLab. As a result, he's not up to date with releases.
+
+Matthieu tried to set up automatic updates, however, as he isn't a Systems Administrator, he wasn't confident in his set-up. He feels he should be able to "upgrade without users even noticing" but hasn't figured out how to do this yet. Matthieu would like the "update process to be triggered from the Admin Panel, perhaps accompanied with a changelog and the option to skip updates."
+
+Matthieu is looking for confirmation that his update procedure is "secure and efficient" so more tutorials related to this topic would be useful to him.
+
+#### Configuration
+Matthieu dislikes using the combination of gitlab.rb and the UI for changing settings. He explains that it "would be nice to be able to configure more from the Admin UI rather than just the config files."
+
+#### Creating a backup
+Matthieu explains that the "backup solution is not well integrated into the UI", for example, he "cannot see if backups succeeded" or whether they have been rolled back to via the UI.
+
+#### Onboarding
+It's Matthieu's responsibility to get teams across his organization up and running with GitLab. He explains that whilst many teams might be leveraging GitLab, they are:
+
+>"..not aware of GitLab's powerful CI or our omnibus install of Mattermost...It would be nice to have a tutorial type walkthrough available when a new user logs in on how to get started with all these features. AutoDevOps may solve some of this, but GitLab has many powerful features wrapped up into it and some [teams] may just think that it is only a Git repo similar to GitHub."
+
+
+He states that there has been: "a sluggishness of others to adapt" and it's "a low-effort adaptation at that."
+
+### Goals
+* To save time. One of the reasons Matthieu moved his company to GitLab was to reduce the effort it took him to manage and configure multiple tools, thus saving him time. He has to balance his day job in addition to managing the company's GitLab installation and onboarding new teams to GitLab.
+* To use a platform which is easy to manage. Matthieu isn't a Systems Administrator, and when updating GitLab, creating backups, etc. He would prefer to work within GitLab's UI. Explanations / guided instructions when configuring settings in GitLab's interface would really help Matthieu. He needs reassurance that what he is about to change is
+
+1. the right setting
+2. will provide him with the desired result he wants.
+
+* Matthieu needs to educate his colleagues about GitLab. Matthieu's colleagues won't adopt GitLab as they're unaware of its capabilities and the positive impact it could have on their work. Matthieu needs support in getting this message across to them.
+
+---
## James Mackey
-- Medium to large size organisations using CE or EE
-- Small organisations using EE
+- Medium to large size organizations using CE or EE
+- Small organizations using EE
-<img src="img/james-mackey.png" width="300px">
+![james-mackey.png](img/james-mackey.png)
### Demographics
-- **Age**<br>36 years old
-- **Location**<br>US
-- **Education**<br>Masters degree in Computer Science
-- **Occupation**<br>Full-stack web developer
-- **Programming experience**<br>Over 10 years
-- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python
-- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics.
+**Age**
+
+36 years old
+
+**Location**
+
+US
+
+**Education**
+
+Masters degree in Computer Science
+
+**Occupation**
+
+Full-stack web developer
+
+**Programming experience**
+
+Over 10 years
+
+**Frequently used programming languages**
+
+JavaScript, SQL, Node.js, Java, PHP, Python
+
+**Hobbies / interests**
+
+DevOps, open source, web development, science, automation, and electronics.
### Motivations
-James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and useâ€. After using GitLab for over a year, he began to wonder about using it at work. James explains:
+James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved "the simplicity of installation, administration and use". After using GitLab for over a year, he began to wonder about using it at work. James explains:
->
-“We first installed the CE edition...on a staging server for a PoC and asked a beta team to use it, specifically for the Merge Request features. Soon other teams began asking us to be beta users too, because the team that was already using GitLab was really enjoying it.â€
->
+>"We first installed the CE edition...on a staging server for a PoC and asked a beta team to use it, specifically for the Merge Request features. Soon other teams began asking us to be beta users too because the team that was already using GitLab was really enjoying it."
-James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHubâ€.
+
+James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it "less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHub".
The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
-James feels partially responsible for his organisation’s decision to start using GitLab.
+James feels partially responsible for his organization's decision to start using GitLab.
+
+>"It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching."
->
-“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.â€
->
### Frustrations
#### Third Party Integration
-Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do.
+Some of GitLab EE's features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn't essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn't sure what is/isn't possible with plugins and what level of custom development he and his team will need to do.
#### UX/UI
-James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised.
+James and his team use CI quite heavily for several projects. Whilst they've welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage's log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab's navigation needs to reviewed and optimized.
#### Permissions
->
-“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.â€
->
+>"There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines."
+
### Goals
-* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
+* To be able to integrate third-party tools easily with GitLab EE and to create custom integrations and patches where needed.
* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily.
* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James.
@@ -130,36 +255,56 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
## Karolina Plaskaty
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
-- Working for a medium to large size organisation
+- Working within a medium to large size organization
-<img src="img/karolina-plaskaty.png" width="300px">
+![karolina-plaskaty.png](img/karolina-plaskaty.png)
### Demographics
-- **Age**<br>26 years old
-- **Location**<br>UK
-- **Education**<br>Self taught
-- **Occupation**<br>Junior web-developer
-- **Programming experience**<br>6 years
-- **Frequently used programming languages**<br>JavaScript and SQL
-- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
+**Age**
+
+26 years old
+
+**Location**
+
+UK
+
+**Education**
+
+Self taught
+
+**Occupation**
+
+Junior web-developer
+
+**Programming experience**
+
+6 years
+
+**Frequently used programming languages**
+
+JavaScript and SQL
+
+**Hobbies / interests**
+
+Web development, mobile development, UX, open source, gaming, and travel.
### Motivations
-Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a companyâ€. She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.†She’s also an avid reader of GitLab’s blog.
+Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab's values and the fact that it isn't a "behemoth of a company". She explains that "displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work." She's also an avid reader of GitLab's blog.
-Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned†and explains that it’s “less of a technical issue and more of a cultural issue†to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms.
+Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as "old fashioned" and explains that it's "less of a technical issue and more of a cultural issue" to convince upper management to move to GitLab. Karolina is also relatively new to the company so she's apprehensive about pushing too hard to change version control platforms.
### Frustrations
#### Unable to use GitLab at work
-Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab.
+Karolina wants to use GitLab at work but isn't sure how to approach the subject with management. In her current role, she doesn't feel that she has the authority to request GitLab.
#### Performance
-GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog†which has deterred her from running GitLab on her own machine for just hobby / personal projects.
+GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a "memory hog" which has deterred her from running GitLab on her own machine for just hobby / personal projects.
#### UX/UI
-Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons†and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.†As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this.
+Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, "it has too many links/buttons" and the navigation "feels a bit weird sometimes. I get lost if I don't pay attention." As Karolina also enjoys contributing to open-source projects, it's important to her that GitLab is well designed for public repositories, she doesn't feel that GitLab currently achieves this.
### Goals
* To develop her programming experience and to learn from other developers.
* To contribute to both her own and other open source projects.
-* To use a fast and intuitive version control platform.
+* To use a fast and intuitive version control platform. \ No newline at end of file
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 05e0a64af18..9d0c62ecc35 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -195,6 +195,63 @@ end
And that's it, we're done!
+## Changing Column Types 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.
+
+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.
+
+Usage of this method is fairly simple:
+
+```ruby
+class ExampleMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+
+ include EachBatch
+
+ def self.to_migrate
+ where('closed_at IS NOT NULL')
+ end
+ end
+
+ def up
+ change_column_type_using_background_migration(
+ Issue.to_migrate,
+ :closed_at,
+ :datetime_with_timezone
+ )
+ end
+
+ def down
+ change_column_type_using_background_migration(
+ Issue.to_migrate,
+ :closed_at,
+ :datetime
+ )
+ end
+end
+```
+
+This would change the type of `issues.closed_at` to `timestamp with time zone`.
+
+Keep in mind that the relation passed to
+`change_column_type_using_background_migration` _must_ include `EachBatch`,
+otherwise it will raise a `TypeError`.
+
## 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 b6def7ef541..43a79ffcaa5 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -1,75 +1,14 @@
# Writing documentation
- **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
+ - **[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).
-## Distinction between General Documentation and 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 live under `doc/articles/article-title/index.md`, and their images should be placed under `doc/articles/article-title/img/`. Find a list of existing [technical articles](../articles/index.md) here.
-
-#### 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: "GitLab Pages from A to Z - [Part 1](../user/project/pages/getting_started_part_one.md) to [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, in the very beginning, a blockquote with the following information:
-
-- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial)
-- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced)
-- A reference to the **author's name** and **GitLab.com handle**
-- A reference of the **publication date**
-
-```md
-> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial ||
-> **Level:** intermediary ||
-> **Author:** [Name Surname](https://gitlab.com/username) ||
-> **Publication date:** AAAA/MM/DD
-```
-
-#### Technical Articles - Writing Method
-
-Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team.
-
## Documentation style guidelines
All the docs follow the same [styleguide](doc_styleguide.md).
-### Contributing to 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
@@ -118,13 +57,31 @@ and for every **major** feature present in Community Edition.
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.
-## Testing
+### Previewing locally
-We try to treat documentation as code, thus have implemented some testing.
+To preview your changes to documentation locally, please follow
+this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
+
+### 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.
+ 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
@@ -139,17 +96,7 @@ choices:
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).
----
-
-When you submit a merge request to GitLab Community Edition (CE), there is an
-additional job called `rake ee_compat_check` 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.
-Contributors do not need to submit their changes to EE, GitLab Inc. employees
-on the other hand need to make sure that their changes apply cleanly to both
-CE and EE.
-
-## Previewing the changes live
+### Previewing the changes live
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
@@ -164,7 +111,7 @@ 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](#testing) to avoid long running pipelines.
+[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.
@@ -209,12 +156,12 @@ working on. If you don't, the remote docs branch won't be removed either,
and the server where the Review Apps are hosted will eventually be out of
disk space.
-### Behind the scenes
+#### 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 [`scirpts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
+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
@@ -243,3 +190,65 @@ The following GitLab features are used among others:
- [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)
+
+## 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/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: "GitLab Pages from A to Z - [Part 1](../user/project/pages/getting_started_part_one.md) to [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, in the very beginning, a blockquote with the following information:
+
+- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial)
+- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced)
+- A reference to the **author's name** and **GitLab.com handle**
+- A reference of the **publication date**
+
+```md
+> **[Article Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial ||
+> **Level:** intermediary ||
+> **Author:** [Name Surname](https://gitlab.com/username) ||
+> **Publication date:** AAAA-MM-DD
+```
+
+#### Technical Articles - Writing Method
+
+Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team.
+
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 570b0d5b22f..2b7352d3561 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -80,7 +80,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 2.13.6 or higher
+ # Make sure Git is version 2.14.3 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -133,9 +133,9 @@ Remove the old Ruby 1.8 if present:
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
- echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
- cd ruby-2.3.5
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.6.tar.gz
+ echo '4e6a0f828819e15d274ae58485585fc8b7caace0 ruby-2.3.6.tar.gz' | shasum -c - && tar xzf ruby-2.3.6.tar.gz
+ cd ruby-2.3.6
./configure --disable-install-rdoc
make
sudo make install
@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-2-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-4-stable gitlab
-**Note:** You can change `10-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -367,7 +367,7 @@ sudo usermod -aG redis git
# Enable packfile bitmaps
sudo -u git -H git config --global repack.writeBitmaps true
-
+
# Enable push options
sudo -u git -H git config --global receive.advertisePushOptions true
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index 0932e1eee3a..cd889e74487 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -9,11 +9,11 @@ should be deployed, upgraded, and configured.
## Chart Overview
-* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small to medium deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
+* **[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/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in development. Will support large deployments with horizontal scaling of individual GitLab components.
* Other Charts
* [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
- * [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
+ * [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
## GitLab-Omnibus Chart (Recommended)
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 0e20b8096e9..20087a981f9 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -229,16 +229,18 @@ In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings ->
## Keep OmniAuth user profiles up to date
You can enable profile syncing from selected OmniAuth providers and for all or for specific user information.
-
+
+When authenticating using LDAP, the user's email is always synced.
+
```ruby
gitlab_rails['sync_profile_from_provider'] = ['twitter', 'google_oauth2']
gitlab_rails['sync_profile_attributes'] = ['name', 'email', 'location']
```
-
+
**For installations from source**
-
+
```yaml
omniauth:
sync_profile_from_provider: ['twitter', 'google_oauth2']
- sync_profile_claims_from_provider: ['email', 'location']
- ``` \ No newline at end of file
+ sync_profile_attributes: ['email', 'location']
+ ```
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index ae88baa0c14..4d6f02b6547 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -1 +1 @@
-This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/introduction.md).
+This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/index.md).
diff --git a/doc/operations/README.md b/doc/operations/README.md
index 58f16aff7bd..d7a83948b87 100644
--- a/doc/operations/README.md
+++ b/doc/operations/README.md
@@ -1 +1 @@
-This document was moved to [administration/operations](../administration/operations.md).
+This document was moved to [another location](../administration/operations/index.md).
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 597c98fbf6b..a8aa11265d0 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -6,6 +6,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [SSH](../../ssh/README.md)
- [Two-Factor Authentication (2FA)](../../user/profile/account/two_factor_authentication.md#two-factor-authentication)
+- [Why do I keep getting signed out?](../../user/profile/index.md#why-do-i-keep-getting-signed-out)
- **Articles:**
- [Support for Universal 2nd Factor Authentication - YubiKeys](https://about.gitlab.com/2016/06/22/gitlab-adds-support-for-u2f/)
- [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/)
@@ -19,7 +20,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html)
- [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
- **Articles:**
- - [How to Configure LDAP with GitLab CE](../../articles/how_to_configure_ldap_gitlab_ce/index.md)
+ - [How to Configure LDAP with GitLab CE](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md)
- [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/)
- [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
- [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/ldap/debugging_ldap.html)
diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png
deleted file mode 100644
index 067c9da3fdc..00000000000
--- a/doc/topics/autodevops/img/auto_devops_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index d100b431721..e23c73f46fb 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -19,6 +19,8 @@ project in an easy 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 Browser Performance Testing](#auto-browser-performance-testing)
1. [Auto Review Apps](#auto-review-apps)
1. [Auto Deploy](#auto-deploy)
1. [Auto Monitoring](#auto-monitoring)
@@ -121,11 +123,13 @@ Google Cloud.
## Enabling Auto DevOps
-**Note:**
If you haven't done already, read the [prerequisites](#prerequisites) to make
full use of Auto DevOps. If this is your fist time, we recommend you follow the
[quick start guide](#quick-start).
+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. Select "Enable Auto DevOps"
@@ -133,18 +137,13 @@ full use of Auto DevOps. If this is your fist time, we recommend you follow the
that will be used by Kubernetes to deploy your application
1. Hit **Save changes** for the changes to take effect
-![Project AutoDevops settings section](img/auto_devops_settings.png)
-
-Now that it's enabled, there are a few more steps depending on whether your project
-has a `.gitlab-ci.yml` or not:
+Once saved, an Auto DevOps pipeline will be triggered on the default branch.
-- **For projects with no `.gitlab-ci.yml` present:**
- A pipeline needs to be triggered either by pushing a new commit to the
- repository or manually visiting `https://example.gitlab.com/<username>/<project>/pipelines/new`
- and creating a new pipeline for your default branch, generally `master`.
-- **For projects with a `.gitlab-ci.yml` present:**
- All you need to do is remove your existing `.gitlab-ci.yml`, and you can even
- do that in a branch to test Auto DevOps before committing to `master`.
+NOTE: **Note:**
+For GitLab versions 10.0 - 10.2, when enabling Auto DevOps, a pipeline needs to be
+manually triggered either by pushing a new commit to the repository or by visiting
+`https://example.gitlab.com/<username>/<project>/pipelines/new` and creating
+a new pipeline for your default branch, generally `master`.
NOTE: **Note:**
If you are a GitLab Administrator, you can enable Auto DevOps instance wide
@@ -198,6 +197,32 @@ out. In GitLab Enterprise Edition Starter, differences between the source and
target branches are
[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
+### Auto SAST
+
+> Introduced in [GitLab Enterprise Edition Ultimate][ee] 10.3.
+
+Static Application Security Testing (SAST) uses the
+[gl-sast Docker image](https://gitlab.com/gitlab-org/gl-sast) to run static
+analysis on the current code and checks for potential security issues. Once the
+report is created, it's uploaded as an artifact which you can later download and
+check out.
+
+Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html).
+
+### Auto Browser Performance Testing
+
+> Introduced in [GitLab Enterprise Edition Premium][ee] 10.4.
+
+Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) to measure the performance of a web page. A JSON report is created and uploaded as an artifact, which includes the overall performance score for each page. By default, the root page of Review and Production environments will be tested. If you would like to add additional URL's to test, simply add the paths to a file named `.gitlab-urls.txt` in the root directory, one per line. For example:
+
+```
+/
+/features
+/direction
+```
+
+In GitLab Enterprise Edition 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 Review Apps
NOTE: **Note:**
@@ -536,3 +561,4 @@ 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/gitlab-ee/
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index df56f031970..588f4fa369f 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -61,6 +61,10 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/)
- [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
+## Troubleshooting
+
+- Learn a few [Git troubleshooting](troubleshooting_git.md) techniques to help you out.
+
## General information
- **Articles:**
diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md
new file mode 100644
index 00000000000..8555c5e91ea
--- /dev/null
+++ b/doc/topics/git/troubleshooting_git.md
@@ -0,0 +1,82 @@
+# Troubleshooting Git
+
+Sometimes things don't work the way they should or as you might expect when
+you're using Git. Here are some tips on troubleshooting and resolving issues
+with Git.
+
+## Broken pipe errors on git push
+
+'Broken pipe' errors can occur when attempting to push to a remote repository.
+When pushing you will usually see:
+
+```
+Write failed: Broken pipe
+fatal: The remote end hung up unexpectedly
+```
+
+To fix this issue, here are some possible solutions.
+
+### Increase the POST buffer size in Git
+
+**If pushing over HTTP**, you can try increasing the POST buffer size in Git's
+configuration. Open a terminal and enter:
+
+```sh
+git config http.postBuffer 52428800
+```
+
+The value is specified in bytes, so in the above case the buffer size has been
+set to 50MB. The default is 1MB.
+
+### Check your SSH configuration
+
+**If pushing over SSH**, first check your SSH configuration as 'Broken pipe'
+errors can sometimes be caused by underlying issues with SSH (such as
+authentication). Make sure that SSH is correctly configured by following the
+instructions in the [SSH troubleshooting] docs.
+
+There's another option where you can prevent session timeouts by configuring
+SSH 'keep alive' either on the client or on the server (if you are a GitLab
+admin and have access to the server).
+
+NOTE: **Note:** configuring *both* the client and the server is unnecessary.
+
+**To configure SSH on the client side**:
+
+- On UNIX, edit `~/.ssh/config` (create the file if it doesn’t exist) and
+ add or edit:
+
+ ```
+ Host your-gitlab-instance-url.com
+ ServerAliveInterval 60
+ ServerAliveCountMax 5
+ ```
+
+- On Windows, if you are using PuTTY, go to your session properties, then
+ navigate to "Connection" and under "Sending of null packets to keep
+ session active", set "Seconds between keepalives (0 to turn off)" to `60`.
+
+**To configure SSH on the server side**, edit `/etc/ssh/sshd_config` and add:
+
+```
+ClientAliveInterval 60
+ClientAliveCountMax 5
+```
+
+### Running a git repack
+
+**If 'pack-objects' type errors are also being displayed**, you can try to
+run a `git repack` before attempting to push to the remote repository again:
+
+```sh
+git repack
+git push
+```
+
+### Upgrade your Git client
+
+In case you're running an older version of Git (< 2.9), consider upgrading
+to >= 2.9 (see [Broken pipe when pushing to Git repository][Broken-Pipe]).
+
+[SSH troubleshooting]: ../../ssh/README.md#troubleshooting "SSH Troubleshooting"
+[Broken-Pipe]: https://stackoverflow.com/questions/19120120/broken-pipe-when-pushing-to-git-repository/36971469#36971469 "StackOverflow: 'Broken pipe when pushing to Git repository'"
diff --git a/doc/update/10.1-to-10.2.md b/doc/update/10.1-to-10.2.md
index 9e0d8f79522..632e8befa74 100644
--- a/doc/update/10.1-to-10.2.md
+++ b/doc/update/10.1-to-10.2.md
@@ -339,11 +339,11 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete!
-## Things went south? Revert to previous version (10.0)
+## Things went south? Revert to previous version (10.1)
### 1. Revert the code to the previous version
-Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the
+Follow the [upgrade guide from 10.0 to 10.1](10.0-to-10.1.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
diff --git a/doc/update/10.2-to-10.3.md b/doc/update/10.2-to-10.3.md
new file mode 100644
index 00000000000..d6e2db8a353
--- /dev/null
+++ b/doc/update/10.2-to-10.3.md
@@ -0,0 +1,360 @@
+---
+comments: false
+---
+
+# From 10.2 to 10.3
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+cd ruby-2.3.5
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-3-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-3-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-2-stable:config/gitlab.yml.example origin/10-3-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-2-stable:lib/support/nginx/gitlab-ssl origin/10-3-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-2-stable:lib/support/nginx/gitlab origin/10-3-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-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-2-stable:lib/support/init.d/gitlab.default.example origin/10-3-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.2)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.1 to 10.2](10.1-to-10.2.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/10.3-to-10.4.md b/doc/update/10.3-to-10.4.md
new file mode 100644
index 00000000000..850cb3103f4
--- /dev/null
+++ b/doc/update/10.3-to-10.4.md
@@ -0,0 +1,360 @@
+---
+comments: false
+---
+
+# From 10.3 to 10.4
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.6.tar.gz
+echo '4e6a0f828819e15d274ae58485585fc8b7caace0 ruby-2.3.6.tar.gz' | shasum -c - && tar xzf ruby-2.3.6.tar.gz
+cd ruby-2.3.6
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-4-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-4-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-3-stable:config/gitlab.yml.example origin/10-4-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-3-stable:lib/support/nginx/gitlab-ssl origin/10-4-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-3-stable:lib/support/nginx/gitlab origin/10-4-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-4-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-4-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-3-stable:lib/support/init.d/gitlab.default.example origin/10-4-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.3)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.2 to 10.3](10.2-to-10.3.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-4-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-4-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/discussions/img/commit_comment_mr_context.png b/doc/user/discussions/img/commit_comment_mr_context.png
new file mode 100644
index 00000000000..b363e0035e8
--- /dev/null
+++ b/doc/user/discussions/img/commit_comment_mr_context.png
Binary files differ
diff --git a/doc/user/discussions/img/commit_comment_mr_discussions_tab.png b/doc/user/discussions/img/commit_comment_mr_discussions_tab.png
new file mode 100644
index 00000000000..2b06cdcc055
--- /dev/null
+++ b/doc/user/discussions/img/commit_comment_mr_discussions_tab.png
Binary files differ
diff --git a/doc/user/discussions/img/merge_request_commits_tab.png b/doc/user/discussions/img/merge_request_commits_tab.png
new file mode 100644
index 00000000000..41a3648f390
--- /dev/null
+++ b/doc/user/discussions/img/merge_request_commits_tab.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 2206b2860f4..eacfe2baa27 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -32,6 +32,43 @@ hide discussions that are no longer relevant.
Comments and discussions can be resolved by anyone with at least Developer
access to the project or the author of the merge request.
+### Commit discussions in the context of a merge request
+
+> [Introduced][ce-31847] in GitLab 10.3.
+
+For reviewers with commit-based workflow, it may be useful to add discussions to
+specific commit diffs in the context of a merge request. These discussions will
+persist through a commit ID change when:
+
+- force-pushing after a rebase
+- amending a commit
+
+To create a commit diff discussion:
+
+1. Navigate to the merge request **Commits** tab. A list of commits that
+ constitute the merge request will be shown.
+
+ ![Merge request commits tab](img/merge_request_commits_tab.png)
+
+1. Navigate to a specific commit, click on the **Changes** tab (where you
+ will only be presented diffs from the selected commit), and leave a comment.
+
+ ![Commit diff discussion in merge request context](img/commit_comment_mr_context.png)
+
+1. Any discussions created this way will be shown in the merge request's
+ **Discussions** tab and are resolvable.
+
+ ![Merge request Discussions tab](img/commit_comment_mr_discussions_tab.png)
+
+Discussions created this way will only appear in the original merge request
+and not when navigating to that commit under your project's
+**Repository > Commits** page.
+
+TIP: **Tip:**
+When a link of a commit reference is found in a discussion inside a merge
+request, it will be automatically converted to a link in the context of the
+current merge request.
+
### Jumping between unresolved discussions
When a merge request has a large number of comments it can be difficult to track
@@ -133,6 +170,15 @@ From now on, any discussions on a diff will be resolved by default if a push
makes that diff section outdated. Discussions on lines that don't change and
top-level resolvable discussions are not automatically resolved.
+## Commit discussions
+
+You can add comments and discussion threads to a particular commit under your
+project's **Repository > Commits**.
+
+CAUTION: **Attention:**
+Discussions created this way will be lost if the commit ID changes after a
+force push.
+
## Threaded discussions
> [Introduced][ce-7527] in GitLab 9.1.
@@ -229,6 +275,7 @@ edit existing comments. Non-team members are restricted from adding or editing c
[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053
[ce-14061]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14061
[ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
+[ce-31847]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31847
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
diff --git a/doc/user/group/img/transfer_project_to_other_group.png b/doc/user/group/img/transfer_project_to_other_group.png
deleted file mode 100644
index 042c002f83f..00000000000
--- a/doc/user/group/img/transfer_project_to_other_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index a1671f9dd91..7f77a33aadc 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -65,7 +65,7 @@ together in a single list view.
> **Notes:**
- For a list of words that are not allowed to be used as group names see the
- [reserved names][reserved].
+ [reserved names](../reserved_names.md).
You can create a group in GitLab from:
@@ -97,7 +97,7 @@ Add members to a group by navigating to the group's dashboard, and clicking **Me
![add members to group](img/add_new_members.png)
-Select the [permission level][permissions] and add the new member. You can also set the expiring
+Select the [permission level](../permissions.md#permissions) and add the new member. You can also set the expiring
date for that user, from which they will no longer have access to your group.
One of the benefits of putting multiple projects in one group is that you can
@@ -152,16 +152,9 @@ There are two different ways to add a new project to a group:
![Select group](img/select_group_dropdown.png)
-## Transfer an existing project into a group
+## Transfer projects into groups
-You can transfer an existing project into a group as long as you have at least **Master** [permissions][permissions] to that group
-and if you are an **Owner** of the project.
-
-![Transfer a project to a new namespace](img/transfer_project_to_other_group.png)
-
-Find this option under your project's settings.
-
-GitLab administrators can use the admin interface to move any project to any namespace if needed.
+Learn how to [transfer a project into a group](../project/index.md#transfer-an-existing-project-into-a-group).
## Sharing a project with a group
@@ -190,24 +183,29 @@ access further configurations for your group.
#### Changing a group's path
-> **Note:** If you want to retain ownership over the original namespace and
-protect the URL redirects, then instead of changing a group's path or renaming a
-username, you can create a new group and transfer projects to it.
+Changing a group's path can have unintended side effects. Read
+[how redirects will behave](../project/index.md#redirects-when-changing-repository-paths)
+before proceeding.
-Changing a group's path can have unintended side effects.
+If you are vacating the path so it can be claimed by another group or user,
+you may need to rename the group name as well since both names and paths must
+be unique.
-* Existing web URLs for the group and anything under it (i.e. projects) will
-redirect to the new URLs
-* Existing Git remote URLs for projects under the group will no longer work, but
-Git responses will show an error with the new remote URL
-* The original namespace can be claimed again by any group or user, which will
-destroy web redirects and Git remote warnings
-* If you are vacating the path so it can be claimed by another group or user,
-you may need to rename the group name as well since both names and paths must be
-unique
+To change your group path:
-> It is currently not possible to rename a namespace if it contains a
-project with container registry tags, because the project cannot be moved.
+1. Navigate to your group's **Settings > General**.
+1. Enter a new name under "Group path".
+1. Hit **Save group**.
+
+CAUTION: **Caution:**
+It is currently not possible to rename a namespace if it contains a
+project with [Container Registry](../project/container_registry.md) tags,
+because the project cannot be moved.
+
+TIP: **TIP:**
+If you want to retain ownership over the original namespace and
+protect the URL redirects, then instead of changing a group's path or renaming a
+username, you can create a new group and transfer projects to it.
#### Enforce 2FA to group members
@@ -246,11 +244,7 @@ Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#
- **Projects**: view all projects within that group, add members to each project,
access each project's settings, and remove any project from the same screen.
- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md)
-and [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group (Push Rules is available in [GitLab Enteprise Edition Starter][ee].)
+and [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group (Push Rules is available in [GitLab Enteprise Edition Starter](https://about.gitlab.com/products/).)
- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html#audit-events)
for the group (GitLab admins only, available in [GitLab Enterprise Edition Starter][ee]).
- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group
-
-[permissions]: ../permissions.md#permissions
-[ee]: https://about.gitlab.com/products/
-[reserved]: ../reserved_names.md
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index a671c92640a..552abac747b 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -195,12 +195,23 @@ With inline diffs tags you can display {+ additions +} or [- deletions -].
The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+Examples:
+
+```
+- {+ additions +}
+- [+ additions +]
+- {- deletions -}
+- [- deletions -]
+```
+
However the wrapping tags cannot be mixed as such:
+```
- {+ additions +]
- [+ additions +}
- {- deletions -]
- [- deletions -}
+```
### Emoji
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 5fcc0501dc1..ab16f8d14c1 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -1,34 +1,12 @@
# User account
-When logged into their GitLab account, users can customize their
+When signed into their GitLab account, users can customize their
experience according to the best approach to their cases.
-## Username
+## Signing in
-Your `username` is a unique [`namespace`](../group/index.md#namespaces)
-related to your user ID.
-
-### Changing your username
-
-You can change your `username` from your
-[profile settings](#profile-settings).
-
-> **Note:** If you want to retain ownership over the original namespace and
-protect the URL redirects, then instead of changing your username, you can
-create a new group and transfer projects to it.
-Alternatively, you can follow [this detailed procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom).
-
-Changing your username can have unintended side effects.
-
-* Existing web URLs for the user and anything under it (i.e. projects) will
-redirect to the new URLs
-* Existing Git remote URLs for projects under the user will no longer work, but
-Git responses will show an error with the new remote URL
-* The original namespace can be claimed again by any group or user, which will
-destroy any web redirects and Git remote warnings
-
-> It is currently not possible to rename a namespace if it contains a
-project with container registry tags, because the project cannot be moved.
+There are several ways to sign into your GitLab account.
+See the [authentication topic](../../topics/authentication/index.md) for more details.
## User profile
@@ -61,4 +39,51 @@ From there, you can:
- Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH
- Manage your [preferences](preferences.md#syntax-highlighting-theme)
to customize your own GitLab experience
-- Acess your audit log, a security log of important events involving your account
+- Access your audit log, a security log of important events involving your account
+
+## Changing your username
+
+Your `username` is a unique [`namespace`](../group/index.md#namespaces)
+related to your user ID. Changing it can have unintended side effects, read
+[how redirects will behave](../project/index.md#redirects-when-changing-repository-paths)
+before proceeding.
+
+To change your `username`:
+
+1. Navigate to your [profile's](#profile-settings) **Settings > Account**.
+1. Enter a new username under "Change username".
+1. Hit **Update username**.
+
+CAUTION: **Caution:**
+It is currently not possible to change your username if it contains a
+project with [Container Registry](../project/container_registry.md) tags,
+because the project cannot be moved.
+
+TIP: **Tip:**
+If you want to retain ownership over the original namespace and
+protect the URL redirects, then instead of changing a group's path or renaming a
+username, you can create a new group and transfer projects to it.
+Alternatively, you can follow [this detailed procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom)
+which also covers the case where you have projects hosted with
+[GitLab Pages](../project/pages/index.md).
+
+## Troubleshooting
+
+### Why do I keep getting signed out?
+
+When signing in to the main GitLab application, a `_gitlab_session` cookie is
+set. `_gitlab_session` is cleared client-side when you close your browser
+and expires after "Application settings -> Session duration (minutes)"/`session_expire_delay`
+(defaults to `10080` minutes = 7 days).
+
+When signing in to the main GitLab application, you can also check the
+"Remember me" option which sets the `remember_user_token`
+cookie (via [`devise`](https://github.com/plataformatec/devise)).
+`remember_user_token` expires after
+`config/initializers/devise.rb` -> `config.remember_for` (defaults to 2 weeks).
+
+When the `_gitlab_session` expires or isn't available, GitLab uses the `remember_user_token`
+to get you a new `_gitlab_session` and keep you signed in through browser restarts.
+
+After your `remember_user_token` expires and your `_gitlab_session` is cleared/expired,
+you will be asked to sign in again to verify your identity (which is for security reasons).
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index e2924c66e70..d5619c7b563 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -83,6 +83,7 @@ added directly to your configured cluster. Those applications are needed for
| ----------- | :------------: | ----------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
+| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
## Enabling or disabling the Cluster integration
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 5d91aef5735..4c772c62f8d 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -93,6 +93,9 @@ from your fork to the upstream project
## Project settings
+Set the project's visibility level and the access levels to its various pages
+and perform actions like archiving, renaming or transferring a project.
+
Read through the documentation on [project settings](settings/index.md).
## Import or export a project
@@ -116,3 +119,32 @@ when a project is part of a group (under a
[group namespace](../group/index.md#namespaces)).
If you choose to leave a project you will no longer be a project
member, therefore, unable to contribute.
+
+## Redirects when changing repository paths
+
+When a repository path changes, it is essential to smoothly transition from the
+old location to the new one. GitLab provides two kinds of redirects: the web UI
+and Git push/pull redirects.
+
+Depending on the situation, different things apply.
+
+When [transferring a project](settings/index.md#transferring-an-existing-project-into-another-namespace),
+or [renaming a user](../profile/index.md#changing-your-username) or
+[changing a group path](../group/index.md#changing-a-group-s-path):
+
+- **The redirect to the new URL is permanent**, which means that the original
+ namespace can't be claimed again by any group or user.
+- Existing web URLs for the namespace and anything under it (e.g., projects) will
+ redirect to the new URLs.
+- Starting with GitLab 10.3, existing Git remote URLs for projects under the
+ namespace will redirect to the new remote URL. Every time you push/pull to a
+ repository that has changed its location, a warning message to update
+ your remote will be displayed instead of rejecting your action.
+ This means that any automation scripts, or Git clients will continue to
+ work after a rename, making any transition a lot smoother.
+ To avoid pulling from or pushing to an entirely incorrect repository, the old
+ path will be reserved.
+
+When [renaming-a-repository](settings/index.md#renaming-a-repository), the same
+things apply, except for the Git push/pull actions which will be rejected with a
+warning message to change to the new remote URL.
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 93aec56f8dc..f77569e4886 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -98,6 +98,9 @@ password as they will be needed when configuring GitLab in the next section.
- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified
the configuration options you have to enter. If you are using an older version,
[follow this documentation][jira-repo-old-docs].
+- In order to support Oracle's Access Manager, GitLab will send additional cookies
+ to enable Basic Auth. The cookie being added to each request is `OBBasicAuth` with
+ a value of `fromDialog`.
To enable JIRA integration in a project, navigate to the
[Integrations page](project_services.md#accessing-the-project-services), click
@@ -170,6 +173,7 @@ where `PROJECT-1` is the issue ID of the JIRA project.
- Only commits and merges into the project's default branch (usually **master**) will
close an issue in Jira. You can change your projects default branch under
[project settings](img/jira_project_settings.png).
+- The JIRA issue will not be transitioned if it has a resolution.
### JIRA issue closing example
@@ -219,6 +223,10 @@ JIRA issue references and update comments will not work if the GitLab issue trac
Make sure the `Transition ID` you set within the JIRA settings matches the one
your project needs to close a ticket.
+Make sure that the JIRA issue is not already marked as resolved, in other words that
+the JIRA issue resolution field is not set. (It should not be struck through in
+JIRA lists.)
+
[services-templates]: services_templates.md
[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
[jira]: https://www.atlassian.com/software/jira
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index e9738b683f9..710cf78e84f 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -1,7 +1,10 @@
---
-last_updated: 2017-09-25
+last_updated: 2017-12-28
---
+CAUTION: **Warning:**
+Kubernetes service integration has been deprecated in GitLab 10.3. If the service is active the cluster information still be editable, however we advised to disable and reconfigure the clusters using the new [Clusters](../clusters/index.md) page. If the service is inactive the fields will be uneditable. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
+
# GitLab Kubernetes / OpenShift integration
GitLab can be configured to interact with Kubernetes, or other systems using the
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index a0405161495..9496d6f2ce0 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -39,7 +39,7 @@ 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) | A containerized deployment service |
+| [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/samples/prometheus.yml b/doc/user/project/integrations/samples/prometheus.yml
index 30b59e172a1..3a4735d282f 100644
--- a/doc/user/project/integrations/samples/prometheus.yml
+++ b/doc/user/project/integrations/samples/prometheus.yml
@@ -94,7 +94,7 @@ spec:
- name: prometheus
image: prom/prometheus:latest
args:
- - '-config.file=/prometheus-data/prometheus.yml'
+ - '--config.file=/prometheus-data/prometheus.yml'
ports:
- name: prometheus
containerPort: 9090
diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md
index 085170d9f03..3cd91a185e3 100644
--- a/doc/user/project/merge_requests/fast_forward_merge.md
+++ b/doc/user/project/merge_requests/fast_forward_merge.md
@@ -9,7 +9,7 @@ When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge
commits will be created and all merges are fast-forwarded, which means that
merging is only allowed if the branch could be fast-forwarded.
-When a fast-forward merge is not possible, the user must rebase the branch manually.
+When a fast-forward merge is not possible, the user is given the option to rebase.
## Use cases
@@ -25,7 +25,7 @@ merge commits. In such cases, the fast-forward merge is the perfect candidate.
Now, when you visit the merge request page, you will be able to accept it
**only if a fast-forward merge is possible**.
-![Fast forward merge request](img/ff_merge_mr.png)
+![Fast forward merge request](img/ff_merge_rebase.png)
If the target branch is ahead of the source branch, you need to rebase the
source branch locally before you will be able to do a fast-forward merge.
diff --git a/doc/user/project/merge_requests/img/create_from_email.png b/doc/user/project/merge_requests/img/create_from_email.png
new file mode 100644
index 00000000000..71eb4bf267d
--- /dev/null
+++ b/doc/user/project/merge_requests/img/create_from_email.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png
deleted file mode 100644
index 241cc990343..00000000000
--- a/doc/user/project/merge_requests/img/ff_merge_mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/ff_merge_rebase.png b/doc/user/project/merge_requests/img/ff_merge_rebase.png
new file mode 100644
index 00000000000..f6139f189ce
--- /dev/null
+++ b/doc/user/project/merge_requests/img/ff_merge_rebase.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
index 9b8aee47411..4eee734ff8d 100644
--- a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
+++ b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index d76ea259301..7037d7f5989 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -27,7 +27,7 @@ With GitLab merge requests, you can:
- [Resolve merge conflicts from the UI](#resolve-conflicts)
- Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
-- [Create new merge requests by email](#create_by_email)
+- [Create new merge requests by email](#create-new-merge-requests-by-email)
With **[GitLab Enterprise Edition][ee]**, you can also:
@@ -138,7 +138,13 @@ You can create a new merge request by sending an email to a user-specific email
address. The address can be obtained on the merge requests page by clicking on
a **Email a new merge request to this project** button. The subject will be
used as the source branch name for the new merge request and the target branch
-will be the default branch for the project.
+will be the default branch for the project. The message body (if not empty)
+will be used as the merge request description. You need
+["Reply by email"](../../../administration/reply_by_email.md) enabled to use
+this feature. If it's not enabled to your instance, you may ask your GitLab
+administrator to do so.
+
+![Create new merge requests by email](img/create_from_email.png)
## Revert changes
diff --git a/doc/user/project/milestones/img/progress.png b/doc/user/project/milestones/img/progress.png
deleted file mode 100644
index c85aecca729..00000000000
--- a/doc/user/project/milestones/img/progress.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/milestones/img/sidebar.png b/doc/user/project/milestones/img/sidebar.png
new file mode 100644
index 00000000000..274962a936c
--- /dev/null
+++ b/doc/user/project/milestones/img/sidebar.png
Binary files differ
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 83adbd8cce2..20249926910 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -47,13 +47,15 @@ special options available when filtering by milestone:
date less than today. Note that this can return results from several
milestones in the same project.
-## Milestone progress statistics
+## Milestone sidebar
-Milestone statistics can be viewed in the milestone sidebar. The milestone percentage statistic
-is calculated as; closed and merged merge requests plus all closed issues divided by
+The milestone sidebar shows percentage complete, start date and due date,
+issues, total issue weight, total issue time spent, and merge requests.
+
+The percentage complete is calcualted as: Closed and merged merge requests plus all closed issues divided by
total merge requests and issues.
-![Milestone statistics](img/progress.png)
+![Milestone sidebar](img/sidebar.png)
## Quick actions
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index daa5463d680..43451844f2d 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -68,7 +68,7 @@ in the pipelines settings page.
Access to pipelines and job details (including output of logs and artifacts)
is checked against your current user access level and the **Public pipelines**
-project setting.
+project setting under your project's **Settings > CI/CD > General pipelines settings**.
If **Public pipelines** is enabled (default):
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index e81e935e37d..442fc978284 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -39,3 +39,5 @@ do.
| `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
| `/move path/to/project` | Moves issue to another project |
+| `/tableflip` | Append the comment with `(╯°□°)╯︵ â”»â”â”»` |
+| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | \ No newline at end of file
diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md
index 6b9976d133c..d41be0989d2 100644
--- a/doc/user/project/repository/gpg_signed_commits/index.md
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -1,6 +1,11 @@
# Signing commits with GPG
-> [Introduced][ce-9546] in GitLab 9.5.
+NOTE: **Note:**
+The term GPG is used for all OpenPGP/PGP/GPG related material and
+implementations.
+
+> - [Introduced][ce-9546] in GitLab 9.5.
+> - Subkeys support was added in GitLab 10.1.
GitLab can show whether a commit is verified or not when signed with a GPG key.
All you need to do is upload the public GPG key in your profile settings.
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 1b8a84c9599..b8f865679a2 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -30,7 +30,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.3 to current | 0.2.1 |
+| 10.4 to current | 0.2.2 |
+| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
| 9.2.0 | 0.1.7 |
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index a234a647b77..f01fa5b1860 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -1,11 +1,20 @@
# Project settings
+NOTE: **Note:**
+Only project Masters and Admin users have the [permissions] to access a project
+settings.
+
You can adjust your [project](../index.md) settings by navigating
to your project's homepage and clicking **Settings**.
## General settings
-Adjust your project's path and name, description, avatar, [default branch](../repository/branches/index.md#default-branch), and tags:
+Under a project's general settings you can find everything concerning the
+functionality of a project.
+
+### General project settings
+
+Adjust your project's name, description, avatar, [default branch](../repository/branches/index.md#default-branch), and tags:
![general project settings](img/general_settings.png)
@@ -45,8 +54,64 @@ Here you can run housekeeping, archive, rename, transfer, or remove a project.
#### Archiving a project
->**Note:** Only Project Owners and Admin users have the permission to archive a project
+NOTE: **Note:**
+Only project Owners and Admin users have the [permissions] to archive a project.
+
+An archived project will be hidden by default in the project listings.
+
+1. Navigate to your project's **Settings > General > Advanced settings**.
+1. Under "Archive project", hit the **Archive project** button.
+1. Confirm the action when asked to.
+
+An archived project can be fully restored and will therefore retain its
+repository and all associated resources whilst in an archived state.
+
+#### Renaming a repository
+
+NOTE: **Note:**
+Only project Masters 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).
+
+A project's repository name defines its URL (the one you use to access the
+project via a browser) and its place on the file disk where GitLab is installed.
+
+To rename a repository:
+
+1. Navigate to your project's **Settings > General > Advanced settings**.
+1. Under "Rename repository", change the "Path" to your liking.
+1. Hit **Rename project**.
+
+Remember that this can have unintended side effects since everyone with the
+old URL will not be able to push or pull. Read more about what happens with the
+[redirects when renaming repositories](../index.md#redirects-when-changing-repository-paths).
+
+#### Transferring an existing project into another namespace
+
+NOTE: **Note:**
+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 are an **Owner** of the project.
+
+Similarly, if you are an owner of a group, you can transfer any of its projects
+under your own user.
+
+To transfer a project:
+
+1. Navigate to your project's **Settings > General > Advanced settings**.
+1. Under "Transfer project", choose the namespace you want to transfer the
+ project to.
+1. Confirm the transfer by typing the project's path as instructed.
+
+Once done, you will be taken to the new project's namespace. At this point,
+read what happens with the
+[redirects from the old project to the new one](../index.md#redirects-when-changing-repository-paths).
-It's possible to mark a project as archived via the Project Settings. An archived project will be hidden by default in the project listings.
+NOTE: **Note:**
+GitLab administrators can use the admin interface to move any project to any
+namespace if needed.
-An archived project can be fully restored and will therefore retain it's repository and all associated resources whilst in an archived state.
+[permissions]: ../../permissions.md##project-members-permissions
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 6adde447975..f7b87dee8e1 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -163,3 +163,11 @@ For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Wi
More details about various methods of storing the user credentials can be found
on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
+
+### LFS objects are missing on push
+
+GitLab checks files to detect LFS pointers on push. If LFS pointers are detected, GitLab tries to verify that those files already exist in LFS on GitLab.
+
+Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
+
+If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
deleted file mode 100644
index 830810615e0..00000000000
--- a/features/explore/groups.feature
+++ /dev/null
@@ -1,105 +0,0 @@
-@public
-Feature: Explore Groups
- Background:
- Given group "TestGroup" has private project "Enterprise"
-
- @javascript
- Scenario: I should see group with private and internal projects as user
- Given group "TestGroup" has internal project "Internal"
- When I sign in as a user
- And I visit group "TestGroup" page
- Then I should see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group issues for internal project as user
- Given group "TestGroup" has internal project "Internal"
- When I sign in as a user
- And I visit group "TestGroup" issues page
- Then I should see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group merge requests for internal project as user
- Given group "TestGroup" has internal project "Internal"
- When I sign in as a user
- And I visit group "TestGroup" merge requests page
- Then I should see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group with private, internal and public projects as visitor
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- When I visit group "TestGroup" page
- Then I should see project "Community" items
- And I should not see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group issues for public project as visitor
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- When I visit group "TestGroup" issues page
- Then I should see project "Community" items
- And I should not see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group merge requests for public project as visitor
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- When I visit group "TestGroup" merge requests page
- Then I should see project "Community" items
- And I should not see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group with private, internal and public projects as user
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- When I sign in as a user
- And I visit group "TestGroup" page
- Then I should see project "Community" items
- And I should see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group issues for internal and public projects as user
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- When I sign in as a user
- And I visit group "TestGroup" issues page
- Then I should see project "Community" items
- And I should see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group merge requests for internal and public projects as user
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- When I sign in as a user
- And I visit group "TestGroup" merge requests page
- Then I should see project "Community" items
- And I should see project "Internal" items
- And I should not see project "Enterprise" items
-
- @javascript
- Scenario: I should see group with public project in public groups area
- Given group "TestGroup" has public project "Community"
- When I visit the public groups area
- Then I should see group "TestGroup"
-
- @javascript
- Scenario: I should see group with public project in public groups area as user
- Given group "TestGroup" has public project "Community"
- When I sign in as a user
- And I visit the public groups area
- Then I should see group "TestGroup"
-
- @javascript
- Scenario: I should see group with internal project in public groups area as user
- Given group "TestGroup" has internal project "Internal"
- When I sign in as a user
- And I visit the public groups area
- Then I should see group "TestGroup"
diff --git a/features/invites.feature b/features/invites.feature
deleted file mode 100644
index dc8eefaeaed..00000000000
--- a/features/invites.feature
+++ /dev/null
@@ -1,45 +0,0 @@
-Feature: Invites
- Background:
- Given "John Doe" is owner of group "Owned"
- And "John Doe" has invited "user@example.com" to group "Owned"
-
- Scenario: Viewing invitation when signed out
- When I visit the invitation page
- Then I should be redirected to the sign in page
- And I should see a notice telling me to sign in
-
- Scenario: Signing in to view invitation
- When I visit the invitation page
- And I sign in as "Mary Jane"
- Then I should be redirected to the invitation page
-
- Scenario: Viewing invitation when signed in
- Given I sign in as "Mary Jane"
- And I visit the invitation page
- Then I should see the invitation details
- And I should see an "Accept invitation" button
- And I should see a "Decline" button
-
- Scenario: Viewing invitation as an existing member
- Given I sign in as "John Doe"
- And I visit the invitation page
- Then I should see a message telling me I'm already a member
-
- Scenario: Accepting the invitation
- Given I sign in as "Mary Jane"
- And I visit the invitation page
- And I click the "Accept invitation" button
- Then I should be redirected to the group page
- And I should see a notice telling me I have access
-
- Scenario: Declining the application when signed in
- Given I sign in as "Mary Jane"
- And I visit the invitation page
- And I click the "Decline" button
- Then I should be redirected to the dashboard
- And I should see a notice telling me I have declined
-
- Scenario: Declining the application when signed out
- When I visit the invitation's decline page
- Then I should be redirected to the sign in page
- And I should see a notice telling me I have declined
diff --git a/features/project/ff_merge_requests.feature b/features/project/ff_merge_requests.feature
index 995e52f9332..39035d551d1 100644
--- a/features/project/ff_merge_requests.feature
+++ b/features/project/ff_merge_requests.feature
@@ -22,3 +22,20 @@ Feature: Project Ff Merge Requests
Then I should see ff-only merge button
When I accept this merge request
Then I should see merged request
+
+ @javascript
+ Scenario: I do rebase before ff-only merge
+ Given ff merge enabled
+ And rebase before merge enabled
+ When I visit merge request page "Bug NS-05"
+ Then I should see rebase button
+ When I press rebase button
+ Then I should see rebase in progress message
+
+ @javascript
+ Scenario: I do rebase before regular merge
+ Given rebase before merge enabled
+ When I visit merge request page "Bug NS-05"
+ Then I should see rebase button
+ When I press rebase button
+ Then I should see rebase in progress message
diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb
deleted file mode 100644
index 409bf0cb416..00000000000
--- a/features/steps/explore/groups.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedGroup
- include SharedProject
-
- step 'group "TestGroup" has private project "Enterprise"' do
- group_has_project("TestGroup", "Enterprise", Gitlab::VisibilityLevel::PRIVATE)
- end
-
- step 'group "TestGroup" has internal project "Internal"' do
- group_has_project("TestGroup", "Internal", Gitlab::VisibilityLevel::INTERNAL)
- end
-
- step 'group "TestGroup" has public project "Community"' do
- group_has_project("TestGroup", "Community", Gitlab::VisibilityLevel::PUBLIC)
- end
-
- step '"John Doe" is owner of group "TestGroup"' do
- group = Group.find_by(name: "TestGroup") || create(:group, name: "TestGroup")
- user = create(:user, name: "John Doe")
- group.add_owner(user)
- end
-
- step 'I visit group "TestGroup" page' do
- visit group_path(Group.find_by(name: "TestGroup"))
- end
-
- step 'I visit group "TestGroup" issues page' do
- visit issues_group_path(Group.find_by(name: "TestGroup"))
- end
-
- step 'I visit group "TestGroup" merge requests page' do
- visit merge_requests_group_path(Group.find_by(name: "TestGroup"))
- end
-
- step 'I visit group "TestGroup" members page' do
- visit group_group_members_path(Group.find_by(name: "TestGroup"))
- end
-
- step 'I should not see project "Enterprise" items' do
- expect(page).not_to have_content "Enterprise"
- end
-
- step 'I should see project "Internal" items' do
- expect(page).to have_content "Internal"
- end
-
- step 'I should not see project "Internal" items' do
- expect(page).not_to have_content "Internal"
- end
-
- step 'I should see project "Community" items' do
- expect(page).to have_content "Community"
- end
-
- step 'I change filter to Everyone\'s' do
- click_link "Everyone's"
- end
-
- step 'I should see group member "John Doe"' do
- expect(page).to have_content "John Doe"
- end
-
- protected
-
- def group_has_project(groupname, projectname, visibility_level)
- group = Group.find_by(name: groupname) || create(:group, name: groupname)
- project = create(:project,
- namespace: group,
- name: projectname,
- path: "#{groupname}-#{projectname}",
- visibility_level: visibility_level
- )
- create(:issue,
- title: "#{projectname} feature",
- project: project
- )
- create(:merge_request,
- title: "#{projectname} feature implemented",
- source_project: project,
- target_project: project
- )
- create(:closed_issue_event,
- project: project
- )
- end
-end
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index a2d9a0332e0..753694a5392 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -138,7 +138,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
private
def assigned_to_me(key)
- project.send(key).where(assignee_id: current_user.id)
+ project.send(key).assigned_to(current_user)
end
def project
diff --git a/features/steps/invites.rb b/features/steps/invites.rb
deleted file mode 100644
index dac972172aa..00000000000
--- a/features/steps/invites.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-class Spinach::Features::Invites < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedUser
- include SharedGroup
-
- step '"John Doe" has invited "user@example.com" to group "Owned"' do
- user = User.find_by(name: "John Doe")
- group = Group.find_by(name: "Owned")
- group.add_developer("user@example.com", user)
- end
-
- step 'I visit the invitation page' do
- group = Group.find_by(name: "Owned")
- invite = group.group_members.invite.last
- invite.generate_invite_token!
- @raw_invite_token = invite.raw_invite_token
- visit invite_path(@raw_invite_token)
- end
-
- step 'I should be redirected to the sign in page' do
- expect(current_path).to eq(new_user_session_path)
- end
-
- step 'I should see a notice telling me to sign in' do
- expect(page).to have_content "To accept this invitation, sign in"
- end
-
- step 'I should be redirected to the invitation page' do
- expect(current_path).to eq(invite_path(@raw_invite_token))
- end
-
- step 'I should see the invitation details' do
- expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.")
- end
-
- step "I should see a message telling me I'm already a member" do
- expect(page).to have_content("However, you are already a member of this group.")
- end
-
- step 'I should see an "Accept invitation" button' do
- expect(page).to have_link("Accept invitation")
- end
-
- step 'I should see a "Decline" button' do
- expect(page).to have_link("Decline")
- end
-
- step 'I click the "Accept invitation" button' do
- page.click_link "Accept invitation"
- end
-
- step 'I should be redirected to the group page' do
- group = Group.find_by(name: "Owned")
- expect(current_path).to eq(group_path(group))
- end
-
- step 'I should see a notice telling me I have access' do
- expect(page).to have_content("You have been granted Developer access to group Owned.")
- end
-
- step 'I click the "Decline" button' do
- page.click_link "Decline"
- end
-
- step 'I should be redirected to the dashboard' do
- expect(current_path).to eq(dashboard_projects_path)
- end
-
- step 'I should see a notice telling me I have declined' do
- expect(page).to have_content("You have declined the invitation to join group Owned.")
- end
-
- step "I visit the invitation's decline page" do
- group = Group.find_by(name: "Owned")
- invite = group.group_members.invite.last
- invite.generate_invite_token!
- @raw_invite_token = invite.raw_invite_token
- visit decline_invite_path(@raw_invite_token)
- end
-end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 4b88cb5e27f..d3b88ae8d2a 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -165,7 +165,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
@project = create(:project, :repository, namespace: @group)
@event = create(:closed_issue_event, project: @project)
- @project.team << [current_user, :master]
+ @project.add_master(current_user)
end
step 'I should see groups I belong to' do
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index b4403becb0d..9db31522c5c 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -48,11 +48,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
step 'other projects have deploy keys' do
@second_project = create(:project, namespace: create(:group))
- @second_project.team << [current_user, :master]
+ @second_project.add_master(current_user)
create(:deploy_keys_project, project: @second_project)
@third_project = create(:project, namespace: create(:group))
- @third_project.team << [current_user, :master]
+ @third_project.add_master(current_user)
create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
end
diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb
index d68fe71e16e..27efcfd65b6 100644
--- a/features/steps/project/ff_merge_requests.rb
+++ b/features/steps/project/ff_merge_requests.rb
@@ -17,6 +17,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
author: project.users.first)
end
+ step 'merge request is mergeable' do
+ expect(page).to have_button 'Merge'
+ end
+
step 'I should see ff-only merge button' do
expect(page).to have_content "Fast-forward merge without a merge commit"
expect(page).to have_button 'Merge'
@@ -45,6 +49,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
project.save!
end
+ step 'I should see rebase button' do
+ expect(page).to have_button "Rebase"
+ end
+
step 'merge request "Bug NS-05" is rebased' do
merge_request.source_branch = 'flatten-dir'
merge_request.target_branch = 'improve/awesome'
@@ -59,6 +67,20 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
merge_request.save!
end
+ step 'rebase before merge enabled' do
+ project = merge_request.target_project
+ project.merge_requests_rebase_enabled = true
+ project.save!
+ end
+
+ step 'I press rebase button' do
+ click_button "Rebase"
+ end
+
+ step "I should see rebase in progress message" do
+ expect(page).to have_content("Rebase in progress")
+ end
+
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 60707f26aee..0350e1c2aef 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -10,7 +10,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I am a member of project "Shop"' do
@project = create(:project, :repository, name: "Shop")
- @project.team << [@user, :reporter]
+ @project.add_reporter(@user)
end
step 'I should see the forked project page' do
@@ -71,7 +71,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike')
- @project.team << [user, :reporter]
+ @project.add_reporter(user)
@forked_project = Projects::ForkService.new(@project, user).execute
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 6781a906a94..fd51ee1a316 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -10,7 +10,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
step 'I am a member of project "Shop"' do
@project = ::Project.find_by(name: "Shop")
@project ||= create(:project, :repository, name: "Shop")
- @project.team << [@user, :reporter]
+ @project.add_reporter(@user)
end
step 'I have a project forked off of "Shop" called "Forked Shop"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 6e04f09f322..afaad4b255e 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -8,7 +8,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step "I don't have write access" do
@project = create(:project, :repository, name: "Other Project", path: "other-project")
- @project.team << [@user, :reporter]
+ @project.add_reporter(@user)
visit project_tree_path(@project, root_ref)
end
@@ -278,17 +278,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content('Your changes could not be committed')
end
- step 'I create bare repo' do
- click_link 'Create empty bare repository'
- end
-
- step 'I click on "README" link' do
- click_link 'README'
-
- # Remove pre-receive hook so we can push without auth
- FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive'))
- end
-
step "I switch ref to 'test'" do
first('.js-project-refs-dropdown').click
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index f6445b57ec0..fc4ef26f6b6 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -10,7 +10,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I own project "Delta"' do
@project = ::Project.find_by(name: "Delta")
@project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
- @project.team << [@user, :master]
+ @project.add_master(@user)
end
step 'I should see files from repository in markdown' do
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index c267195f0e8..a3e4459f169 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do
@pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
- @build = create(:ci_build, :coverage, pipeline: @pipeline)
+ @build = create(:ci_build, :running, :coverage, pipeline: @pipeline)
end
step 'recent build is successful' do
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index 03bc7e798e0..0a0588346b1 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -41,7 +41,7 @@ module SharedGroup
group.add_user(user, role)
project ||= create(:project, :repository, namespace: group)
create(:closed_issue_event, project: project)
- project.team << [user, :master]
+ project.add_master(user)
end
def owned_group
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index 714985f2051..f90247c3fe8 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -2,7 +2,7 @@ module SharedIssuable
include Spinach::DSL
def edit_issuable
- find('.issuable-edit', visible: true).click
+ find('.js-issuable-edit', visible: true).click
end
step 'project "Community" has "Community issue" open issue' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 5e4edaf99a6..affbccccdf9 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -4,13 +4,13 @@ module SharedProject
# Create a project without caring about what it's called
step "I own a project" do
@project = create(:project, :repository, namespace: @user.namespace)
- @project.team << [@user, :master]
+ @project.add_master(@user)
end
step "I own a project in some group namespace" do
@group = create(:group, name: 'some group')
@project = create(:project, namespace: @group)
- @project.team << [@user, :master]
+ @project.add_master(@user)
end
step "project exists in some group namespace" do
@@ -22,7 +22,7 @@ module SharedProject
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
@project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace)
- @project.team << [@user, :master]
+ @project.add_master(@user)
end
step 'I disable snippets in project' do
@@ -40,7 +40,7 @@ module SharedProject
step 'I add a user to project "Shop"' do
@project = Project.find_by(name: "Shop")
other_user = create(:user, name: 'Alpha')
- @project.team << [other_user, :master]
+ @project.add_master(other_user)
end
# Create another specific project called "Forum"
@@ -49,14 +49,13 @@ module SharedProject
@project ||= create(:project, :repository, name: "Forum", namespace: @user.namespace, path: 'forum_project')
@project.build_project_feature
@project.project_feature.save
- @project.team << [@user, :master]
+ @project.add_master(@user)
end
# Create an empty project without caring about the name
step 'I own an empty project' do
- @project = create(:project,
- name: 'Empty Project', namespace: @user.namespace)
- @project.team << [@user, :master]
+ @project = create(:project, name: 'Empty Project', namespace: @user.namespace)
+ @project.add_master(@user)
end
step 'I visit my empty project page' do
@@ -101,11 +100,11 @@ module SharedProject
# ----------------------------------------
step 'I am member of a project with a guest role' do
- @project.team << [@user, Gitlab::Access::GUEST]
+ @project.add_guest(@user)
end
step 'I am member of a project with a reporter role' do
- @project.team << [@user, Gitlab::Access::REPORTER]
+ @project.add_reporter(@user)
end
# ----------------------------------------
@@ -245,6 +244,6 @@ module SharedProject
user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
project = Project.find_by(name: project_name)
project ||= create(:project, visibility, name: project_name, namespace: user.namespace)
- project.team << [user, :master]
+ project.add_master(user)
end
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 5a77b859113..4e2b3c67af5 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -29,6 +29,9 @@ Capybara.register_driver :chrome do |app|
options.add_argument("disable-gpu")
end
+ # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
+ options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
+
Capybara::Selenium::Driver.new(
app,
browser: :chrome,
diff --git a/features/support/env.rb b/features/support/env.rb
index 5962745d501..91a92314959 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -27,7 +27,7 @@ Spinach.hooks.before_run do
# web editor and merge
TestEnv.disable_pre_receive
- include FactoryGirl::Syntax::Methods
+ include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
end
@@ -42,11 +42,11 @@ module StdoutReporterWithScenarioLocation
# Override the standard reporter to show filename and line number next to each
# scenario for easy, focused re-runs
def before_scenario_run(scenario, step_definitions = nil)
- @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any?
+ @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables
name = scenario.name
# This number has no significance, it's just to line things up
- max_length = @max_step_name_length + 19
+ max_length = @max_step_name_length + 19 # rubocop:disable Gitlab/ModuleWithInstanceVariables
out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \
" # #{scenario.feature.filename}:#{scenario.line}"
end
diff --git a/lib/after_commit_queue.rb b/lib/after_commit_queue.rb
index db63c5038ae..a4d8507960e 100644
--- a/lib/after_commit_queue.rb
+++ b/lib/after_commit_queue.rb
@@ -14,7 +14,15 @@ module AfterCommitQueue
def run_after_commit_or_now(&block)
if AfterCommitQueue.inside_transaction?
- run_after_commit(&block)
+ if ActiveRecord::Base.connection.current_transaction.records.include?(self)
+ run_after_commit(&block)
+ else
+ # If the current transaction does not include this record, we can run
+ # the block now, even if it queues a Sidekiq job.
+ Sidekiq::Worker.skipping_transaction_check do
+ instance_eval(&block)
+ end
+ end
else
instance_eval(&block)
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 8094597d238..e0d14281c96 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -119,6 +119,7 @@ module API
mount ::API::Features
mount ::API::Files
mount ::API::Groups
+ mount ::API::GroupMilestones
mount ::API::Internal
mount ::API::Issues
mount ::API::Jobs
@@ -129,8 +130,6 @@ module API
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
- mount ::API::ProjectMilestones
- mount ::API::GroupMilestones
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
@@ -139,6 +138,7 @@ module API
mount ::API::PipelineSchedules
mount ::API::ProjectHooks
mount ::API::Projects
+ mount ::API::ProjectMilestones
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::Repositories
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 366b0dc9a6f..6c706b2b4e1 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -1,45 +1,46 @@
module API
class Boards < Grape::API
+ include BoardsResponses
include PaginationParams
before { authenticate! }
+ helpers do
+ def board_parent
+ user_project
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
- desc 'Get all project boards' do
- detail 'This feature was introduced in 8.13'
- success Entities::Board
- end
- params do
- use :pagination
- end
- get ':id/boards' do
- authorize!(:read_board, user_project)
- present paginate(user_project.boards), with: Entities::Board
+ segment ':id/boards' do
+ desc 'Get all project boards' do
+ detail 'This feature was introduced in 8.13'
+ success Entities::Board
+ end
+ params do
+ use :pagination
+ end
+ get '/' do
+ authorize!(:read_board, user_project)
+ present paginate(board_parent.boards), with: Entities::Board
+ end
+
+ desc 'Find a project board' do
+ detail 'This feature was introduced in 10.4'
+ success Entities::Board
+ end
+ get '/:board_id' do
+ present board, with: Entities::Board
+ end
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 Entities::List
@@ -72,22 +73,13 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label'
end
post '/lists' do
- unless available_labels.exists?(params[:label_id])
+ unless available_labels_for(user_project).exists?(params[:label_id])
render_api_error!({ error: 'Label not found!' }, 400)
end
authorize!(:admin_list, user_project)
- service = ::Boards::Lists::CreateService.new(user_project, current_user,
- { label_id: params[:label_id] })
-
- list = service.execute(project_board)
-
- if list.valid?
- present list, with: Entities::List
- else
- render_validation_error!(list)
- end
+ create_list
end
desc 'Moves a board list to a new position' do
@@ -99,18 +91,11 @@ module API
requires :position, type: Integer, desc: 'The position of the list'
end
put '/lists/:list_id' do
- list = project_board.lists.movable.find(params[:list_id])
+ list = board_lists.find(params[:list_id])
authorize!(:admin_list, user_project)
- service = ::Boards::Lists::MoveService.new(user_project, current_user,
- { position: params[:position] })
-
- if service.execute(list)
- present list, with: Entities::List
- else
- render_api_error!({ error: "List could not be moved!" }, 400)
- end
+ move_list(list)
end
desc 'Delete a board list' do
@@ -124,12 +109,7 @@ module API
authorize!(:admin_list, user_project)
list = board_lists.find(params[:list_id])
- destroy_conditionally!(list) do |list|
- service = ::Boards::Lists::DestroyService.new(user_project, current_user)
- unless service.execute(list)
- render_api_error!({ error: 'List could not be deleted!' }, 400)
- end
- end
+ destroy_list(list)
end
end
end
diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb
new file mode 100644
index 00000000000..ead0943a74d
--- /dev/null
+++ b/lib/api/boards_responses.rb
@@ -0,0 +1,50 @@
+module API
+ module BoardsResponses
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def board
+ board_parent.boards.find(params[:board_id])
+ end
+
+ def board_lists
+ board.lists.destroyable
+ end
+
+ def create_list
+ create_list_service =
+ ::Boards::Lists::CreateService.new(board_parent, current_user, { label_id: params[:label_id] })
+
+ list = create_list_service.execute(board)
+
+ if list.valid?
+ present list, with: Entities::List
+ else
+ render_validation_error!(list)
+ end
+ end
+
+ def move_list(list)
+ move_list_service =
+ ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i })
+
+ if move_list_service.execute(list)
+ present list, with: Entities::List
+ else
+ render_api_error!({ error: "List could not be moved!" }, 400)
+ end
+ end
+
+ def destroy_list(list)
+ destroy_conditionally!(list) do |list|
+ service = ::Boards::Lists::DestroyService.new(board_parent, current_user)
+ unless service.execute(list)
+ render_api_error!({ error: 'List could not be deleted!' }, 400)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index d96e7f2770f..bd0c54a1b04 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -248,8 +248,21 @@ module API
end
class GroupDetail < Group
- expose :projects, using: Entities::Project
- expose :shared_projects, using: Entities::Project
+ expose :projects, using: Entities::Project do |group, options|
+ GroupProjectsFinder.new(
+ group: group,
+ current_user: options[:current_user],
+ options: { only_owned: true }
+ ).execute
+ end
+
+ expose :shared_projects, using: Entities::Project do |group, options|
+ GroupProjectsFinder.new(
+ group: group,
+ current_user: options[:current_user],
+ options: { only_shared: true }
+ ).execute
+ end
end
class Commit < Grape::Entity
@@ -695,8 +708,9 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :pipeline_events
+ expose :push_events, :issues_events, :confidential_issues_events
+ expose :merge_requests_events, :tag_push_events, :note_events
+ expose :pipeline_events, :wiki_page_events
expose :job_events
# Expose serialized properties
expose :properties do |service, options|
@@ -777,6 +791,8 @@ module API
class Board < Grape::Entity
expose :id
+ expose :project, using: Entities::BasicProjectDetails
+
expose :lists, using: Entities::List do |board|
board.lists.destroyable
end
@@ -848,6 +864,8 @@ module API
expose :active
expose :is_shared
expose :name
+ expose :online?, as: :online
+ expose :status
end
class RunnerDetails < Runner
@@ -1119,6 +1137,7 @@ module API
class PagesDomainBasic < Grape::Entity
expose :domain
expose :url
+ expose :project_id
expose :certificate,
as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? },
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 72ada181dd2..cc81e4d3595 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -32,6 +32,11 @@ module API
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # We can't rewrite this with StrongMemoize because `sudo!` would
+ # actually write to `@current_user`, and `sudo?` would immediately
+ # call `current_user` again which reads from `@current_user`.
+ # We should rewrite this in a way that using StrongMemoize is possible
def current_user
return @current_user if defined?(@current_user)
@@ -45,6 +50,7 @@ module API
@current_user
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def sudo?
initial_current_user != current_user
@@ -63,13 +69,20 @@ module API
end
def wiki_page
- page = user_project.wiki.find_page(params[:slug])
+ page = ProjectWiki.new(user_project, current_user).find_page(params[:slug])
page || not_found!('Wiki Page')
end
- def available_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
+ def available_labels_for(label_parent)
+ search_params =
+ if label_parent.is_a?(Project)
+ { project_id: label_parent.id }
+ else
+ { group_id: label_parent.id, only_group_labels: true }
+ end
+
+ LabelsFinder.new(current_user, search_params).execute
end
def find_user(id)
@@ -135,7 +148,9 @@ module API
end
def find_project_label(id)
- label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
+ labels = available_labels_for(user_project)
+ label = labels.find_by_id(id) || labels.find_by_title(id)
+
label || not_found!('Label')
end
@@ -415,6 +430,7 @@ module API
private
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
@@ -424,6 +440,7 @@ module API
unauthorized!
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def sudo!
return unless sudo_identifier
@@ -443,7 +460,7 @@ module API
sudoed_user = find_user(sudo_identifier)
not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
- @current_user = sudoed_user
+ @current_user = sudoed_user # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def sudo_identifier
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index d6dea4c30e3..eff1c5b70ea 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -6,18 +6,16 @@ module API
'git-upload-pack' => [:ssh_upload_pack, Gitlab::GitalyClient::MigrationStatus::OPT_OUT]
}.freeze
+ attr_reader :redirected_path
+
def wiki?
- set_project unless defined?(@wiki)
- @wiki
+ set_project unless defined?(@wiki) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @wiki # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def project
- set_project unless defined?(@project)
- @project
- end
-
- def redirected_path
- @redirected_path
+ set_project unless defined?(@project) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @project # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def ssh_authentication_abilities
@@ -69,6 +67,7 @@ module API
private
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_project
if params[:gl_repository]
@project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository])
@@ -77,6 +76,7 @@ module API
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse(params[:project])
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 451121a4cea..79b302aae70 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -4,6 +4,7 @@ module API
before { authenticate_by_gitlab_shell_token! }
helpers ::API::Helpers::InternalHelpers
+ helpers ::Gitlab::Identifier
namespace 'internal' do
# Check if git command is allowed to project
@@ -176,17 +177,28 @@ module API
post '/post_receive' do
status 200
-
PostReceive.perform_async(params[:gl_repository], params[:identifier],
params[:changes])
broadcast_message = BroadcastMessage.current&.last&.message
reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
- {
+ output = {
merge_request_urls: merge_request_urls,
broadcast_message: broadcast_message,
reference_counter_decreased: reference_counter_decreased
}
+
+ project = Gitlab::GlRepository.parse(params[:gl_repository]).first
+ user = identify(params[:identifier])
+
+ # A user is not guaranteed to be returned; an orphaned write deploy
+ # key could be used
+ if user
+ redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)
+ output[:redirected_message] = redirect_message if redirect_message
+ end
+
+ output
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index e60e00d7956..7aa10631d53 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -8,7 +8,7 @@ module API
helpers do
def find_issues(args = {})
- args = params.merge(args)
+ args = declared_params.merge(args)
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
@@ -161,6 +161,8 @@ module API
use :issue_params
end
post ':id/issues' do
+ authorize! :create_issue, user_project
+
# Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at)
@@ -275,6 +277,19 @@ module API
present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end
+ desc 'List participants for an issue' do
+ success Entities::UserBasic
+ end
+ params do
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ end
+ get ':id/issues/:issue_iid/participants' do
+ issue = find_project_issue(params[:issue_iid])
+ participants = ::Kaminari.paginate_array(issue.participants)
+
+ present paginate(participants), with: Entities::UserBasic, current_user: current_user, project: user_project
+ end
+
desc 'Get the user agent details for an issue' do
success Entities::UserAgentDetail
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index e41a1720ac1..81eaf56e48e 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -15,7 +15,7 @@ module API
use :pagination
end
get ':id/labels' do
- present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project
+ present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project
end
desc 'Create a new label' do
@@ -30,7 +30,7 @@ module API
post ':id/labels' do
authorize! :admin_label, user_project
- label = available_labels.find_by(title: params[:name])
+ label = available_labels_for(user_project).find_by(title: params[:name])
conflict!('Label already exists') if label
priority = params.delete(:priority)
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 22e4bdead41..5446f6b54b1 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -59,7 +59,9 @@ module API
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
- if member.persisted? && member.valid?
+ if !member
+ not_allowed! # This currently can only be reached in EE
+ elsif member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
render_validation_error!(member)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index d34886fca2e..8f665b39fa8 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -8,7 +8,7 @@ module API
helpers do
def find_merge_requests(args = {})
- args = params.merge(args)
+ args = declared_params.merge(args)
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
@@ -41,6 +41,7 @@ module API
optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
+ optional :search, type: String, desc: 'Search merge requests for text present in the title or description'
use :pagination
end
end
@@ -184,6 +185,16 @@ module API
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
+ desc 'Get the participants of a merge request' do
+ success Entities::UserBasic
+ end
+ get ':id/merge_requests/:merge_request_iid/participants' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
+ participants = ::Kaminari.paginate_array(merge_request.participants)
+
+ present paginate(participants), with: Entities::UserBasic
+ end
+
desc 'Get the commits of a merge request' do
success Entities::Commit
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 7887b886c03..4f36bbd760f 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -110,10 +110,12 @@ module API
end
params do
use :pagination
+ optional :order_by, type: String, values: %w[email name commits], default: nil, desc: 'Return contributors ordered by `name` or `email` or `commits`'
+ optional :sort, type: String, values: %w[asc desc], default: nil, desc: 'Sort by asc (ascending) or desc (descending)'
end
get ':id/repository/contributors' do
begin
- contributors = ::Kaminari.paginate_array(user_project.repository.contributors)
+ contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort]))
present paginate(contributors), with: Entities::Contributor
rescue
not_found!
diff --git a/lib/api/services.rb b/lib/api/services.rb
index bbcc851d07a..a7f44e2869c 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,5 +1,143 @@
module API
class Services < Grape::API
+ chat_notification_settings = [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The chat webhook'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The chat username'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The default chat channel'
+ }
+ ]
+
+ chat_notification_flags = [
+ {
+ required: false,
+ name: :notify_only_broken_pipelines,
+ type: Boolean,
+ desc: 'Send notifications for broken pipelines'
+ },
+ {
+ required: false,
+ name: :notify_only_default_branch,
+ type: Boolean,
+ desc: 'Send notifications only for the default branch'
+ }
+ ]
+
+ chat_notification_channels = [
+ {
+ required: false,
+ name: :push_channel,
+ type: String,
+ desc: 'The name of the channel to receive push_events notifications'
+ },
+ {
+ required: false,
+ name: :issue_channel,
+ type: String,
+ desc: 'The name of the channel to receive issues_events notifications'
+ },
+ {
+ required: false,
+ name: :confidential_issue_channel,
+ type: String,
+ desc: 'The name of the channel to receive confidential_issues_events notifications'
+ },
+ {
+ required: false,
+ name: :merge_request_channel,
+ type: String,
+ desc: 'The name of the channel to receive merge_requests_events notifications'
+ },
+ {
+ required: false,
+ name: :note_channel,
+ type: String,
+ desc: 'The name of the channel to receive note_events notifications'
+ },
+ {
+ required: false,
+ name: :tag_push_channel,
+ type: String,
+ desc: 'The name of the channel to receive tag_push_events notifications'
+ },
+ {
+ required: false,
+ name: :pipeline_channel,
+ type: String,
+ desc: 'The name of the channel to receive pipeline_events notifications'
+ },
+ {
+ required: false,
+ name: :wiki_page_channel,
+ type: String,
+ desc: 'The name of the channel to receive wiki_page_events notifications'
+ }
+ ]
+
+ chat_notification_events = [
+ {
+ required: false,
+ name: :push_events,
+ type: Boolean,
+ desc: 'Enable notifications for push_events'
+ },
+ {
+ required: false,
+ name: :issues_events,
+ type: Boolean,
+ desc: 'Enable notifications for issues_events'
+ },
+ {
+ required: false,
+ name: :confidential_issues_events,
+ type: Boolean,
+ desc: 'Enable notifications for confidential_issues_events'
+ },
+ {
+ required: false,
+ name: :merge_requests_events,
+ type: Boolean,
+ desc: 'Enable notifications for merge_requests_events'
+ },
+ {
+ required: false,
+ name: :note_events,
+ type: Boolean,
+ desc: 'Enable notifications for note_events'
+ },
+ {
+ required: false,
+ name: :tag_push_events,
+ type: Boolean,
+ desc: 'Enable notifications for tag_push_events'
+ },
+ {
+ required: false,
+ name: :pipeline_events,
+ type: Boolean,
+ desc: 'Enable notifications for pipeline_events'
+ },
+ {
+ required: false,
+ name: :wiki_page_events,
+ type: Boolean,
+ desc: 'Enable notifications for wiki_page_events'
+ }
+ ]
+
services = {
'asana' => [
{
@@ -489,25 +627,11 @@ module API
}
],
'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'
- }
- ],
+ chat_notification_settings,
+ chat_notification_flags,
+ chat_notification_channels,
+ chat_notification_events
+ ].flatten,
'microsoft-teams' => [
{
required: true,
@@ -517,19 +641,11 @@ module API
}
],
'mattermost' => [
- {
- required: true,
- name: :webhook,
- type: String,
- desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
- },
- {
- required: false,
- name: :username,
- type: String,
- desc: 'The username to use to post the message'
- }
- ],
+ chat_notification_settings,
+ chat_notification_flags,
+ chat_notification_channels,
+ chat_notification_events
+ ].flatten,
'teamcity' => [
{
required: true,
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 0d394a7b441..5e0afc6a7e4 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -14,10 +14,15 @@ module API
success Entities::Tag
end
params do
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return tags sorted in updated by `asc` or `desc` order.'
+ optional :order_by, type: String, values: %w[name updated], default: 'updated',
+ desc: 'Return tags ordered by `name` or `updated` fields.'
use :pagination
end
get ':id/repository/tags' do
- tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
+ tags = ::Kaminari.paginate_array(::TagsFinder.new(user_project.repository, sort: "#{params[:order_by]}_#{params[:sort]}").execute)
+
present paginate(tags), with: Entities::Tag, project: user_project
end
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
index df4632346dd..2bb451dea89 100644
--- a/lib/api/time_tracking_endpoints.rb
+++ b/lib/api/time_tracking_endpoints.rb
@@ -85,7 +85,7 @@ module API
update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
- user: current_user
+ user_id: current_user.id
})
end
@@ -97,7 +97,7 @@ module API
authorize! update_issuable_key, load_issuable
status :ok
- update_issuable(spend_time: { duration: :reset, user: current_user })
+ update_issuable(spend_time: { duration: :reset, user_id: current_user.id })
end
desc "Show time stats for a project #{issuable_name}"
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
index bd5eb2175e8..4157462ec2a 100644
--- a/lib/api/v3/labels.rb
+++ b/lib/api/v3/labels.rb
@@ -11,7 +11,7 @@ module API
success ::API::Entities::Label
end
get ':id/labels' do
- present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
+ present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project
end
desc 'Delete an existing label' do
diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb
index d5b90e435ba..1aad39815f9 100644
--- a/lib/api/v3/time_tracking_endpoints.rb
+++ b/lib/api/v3/time_tracking_endpoints.rb
@@ -86,7 +86,7 @@ module API
update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
- user: current_user
+ user_id: current_user.id
})
end
@@ -98,7 +98,7 @@ module API
authorize! update_issuable_key, load_issuable
status :ok
- update_issuable(spend_time: { duration: :reset, user: current_user })
+ update_issuable(spend_time: { duration: :reset, user_id: current_user.id })
end
desc "Show time stats for a project #{issuable_name}"
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 30a91647b77..287d591e88d 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -18,7 +18,7 @@ module Backup
FileUtils.rm_f(backup_tarball)
if ENV['STRATEGY'] == 'copy'
- cmd = %W(cp -a #{app_files_dir} #{Gitlab.config.backup.path})
+ cmd = %W(rsync -a --exclude=lost+found #{app_files_dir} #{Gitlab.config.backup.path})
output, status = Gitlab::Popen.popen(cmd)
unless status.zero?
@@ -26,10 +26,10 @@ module Backup
abort 'Backup failed'
end
- run_pipeline!([%W(tar -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 -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
diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb
index b545b947a2c..65c131e08d9 100644
--- a/lib/banzai/filter/mermaid_filter.rb
+++ b/lib/banzai/filter/mermaid_filter.rb
@@ -2,16 +2,7 @@ module Banzai
module Filter
class MermaidFilter < HTML::Pipeline::Filter
def call
- doc.css('pre[lang="mermaid"]').add_class('mermaid')
- doc.css('pre[lang="mermaid"]').add_class('js-render-mermaid')
-
- # The `<code></code>` blocks are added in the lib/banzai/filter/syntax_highlight_filter.rb
- # We want to keep context and consistency, so we the blocks are added for all filters.
- # Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107/diffs?diff_id=7962900#note_45495859
- doc.css('pre[lang="mermaid"]').each do |pre|
- document = pre.at('code')
- document.replace(document.content)
- end
+ doc.css('pre[lang="mermaid"] > code').add_class('js-render-mermaid')
doc
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 758f15c8a67..5c197afd782 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -2,19 +2,21 @@ require 'uri'
module Banzai
module Filter
- # HTML filter that "fixes" relative links to files in a repository.
+ # HTML filter that "fixes" relative links to uploads or files in a repository.
#
# Context options:
# :commit
+ # :group
# :project
# :project_wiki
# :ref
# :requested_path
class RelativeLinkFilter < HTML::Pipeline::Filter
- def call
- return doc unless linkable_files?
+ include Gitlab::Utils::StrongMemoize
+ def call
@uri_types = {}
+ clear_memoization(:linkable_files)
doc.search('a:not(.gfm)').each do |el|
process_link_attr el.attribute('href')
@@ -31,18 +33,40 @@ module Banzai
protected
def linkable_files?
- context[:project_wiki].nil? && repository.try(:exists?) && !repository.empty?
+ strong_memoize(:linkable_files) do
+ context[:project_wiki].nil? && repository.try(:exists?) && !repository.empty?
+ end
end
def process_link_attr(html_attr)
return if html_attr.blank?
return if html_attr.value.start_with?('//')
+ if html_attr.value.start_with?('/uploads/')
+ process_link_to_upload_attr(html_attr)
+ elsif linkable_files?
+ process_link_to_repository_attr(html_attr)
+ end
+ end
+
+ def process_link_to_upload_attr(html_attr)
+ uri_parts = [html_attr.value]
+
+ if group
+ uri_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
+ elsif project
+ uri_parts.unshift(relative_url_root, project.full_path)
+ end
+
+ html_attr.value = File.join(*uri_parts)
+ end
+
+ def process_link_to_repository_attr(html_attr)
uri = URI(html_attr.value)
if uri.relative? && uri.path.present?
html_attr.value = rebuild_relative_uri(uri).to_s
end
- rescue URI::Error
+ rescue URI::Error, Addressable::URI::InvalidURIError
# noop
end
@@ -51,7 +75,7 @@ module Banzai
uri.path = [
relative_url_root,
- context[:project].full_path,
+ project.full_path,
uri_type(file_path),
Addressable::URI.escape(ref),
Addressable::URI.escape(file_path)
@@ -123,11 +147,19 @@ module Banzai
end
def ref
- context[:ref] || context[:project].default_branch
+ context[:ref] || project.default_branch
+ end
+
+ def group
+ context[:group]
+ end
+
+ def project
+ context[:project]
end
def repository
- @repository ||= context[:project].try(:repository)
+ @repository ||= project&.repository
end
end
end
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 47151626208..97244159985 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -32,6 +32,7 @@ module Banzai
.gsub(PUNCTUATION_REGEXP, '') # remove punctuation
.tr(' ', '-') # replace spaces with dash
.squeeze('-') # replace multiple dashes with one
+ .gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs
uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
headers[id] += 1
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
deleted file mode 100644
index d64f9ac4eb6..00000000000
--- a/lib/banzai/filter/upload_link_filter.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'uri'
-
-module Banzai
- module Filter
- # HTML filter that "fixes" relative upload links to files.
- # Context options:
- # :project (required) - Current project
- #
- class UploadLinkFilter < HTML::Pipeline::Filter
- def call
- return doc unless project || group
-
- doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el|
- process_link_attr el.attribute('href')
- end
-
- doc.xpath('descendant-or-self::img[starts-with(@src, "/uploads/")]').each do |el|
- process_link_attr el.attribute('src')
- end
-
- doc
- end
-
- protected
-
- def process_link_attr(html_attr)
- html_attr.value = build_url(html_attr.value).to_s
- end
-
- def build_url(uri)
- base_path = Gitlab.config.gitlab.url
-
- if group
- urls = Gitlab::Routing.url_helpers
- # we need to get last 2 parts of the uri which are secret and filename
- uri_parts = uri.split(File::SEPARATOR)
- file_path = urls.show_group_uploads_path(group, uri_parts[-2], uri_parts[-1])
- File.join(base_path, file_path)
- else
- File.join(base_path, project.full_path, uri)
- end
- end
-
- def project
- context[:project]
- end
-
- def group
- context[:group]
- end
-
- # Ensure that a :project key exists in context
- #
- # Note that while the key might exist, its value could be nil!
- def validate
- needs :project
- end
- end
- end
-end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 55874ad50a3..c746f6f64e9 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -15,7 +15,6 @@ module Banzai
Filter::MathFilter,
Filter::MermaidFilter,
- Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 721ed97bb6b..d8aca3304c5 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -40,7 +40,7 @@ module ExtractsPath
def extract_ref(id)
pair = ['', '']
- return pair unless @project
+ return pair unless @project # rubocop:disable Gitlab/ModuleWithInstanceVariables
if id =~ /^(\h{40})(.+)/
# If the ref appears to be a SHA, we're done, just split the string
@@ -104,6 +104,7 @@ module ExtractsPath
#
# Automatically renders `not_found!` if a valid tree path could not be
# resolved (e.g., when a user inserts an invalid path or ref).
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def assign_ref_vars
# assign allowed options
allowed_options = ["filter_ref"]
@@ -127,13 +128,18 @@ module ExtractsPath
@hex_path = Digest::SHA1.hexdigest(@path)
@logs_path = logs_file_project_ref_path(@project, @ref, @path)
-
rescue RuntimeError, NoMethodError, InvalidPathError
render_404
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def tree
- @tree ||= @repo.tree(@commit.id, @path)
+ @tree ||= @repo.tree(@commit.id, @path) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def lfs_blob_ids
+ blob_ids = tree.blobs.map(&:id)
+ @lfs_blob_ids = Gitlab::Git::Blob.batch_lfs_pointers(@project.repository, blob_ids).map(&:id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
private
@@ -146,8 +152,8 @@ module ExtractsPath
end
def ref_names
- return [] unless @project
+ return [] unless @project # rubocop:disable Gitlab/ModuleWithInstanceVariables
- @ref_names ||= @project.repository.ref_names
+ @ref_names ||= @project.repository.ref_names # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/lib/feature.rb b/lib/feature.rb
index ac3bc65c0d5..8e9ba5c530a 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -1,5 +1,3 @@
-require 'flipper/adapters/active_record'
-
class Feature
# Classes to override flipper table names
class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature
@@ -62,12 +60,7 @@ class Feature
end
def flipper
- @flipper ||= begin
- adapter = Flipper::Adapters::ActiveRecord.new(
- feature_class: FlipperFeature, gate_class: FlipperGate)
-
- Flipper.new(adapter)
- end
+ @flipper ||= Flipper.instance
end
# This method is called from config/initializers/flipper.rb and can be used
diff --git a/lib/gitlab/action_rate_limiter.rb b/lib/gitlab/action_rate_limiter.rb
new file mode 100644
index 00000000000..4cd3bdefda3
--- /dev/null
+++ b/lib/gitlab/action_rate_limiter.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ # This class implements a simple rate limiter that can be used to throttle
+ # certain actions. Unlike Rack Attack and Rack::Throttle, which operate at
+ # the middleware level, this can be used at the controller level.
+ class ActionRateLimiter
+ TIME_TO_EXPIRE = 60 # 1 min
+
+ attr_accessor :action, :expiry_time
+
+ def initialize(action:, expiry_time: TIME_TO_EXPIRE)
+ @action = action
+ @expiry_time = expiry_time
+ end
+
+ # Increments the given cache key and increments the value by 1 with the
+ # given expiration time. Returns the incremented value.
+ #
+ # key - An array of ActiveRecord instances
+ def increment(key)
+ value = 0
+
+ Gitlab::Redis::Cache.with do |redis|
+ cache_key = action_key(key)
+ value = redis.incr(cache_key)
+ redis.expire(cache_key, expiry_time) if value == 1
+ end
+
+ value
+ end
+
+ # Increments the given key and returns true if the action should
+ # be throttled.
+ #
+ # key - An array of ActiveRecord instances
+ # threshold_value - The maximum number of times this action should occur in the given time interval
+ def throttled?(key, threshold_value)
+ self.increment(key) > threshold_value
+ end
+
+ private
+
+ def action_key(key)
+ serialized = key.map { |obj| "#{obj.class.model_name.to_s.underscore}:#{obj.id}" }.join(":")
+ "action_rate_limiter:#{action}:#{serialized}"
+ 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
new file mode 100644
index 00000000000..de622f657b2
--- /dev/null
+++ b/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Background migration for cleaning up a concurrent column rename.
+ class CleanupConcurrentTypeChange
+ include Database::MigrationHelpers
+
+ 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
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/copy_column.rb b/lib/gitlab/background_migration/copy_column.rb
new file mode 100644
index 00000000000..a2cb215c230
--- /dev/null
+++ b/lib/gitlab/background_migration/copy_column.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # CopyColumn is a simple (reusable) background migration that can be used to
+ # update the value of a column based on the value of another column in the
+ # same table.
+ #
+ # For this background migration to work the table that is migrated _has_ to
+ # have an `id` column as the primary key.
+ class CopyColumn
+ # table - The name of the table that contains the columns.
+ # copy_from - The column containing the data to copy.
+ # copy_to - The column to copy the data to.
+ # start_id - The start ID of the range of rows to update.
+ # end_id - The end ID of the range of rows to update.
+ def perform(table, copy_from, copy_to, start_id, end_id)
+ return unless connection.column_exists?(table, copy_to)
+
+ quoted_table = connection.quote_table_name(table)
+ quoted_copy_from = connection.quote_column_name(copy_from)
+ quoted_copy_to = connection.quote_column_name(copy_to)
+
+ # We're using raw SQL here since this job may be frequently executed. As
+ # a result dynamically defining models would lead to many unnecessary
+ # schema information queries.
+ connection.execute <<-SQL.strip_heredoc
+ UPDATE #{quoted_table}
+ SET #{quoted_copy_to} = #{quoted_copy_from}
+ WHERE id BETWEEN #{start_id} AND #{end_id}
+ SQL
+ end
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
index a1af045a71f..21b626dde56 100644
--- a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
+++ b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
@@ -1,44 +1,12 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class DeleteConflictingRedirectRoutesRange
- class Route < ActiveRecord::Base
- self.table_name = 'routes'
- end
-
- class RedirectRoute < ActiveRecord::Base
- self.table_name = 'redirect_routes'
- end
-
- # start_id - The start ID of the range of events to process
- # end_id - The end ID of the range to process.
def perform(start_id, end_id)
- return unless migrate?
-
- conflicts = RedirectRoute.where(routes_match_redirects_clause(start_id, end_id))
- num_rows = conflicts.delete_all
-
- Rails.logger.info("Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange [#{start_id}, #{end_id}] - Deleted #{num_rows} redirect routes that were conflicting with routes.")
- end
-
- def migrate?
- Route.table_exists? && RedirectRoute.table_exists?
- end
-
- def routes_match_redirects_clause(start_id, end_id)
- <<~ROUTES_MATCH_REDIRECTS
- EXISTS (
- SELECT 1 FROM routes
- WHERE (
- LOWER(redirect_routes.path) = LOWER(routes.path)
- OR LOWER(redirect_routes.path) LIKE LOWER(CONCAT(routes.path, '/%'))
- )
- AND routes.id BETWEEN #{start_id} AND #{end_id}
- )
- ROUTES_MATCH_REDIRECTS
+ # No-op.
+ # See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252
end
end
end
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 84ac00f1a5c..7088aa0860a 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
@@ -128,8 +128,14 @@ module Gitlab
end
def process_event(event)
- replicate_event(event)
- create_push_event_payload(event) if event.push_event?
+ ActiveRecord::Base.transaction do
+ replicate_event(event)
+ create_push_event_payload(event) if event.push_event?
+ end
+ rescue ActiveRecord::InvalidForeignKey => e
+ # A foreign key error means the associated event was removed. In this
+ # case we'll just skip migrating the event.
+ Rails.logger.error("Unable to migrate event #{event.id}: #{e}")
end
def replicate_event(event)
@@ -137,9 +143,6 @@ module Gitlab
.with_indifferent_access.except(:title, :data)
EventForMigration.create!(new_attributes)
- rescue ActiveRecord::InvalidForeignKey
- # A foreign key error means the associated event was removed. In this
- # case we'll just skip migrating the event.
end
def create_push_event_payload(event)
@@ -156,9 +159,6 @@ module Gitlab
ref: event.trimmed_ref_name,
commit_title: event.commit_title
)
- rescue ActiveRecord::InvalidForeignKey
- # A foreign key error means the associated event was removed. In this
- # case we'll just skip migrating the event.
end
def find_events(start_id, end_id)
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
new file mode 100644
index 00000000000..8a901a9bf39
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Metrics/MethodLength
+# rubocop:disable Metrics/ClassLength
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class PopulateMergeRequestMetricsWithEventsData
+ def perform(min_merge_request_id, max_merge_request_id)
+ insert_metrics_for_range(min_merge_request_id, max_merge_request_id)
+ update_metrics_with_events_data(min_merge_request_id, max_merge_request_id)
+ end
+
+ # Inserts merge_request_metrics records for merge_requests without it for
+ # a given merge request batch.
+ def insert_metrics_for_range(min, max)
+ metrics_not_exists_clause =
+ <<-SQL.strip_heredoc
+ NOT EXISTS (SELECT 1 FROM merge_request_metrics
+ WHERE merge_request_metrics.merge_request_id = merge_requests.id)
+ SQL
+
+ MergeRequest.where(metrics_not_exists_clause).where(id: min..max).each_batch do |batch|
+ select_sql = batch.select(:id, :created_at, :updated_at).to_sql
+
+ execute("INSERT INTO merge_request_metrics (merge_request_id, created_at, updated_at) #{select_sql}")
+ end
+ end
+
+ def update_metrics_with_events_data(min, max)
+ if Gitlab::Database.postgresql?
+ # Uses WITH syntax in order to update merged and closed events with a single UPDATE.
+ # WITH is not supported by MySQL.
+ update_events_for_range(min, max)
+ else
+ update_merged_events_for_range(min, max)
+ update_closed_events_for_range(min, max)
+ end
+ end
+
+ private
+
+ # Updates merge_request_metrics latest_closed_at, latest_closed_by_id and merged_by_id
+ # based on the latest event records on events table for a given merge request batch.
+ def update_events_for_range(min, max)
+ sql = <<-SQL.strip_heredoc
+ WITH events_for_update AS (
+ SELECT DISTINCT ON (target_id, action) target_id, action, author_id, updated_at
+ FROM events
+ WHERE target_id BETWEEN #{min} AND #{max}
+ AND target_type = 'MergeRequest'
+ AND action IN (#{Event::CLOSED},#{Event::MERGED})
+ ORDER BY target_id, action, id DESC
+ )
+ UPDATE merge_request_metrics met
+ SET latest_closed_at = latest_closed.updated_at,
+ latest_closed_by_id = latest_closed.author_id,
+ merged_by_id = latest_merged.author_id
+ FROM (SELECT * FROM events_for_update WHERE action = #{Event::CLOSED}) AS latest_closed
+ FULL OUTER JOIN
+ (SELECT * FROM events_for_update WHERE action = #{Event::MERGED}) AS latest_merged
+ USING (target_id)
+ WHERE target_id = merge_request_id;
+ SQL
+
+ execute(sql)
+ end
+
+ # Updates merge_request_metrics latest_closed_at, latest_closed_by_id based on the latest closed
+ # records on events table for a given merge request batch.
+ def update_closed_events_for_range(min, max)
+ sql =
+ <<-SQL.strip_heredoc
+ UPDATE merge_request_metrics metrics,
+ (#{select_events(min, max, Event::CLOSED)}) closed_events
+ SET metrics.latest_closed_by_id = closed_events.author_id,
+ metrics.latest_closed_at = closed_events.updated_at #{where_matches_closed_events};
+ SQL
+
+ execute(sql)
+ end
+
+ # Updates merge_request_metrics merged_by_id based on the latest merged
+ # records on events table for a given merge request batch.
+ def update_merged_events_for_range(min, max)
+ sql =
+ <<-SQL.strip_heredoc
+ UPDATE merge_request_metrics metrics,
+ (#{select_events(min, max, Event::MERGED)}) merged_events
+ SET metrics.merged_by_id = merged_events.author_id #{where_matches_merged_events};
+ SQL
+
+ execute(sql)
+ end
+
+ def execute(sql)
+ @connection ||= ActiveRecord::Base.connection
+ @connection.execute(sql)
+ end
+
+ def select_events(min, max, action)
+ select_max_event_id = <<-SQL.strip_heredoc
+ SELECT max(id)
+ FROM events
+ WHERE action = #{action}
+ AND target_type = 'MergeRequest'
+ AND target_id BETWEEN #{min} AND #{max}
+ GROUP BY target_id
+ SQL
+
+ <<-SQL.strip_heredoc
+ SELECT author_id, updated_at, target_id
+ FROM events
+ WHERE id IN(#{select_max_event_id})
+ SQL
+ end
+
+ def where_matches_closed_events
+ <<-SQL.strip_heredoc
+ WHERE metrics.merge_request_id = closed_events.target_id
+ AND metrics.latest_closed_at IS NULL
+ AND metrics.latest_closed_by_id IS NULL
+ SQL
+ end
+
+ def where_matches_merged_events
+ <<-SQL.strip_heredoc
+ WHERE metrics.merge_request_id = merged_events.target_id
+ AND metrics.merged_by_id IS NULL
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb
index 196de667805..709a901aa77 100644
--- a/lib/gitlab/bare_repository_import/importer.rb
+++ b/lib/gitlab/bare_repository_import/importer.rb
@@ -14,7 +14,7 @@ module Gitlab
repos_to_import.each do |repo_path|
bare_repo = Gitlab::BareRepositoryImport::Repository.new(import_path, repo_path)
- if bare_repo.hashed? || bare_repo.wiki?
+ unless bare_repo.processable?
log " * Skipping repo #{bare_repo.repo_path}".color(:yellow)
next
@@ -55,11 +55,15 @@ module Gitlab
name: project_name,
path: project_name,
skip_disk_validation: true,
+ skip_wiki: bare_repo.wiki_exists?,
+ import_type: 'bare_repository',
namespace_id: group&.id).execute
if project.persisted? && mv_repo(project)
log " * Created #{project.name} (#{project_full_path})".color(:green)
+ project.write_repository_config
+
ProjectCacheWorker.perform_async(project.id)
else
log " * Failed trying to create #{project.name} (#{project_full_path})".color(:red)
diff --git a/lib/gitlab/bare_repository_import/repository.rb b/lib/gitlab/bare_repository_import/repository.rb
index 8574ac6eb30..85b79362196 100644
--- a/lib/gitlab/bare_repository_import/repository.rb
+++ b/lib/gitlab/bare_repository_import/repository.rb
@@ -6,37 +6,56 @@ module Gitlab
def initialize(root_path, repo_path)
@root_path = root_path
@repo_path = repo_path
+ @root_path << '/' unless root_path.ends_with?('/')
+
+ full_path =
+ if hashed? && !wiki?
+ repository.config.get('gitlab.fullpath')
+ else
+ repo_relative_path
+ end
# Split path into 'all/the/namespaces' and 'project_name'
- @group_path, _, @project_name = repo_relative_path.rpartition('/')
+ @group_path, _, @project_name = full_path.to_s.rpartition('/')
end
def wiki_exists?
File.exist?(wiki_path)
end
- def wiki?
- @wiki ||= repo_path.end_with?('.wiki.git')
- end
-
def wiki_path
@wiki_path ||= repo_path.sub(/\.git$/, '.wiki.git')
end
- def hashed?
- @hashed ||= group_path.start_with?('@hashed')
- end
-
def project_full_path
@project_full_path ||= "#{group_path}/#{project_name}"
end
+ def processable?
+ return false if wiki?
+ return false if hashed? && (group_path.blank? || project_name.blank?)
+
+ true
+ end
+
private
+ def wiki?
+ @wiki ||= repo_path.end_with?('.wiki.git')
+ end
+
+ def hashed?
+ @hashed ||= repo_relative_path.include?('@hashed')
+ end
+
def repo_relative_path
# Remove root path and `.git` at the end
repo_path[@root_path.size...-4]
end
+
+ def repository
+ @repository ||= Rugged::Repository.new(repo_path)
+ end
end
end
end
diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb
index 754a45c3257..ecc85f847d4 100644
--- a/lib/gitlab/cache/request_cache.rb
+++ b/lib/gitlab/cache/request_cache.rb
@@ -45,11 +45,13 @@ module Gitlab
klass.prepend(extension)
end
+ attr_accessor :request_cache_key_block
+
def request_cache_key(&block)
if block_given?
- @request_cache_key = block
+ self.request_cache_key_block = block
else
- @request_cache_key
+ request_cache_key_block
end
end
diff --git a/lib/gitlab/checks/project_moved.rb b/lib/gitlab/checks/project_moved.rb
new file mode 100644
index 00000000000..dfb2f4d4054
--- /dev/null
+++ b/lib/gitlab/checks/project_moved.rb
@@ -0,0 +1,69 @@
+module Gitlab
+ module Checks
+ class ProjectMoved
+ REDIRECT_NAMESPACE = "redirect_namespace".freeze
+
+ def initialize(project, user, redirected_path, protocol)
+ @project = project
+ @user = user
+ @redirected_path = redirected_path
+ @protocol = protocol
+ end
+
+ def self.fetch_redirect_message(user_id, project_id)
+ redirect_key = redirect_message_key(user_id, project_id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ message = redis.get(redirect_key)
+ redis.del(redirect_key)
+ message
+ end
+ end
+
+ def add_redirect_message
+ # Don't bother with sending a redirect message for anonymous clones
+ # because they never see it via the `/internal/post_receive` endpoint
+ return unless user.present? && project.present?
+
+ Gitlab::Redis::SharedState.with do |redis|
+ key = self.class.redirect_message_key(user.id, project.id)
+ redis.setex(key, 5.minutes, redirect_message)
+ end
+ end
+
+ def redirect_message(rejected: false)
+ <<~MESSAGE.strip_heredoc
+ Project '#{redirected_path}' was moved to '#{project.full_path}'.
+
+ Please update your Git remote:
+
+ #{remote_url_message(rejected)}
+ MESSAGE
+ end
+
+ def permanent_redirect?
+ RedirectRoute.permanent.exists?(path: redirected_path)
+ end
+
+ private
+
+ attr_reader :project, :redirected_path, :protocol, :user
+
+ def self.redirect_message_key(user_id, project_id)
+ "#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}"
+ end
+
+ def remote_url_message(rejected)
+ if rejected
+ "git remote set-url origin #{url} and try again."
+ else
+ "git remote set-url origin #{url}"
+ end
+ end
+
+ def url
+ protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 72b75791bbb..e25916528f4 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -234,7 +234,7 @@ module Gitlab
# Most terminals show bold colored text in the light color variant
# Let's mimic that here
if @style_mask & STYLE_SWITCHES[:bold] != 0
- fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
+ fg_color.sub!(/fg-([a-z]{2,}+)/, 'fg-l-\1')
end
css_classes << fg_color
end
diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb
index 7df7b542d91..525563a97f5 100644
--- a/lib/gitlab/ci/charts.rb
+++ b/lib/gitlab/ci/charts.rb
@@ -6,7 +6,7 @@ module Gitlab
query
.group("DATE(#{::Ci::Pipeline.table_name}.created_at)")
.count(:created_at)
- .transform_keys { |date| date.strftime(@format) }
+ .transform_keys { |date| date.strftime(@format) } # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def interval_step
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index 68b6742385a..db47c2f6185 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -29,15 +29,15 @@ module Gitlab
self.class.nodes.each do |key, factory|
factory
- .value(@config[key])
+ .value(config[key])
.with(key: key, parent: self)
- @entries[key] = factory.create!
+ entries[key] = factory.create!
end
yield if block_given?
- @entries.each_value do |entry|
+ entries.each_value do |entry|
entry.compose!(deps)
end
end
@@ -59,13 +59,13 @@ module Gitlab
def helpers(*nodes)
nodes.each do |symbol|
define_method("#{symbol}_defined?") do
- @entries[symbol]&.specified?
+ entries[symbol]&.specified?
end
define_method("#{symbol}_value") do
- return unless @entries[symbol] && @entries[symbol].valid?
+ return unless entries[symbol] && entries[symbol].valid?
- @entries[symbol].value
+ entries[symbol].value
end
end
end
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
index c868943c42e..26505c91be3 100644
--- a/lib/gitlab/ci/config/entry/node.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -90,6 +90,10 @@ module Gitlab
def self.aspects
@aspects ||= []
end
+
+ private
+
+ attr_reader :entries
end
end
end
diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
index 5ced778d311..e45787773a8 100644
--- a/lib/gitlab/ci/config/entry/validatable.rb
+++ b/lib/gitlab/ci/config/entry/validatable.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def errors
- @validator.messages + descendants.flat_map(&:errors)
+ @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
class_methods do
diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb
index 8d82e1b288d..efed19da21c 100644
--- a/lib/gitlab/ci/pipeline/chain/base.rb
+++ b/lib/gitlab/ci/pipeline/chain/base.rb
@@ -3,14 +3,13 @@ module Gitlab
module Pipeline
module Chain
class Base
- attr_reader :pipeline, :project, :current_user
+ attr_reader :pipeline, :command
+
+ delegate :project, :current_user, to: :command
def initialize(pipeline, command)
@pipeline = pipeline
@command = command
-
- @project = command.project
- @current_user = command.current_user
end
def perform!
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index a126dded1ae..70732d26bbd 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -3,20 +3,18 @@ module Gitlab
module Pipeline
module Chain
class Build < Chain::Base
- include Chain::Helpers
-
def perform!
@pipeline.assign_attributes(
source: @command.source,
- project: @project,
- ref: ref,
- sha: sha,
- before_sha: before_sha,
- tag: tag_exists?,
+ project: @command.project,
+ ref: @command.ref,
+ sha: @command.sha,
+ before_sha: @command.before_sha,
+ tag: @command.tag_exists?,
trigger_requests: Array(@command.trigger_request),
- user: @current_user,
+ user: @command.current_user,
pipeline_schedule: @command.schedule,
- protected: protected_ref?
+ protected: @command.protected_ref?
)
@pipeline.set_config_source
@@ -25,32 +23,6 @@ module Gitlab
def break?
false
end
-
- private
-
- def ref
- @ref ||= Gitlab::Git.ref_name(origin_ref)
- end
-
- def sha
- @project.commit(origin_sha || origin_ref).try(:id)
- end
-
- def origin_ref
- @command.origin_ref
- end
-
- def origin_sha
- @command.checkout_sha || @command.after_sha
- end
-
- def before_sha
- @command.checkout_sha || @command.before_sha || Gitlab::Git::BLANK_SHA
- end
-
- def protected_ref?
- @project.protected_for?(ref)
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
new file mode 100644
index 00000000000..7b19b10e05b
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ Command = Struct.new(
+ :source, :project, :current_user,
+ :origin_ref, :checkout_sha, :after_sha, :before_sha,
+ :trigger_request, :schedule,
+ :ignore_skip_ci, :save_incompleted,
+ :seeds_block
+ ) do
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(**params)
+ params.each do |key, value|
+ self[key] = value
+ end
+ end
+
+ def branch_exists?
+ strong_memoize(:is_branch) do
+ project.repository.branch_exists?(ref)
+ end
+ end
+
+ def tag_exists?
+ strong_memoize(:is_tag) do
+ project.repository.tag_exists?(ref)
+ end
+ end
+
+ def ref
+ strong_memoize(:ref) do
+ Gitlab::Git.ref_name(origin_ref)
+ end
+ end
+
+ def sha
+ strong_memoize(:sha) do
+ project.commit(origin_sha || origin_ref).try(:id)
+ end
+ end
+
+ def origin_sha
+ checkout_sha || after_sha
+ end
+
+ def before_sha
+ self[:before_sha] || checkout_sha || Gitlab::Git::BLANK_SHA
+ end
+
+ def protected_ref?
+ strong_memoize(:protected_ref) do
+ project.protected_for?(ref)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
index 02d81286f21..bf1380a1da9 100644
--- a/lib/gitlab/ci/pipeline/chain/helpers.rb
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -3,18 +3,6 @@ module Gitlab
module Pipeline
module Chain
module Helpers
- def branch_exists?
- return @is_branch if defined?(@is_branch)
-
- @is_branch = project.repository.branch_exists?(pipeline.ref)
- end
-
- def tag_exists?
- return @is_tag if defined?(@is_tag)
-
- @is_tag = project.repository.tag_exists?(pipeline.ref)
- end
-
def error(message)
pipeline.errors.add(:base, message)
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
index 4913a604079..13c6fedd831 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -14,7 +14,7 @@ module Gitlab
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
- return error("Insufficient permissions for protected ref '#{pipeline.ref}'")
+ return error("Insufficient permissions for protected ref '#{command.ref}'")
else
return error('Insufficient permissions to create a new pipeline')
end
@@ -29,7 +29,7 @@ module Gitlab
if current_user
allowed_to_create?
else # legacy triggers don't have a corresponding user
- !project.protected_for?(@pipeline.ref)
+ !@command.protected_ref?
end
end
@@ -38,10 +38,10 @@ module Gitlab
access = Gitlab::UserAccess.new(current_user, project: project)
- if branch_exists?
- access.can_update_branch?(@pipeline.ref)
- elsif tag_exists?
- access.can_create_tag?(@pipeline.ref)
+ if @command.branch_exists?
+ access.can_update_branch?(@command.ref)
+ elsif @command.tag_exists?
+ access.can_create_tag?(@command.ref)
else
true # Allow it for now and we'll reject when we check ref existence
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
index 70a4cfdbdea..9699c24e5b6 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/repository.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
@@ -7,14 +7,11 @@ module Gitlab
include Chain::Helpers
def perform!
- unless branch_exists? || tag_exists?
+ unless @command.branch_exists? || @command.tag_exists?
return error('Reference not found')
end
- ## TODO, we check commit in the service, that is why
- # there is no repository access here.
- #
- unless pipeline.sha
+ unless @command.sha
return error('Commit not found')
end
end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index fb28e80ff73..0a3ae2c3760 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -4,21 +4,24 @@ module Gitlab
attr_reader :merge_request, :resolver
def initialize(merge_request)
- source_repo = merge_request.source_project.repository.raw
our_commit = merge_request.source_branch_head.raw
their_commit = merge_request.target_branch_head.raw
target_repo = merge_request.target_project.repository.raw
- @resolver = Gitlab::Git::Conflict::Resolver.new(source_repo, our_commit, target_repo, their_commit)
+ @source_repo = merge_request.source_project.repository.raw
+ @resolver = Gitlab::Git::Conflict::Resolver.new(target_repo, our_commit.id, their_commit.id)
@merge_request = merge_request
end
def resolve(user, commit_message, files)
+ msg = commit_message || default_commit_message
+ resolution = Gitlab::Git::Conflict::Resolution.new(user, files, msg)
args = {
source_branch: merge_request.source_branch,
- target_branch: merge_request.target_branch,
- commit_message: commit_message || default_commit_message
+ target_branch: merge_request.target_branch
}
- resolver.resolve_conflicts(user, files, args)
+ resolver.resolve_conflicts(@source_repo, resolution, args)
+ ensure
+ @merge_request.clear_memoized_shas
end
def files
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 642f0944354..91fd9cc7631 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -53,7 +53,7 @@ module Gitlab
end
def in_memory_application_settings
- @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults)
+ @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables
rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
# In case migrations the application_settings table is not created yet,
# we fallback to a simple OpenStruct
diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index ab115afcaa5..e3e3767cc75 100644
--- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -56,7 +56,9 @@ module Gitlab
end
def allowed_ids
- nil
+ @allowed_ids ||= allowed_ids_finder_class
+ .new(@options[:current_user], project_id: @project.id)
+ .execute.where(id: event_result_ids).pluck(:id)
end
def event_result_ids
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
index 58729d3ced8..dcbdf9a64b0 100644
--- a/lib/gitlab/cycle_analytics/base_query.rb
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -14,9 +14,9 @@ module Gitlab
def stage_query
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id]))
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
- .where(issue_table[:project_id].eq(@project.id))
+ .where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables
.where(issue_table[:deleted_at].eq(nil))
- .where(issue_table[:created_at].gteq(@options[:from]))
+ .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests
query = query.join(mr_table, Arel::Nodes::OuterJoin)
diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index d5bf6149749..06357c9b377 100644
--- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -1,8 +1,6 @@
module Gitlab
module CycleAnalytics
class CodeEventFetcher < BaseEventFetcher
- include MergeRequestAllowed
-
def initialize(*args)
@projections = [mr_table[:title],
mr_table[:iid],
@@ -20,6 +18,10 @@ module Gitlab
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
end
+
+ def allowed_ids_finder_class
+ MergeRequestsFinder
+ end
end
end
end
diff --git a/lib/gitlab/cycle_analytics/issue_allowed.rb b/lib/gitlab/cycle_analytics/issue_allowed.rb
deleted file mode 100644
index a7652a70641..00000000000
--- a/lib/gitlab/cycle_analytics/issue_allowed.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Gitlab
- module CycleAnalytics
- module IssueAllowed
- def allowed_ids
- @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 3df9cbdcfce..1754f91dccb 100644
--- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -1,8 +1,6 @@
module Gitlab
module CycleAnalytics
class IssueEventFetcher < BaseEventFetcher
- include IssueAllowed
-
def initialize(*args)
@projections = [issue_table[:title],
issue_table[:iid],
@@ -18,6 +16,10 @@ module Gitlab
def serialize(event)
AnalyticsIssueSerializer.new(project: @project).represent(event)
end
+
+ def allowed_ids_finder_class
+ IssuesFinder
+ end
end
end
end
diff --git a/lib/gitlab/cycle_analytics/merge_request_allowed.rb b/lib/gitlab/cycle_analytics/merge_request_allowed.rb
deleted file mode 100644
index 28f6db44759..00000000000
--- a/lib/gitlab/cycle_analytics/merge_request_allowed.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Gitlab
- module CycleAnalytics
- module MergeRequestAllowed
- def allowed_ids
- @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 9230894877f..086203b9ccc 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -18,6 +18,10 @@ module Gitlab
private
+ def allowed_ids
+ nil
+ end
+
def merge_request_diff_commits
@merge_request_diff_commits ||=
MergeRequestDiffCommit
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
index d693443bfa4..7a889b3877f 100644
--- a/lib/gitlab/cycle_analytics/production_helper.rb
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -2,7 +2,9 @@ module Gitlab
module CycleAnalytics
module ProductionHelper
def stage_query
- super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from]))
+ super
+ .where(mr_metrics_table[:first_deployed_to_production_at]
+ .gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
end
diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index 4c7b3f4467f..dada819a2a8 100644
--- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -1,8 +1,6 @@
module Gitlab
module CycleAnalytics
class ReviewEventFetcher < BaseEventFetcher
- include MergeRequestAllowed
-
def initialize(*args)
@projections = [mr_table[:title],
mr_table[:iid],
@@ -14,9 +12,15 @@ module Gitlab
super(*args)
end
+ private
+
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
end
+
+ def allowed_ids_finder_class
+ MergeRequestsFinder
+ end
end
end
end
diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
index 36c0260dbfe..2f014153ca5 100644
--- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -22,6 +22,10 @@ module Gitlab
private
+ def allowed_ids
+ nil
+ end
+
def serialize(event)
AnalyticsBuildSerializer.new.represent(event['build'])
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 3f65bc912de..7b35c24d153 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -385,10 +385,27 @@ module Gitlab
# necessary since we copy over old values further down.
change_column_default(table, new, old_col.default) if old_col.default
- trigger_name = rename_trigger_name(table, old, new)
+ install_rename_triggers(table, old, new)
+
+ update_column_in_batches(table, new, Arel::Table.new(table)[old])
+
+ change_column_null(table, new, false) unless old_col.null
+
+ copy_indexes(table, old, new)
+ copy_foreign_keys(table, old, new)
+ end
+
+ # Installs triggers in a table that keep a new column in sync with an old
+ # one.
+ #
+ # table - The name of the table to install the trigger in.
+ # old_column - The name of the old column.
+ # new_column - The name of the new column.
+ def install_rename_triggers(table, old_column, new_column)
+ trigger_name = rename_trigger_name(table, old_column, new_column)
quoted_table = quote_table_name(table)
- quoted_old = quote_column_name(old)
- quoted_new = quote_column_name(new)
+ quoted_old = quote_column_name(old_column)
+ quoted_new = quote_column_name(new_column)
if Database.postgresql?
install_rename_triggers_for_postgresql(trigger_name, quoted_table,
@@ -397,13 +414,6 @@ module Gitlab
install_rename_triggers_for_mysql(trigger_name, quoted_table,
quoted_old, quoted_new)
end
-
- update_column_in_batches(table, new, Arel::Table.new(table)[old])
-
- change_column_null(table, new, false) unless old_col.null
-
- copy_indexes(table, old, new)
- copy_foreign_keys(table, old, new)
end
# Changes the type of a column concurrently.
@@ -455,6 +465,97 @@ module Gitlab
remove_column(table, old)
end
+ # Changes the column type of a table 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
+ # `change_column_type_concurrently` since it can complete its work in a
+ # much shorter amount of time and doesn't rely on Sidekiq.
+ #
+ # Example usage:
+ #
+ # class Issue < ActiveRecord::Base
+ # self.table_name = 'issues'
+ #
+ # include EachBatch
+ #
+ # def self.to_migrate
+ # where('closed_at IS NOT NULL')
+ # end
+ # end
+ #
+ # change_column_type_using_background_migration(
+ # Issue.to_migrate,
+ # :closed_at,
+ # :datetime_with_timezone
+ # )
+ #
+ # Reverting a migration like this is done exactly the same way, just with
+ # a different type to migrate to (e.g. `:datetime` in the above example).
+ #
+ # relation - An ActiveRecord relation to use for scheduling jobs and
+ # figuring out what table we're modifying. This relation _must_
+ # have the EachBatch module included.
+ #
+ # column - The name of the column for which the type will be changed.
+ #
+ # new_type - The new type of the column.
+ #
+ # batch_size - The number of rows to schedule in a single background
+ # migration.
+ #
+ # interval - The time interval between every background migration.
+ def change_column_type_using_background_migration(
+ relation,
+ column,
+ new_type,
+ batch_size: 10_000,
+ interval: 10.minutes
+ )
+ unless relation.model < EachBatch
+ raise TypeError, 'The relation must include the EachBatch module'
+ end
+
+ temp_column = "#{column}_for_type_change"
+ table = relation.table_name
+ max_index = 0
+
+ add_column(table, temp_column, new_type)
+ install_rename_triggers(table, column, temp_column)
+
+ # Schedule the jobs that will copy the data from the old column to the
+ # new one.
+ relation.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, column, temp_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,
+ 'CleanupConcurrentTypeChange',
+ [table, column, temp_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('CleanupConcurrentTypeChange')
+ end
+ end
+
+ def perform_background_migration_inline?
+ Rails.env.test? || Rails.env.development?
+ end
+
# Performs a concurrent column rename when using PostgreSQL.
def install_rename_triggers_for_postgresql(trigger, table, old, new)
execute <<-EOF.strip_heredoc
@@ -741,6 +842,12 @@ into similar problems in the future (e.g. when new tables are created).
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+ # To not overload the worker too much we enforce a minimum interval both
+ # when scheduling and performing jobs.
+ if delay_interval < BackgroundMigrationWorker::MIN_INTERVAL
+ delay_interval = BackgroundMigrationWorker::MIN_INTERVAL
+ end
+
model_class.each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
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 7e492938eac..fd4a8832ec2 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
@@ -6,7 +6,7 @@ module Gitlab
module Routable
def full_path
if route && route.path.present?
- @full_path ||= route.path
+ @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
else
update_route if persisted?
@@ -30,7 +30,7 @@ module Gitlab
def prepare_route
route || build_route(source: self)
route.path = build_full_path
- @full_path = nil
+ @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index d0cfe2386ca..34b070dd375 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -61,7 +61,9 @@ module Gitlab
end
def line_for_position(pos)
- diff_lines.find { |line| position(line) == pos }
+ return nil unless pos.position_type == 'text'
+
+ diff_lines.find { |line| line.old_line == pos.old_line && line.new_line == pos.new_line }
end
def position_for_line_code(code)
@@ -114,8 +116,10 @@ module Gitlab
new_content_sha || old_content_sha
end
+ # Use #itself to check the value wrapped by a BatchLoader instance, rather
+ # than if the BatchLoader instance itself is falsey.
def blob
- new_blob || old_blob
+ new_blob&.itself || old_blob&.itself
end
attr_writer :highlighted_diff_lines
@@ -171,7 +175,7 @@ module Gitlab
end
def binary?
- has_binary_notice? || old_blob&.binary? || new_blob&.binary?
+ has_binary_notice? || try_blobs(:binary?)
end
def text?
@@ -179,15 +183,15 @@ module Gitlab
end
def external_storage_error?
- old_blob&.external_storage_error? || new_blob&.external_storage_error?
+ try_blobs(:external_storage_error?)
end
def stored_externally?
- old_blob&.stored_externally? || new_blob&.stored_externally?
+ try_blobs(:stored_externally?)
end
def external_storage
- old_blob&.external_storage || new_blob&.external_storage
+ try_blobs(:external_storage)
end
def content_changed?
@@ -202,15 +206,15 @@ module Gitlab
end
def size
- [old_blob&.size, new_blob&.size].compact.sum
+ valid_blobs.map(&:size).sum
end
def raw_size
- [old_blob&.raw_size, new_blob&.raw_size].compact.sum
+ valid_blobs.map(&:raw_size).sum
end
def raw_binary?
- old_blob&.raw_binary? || new_blob&.raw_binary?
+ try_blobs(:raw_binary?)
end
def raw_text?
@@ -233,6 +237,19 @@ module Gitlab
private
+ # The blob instances are instances of BatchLoader, which means calling
+ # &. directly on them won't work. Object#try also won't work, because Blob
+ # doesn't inherit from Object, but from BasicObject (via SimpleDelegator).
+ def try_blobs(meth)
+ old_blob&.itself&.public_send(meth) || new_blob&.itself&.public_send(meth)
+ end
+
+ # We can't use #compact for the same reason we can't use &., but calling
+ # #nil? explicitly does work because it is proxied to the blob itself.
+ def valid_blobs
+ [old_blob, new_blob].reject(&:nil?)
+ end
+
def text_position_properties(line)
{ old_line: line.old_line, new_line: line.new_line }
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 4a9d3e52fae..37face8e7d0 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -280,7 +280,7 @@ module Gitlab
The `#{branch}` branch applies cleanly to EE/master!
Much â¤ï¸! For more information, see
- https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
#{THANKS_FOR_READING_BANNER}
}
end
@@ -357,7 +357,7 @@ module Gitlab
Once this is done, you can retry this failed build, and it should pass.
Stay 💪 ! For more information, see
- https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
#{THANKS_FOR_READING_BANNER}
}
end
@@ -378,7 +378,7 @@ module Gitlab
retry this build.
Stay 💪 ! For more information, see
- https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
#{THANKS_FOR_READING_BANNER}
}
end
diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb
index c63666b98c1..e2f7c1d0257 100644
--- a/lib/gitlab/email/handler/create_merge_request_handler.rb
+++ b/lib/gitlab/email/handler/create_merge_request_handler.rb
@@ -55,11 +55,13 @@ module Gitlab
end
def merge_request_params
- {
+ params = {
source_project_id: project.id,
source_branch: mail.subject,
target_project_id: project.id
}
+ params[:description] = message if message.present?
+ params
end
end
end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index e3e36b35ce9..89cf659bce4 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -31,8 +31,7 @@ module Gitlab
end
def emoji_unicode_version(name)
- @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
- @emoji_unicode_versions_by_name[name]
+ emoji_unicode_versions_by_name[name]
end
def normalize_emoji_name(name)
@@ -56,5 +55,12 @@ module Gitlab
ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], title: emoji_info['description'], data: data)
end
+
+ private
+
+ def emoji_unicode_versions_by_name
+ @emoji_unicode_versions_by_name ||=
+ JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
+ end
end
end
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 582028493e9..c0edcabc6fd 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -14,14 +14,7 @@ module Gitlab
ENCODING_CONFIDENCE_THRESHOLD = 50
def encode!(message)
- return nil unless message.respond_to?(:force_encoding)
- return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
-
- if message.respond_to?(:frozen?) && message.frozen?
- message = message.dup
- end
-
- message.force_encoding("UTF-8")
+ message = force_encode_utf8(message)
return message if message.valid_encoding?
# return message if message type is binary
@@ -35,6 +28,8 @@ module Gitlab
# encode and clean the bad chars
message.replace clean(message)
+ rescue ArgumentError
+ return nil
rescue
encoding = detect ? detect[:encoding] : "unknown"
"--broken encoding: #{encoding}"
@@ -54,8 +49,8 @@ module Gitlab
end
def encode_utf8(message)
- return nil unless message.is_a?(String)
- return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
+ message = force_encode_utf8(message)
+ return message if message.valid_encoding?
detect = CharlockHolmes::EncodingDetector.detect(message)
if detect && detect[:encoding]
@@ -69,10 +64,31 @@ module Gitlab
else
clean(message)
end
+ rescue ArgumentError
+ return nil
+ end
+
+ def encode_binary(s)
+ return "" if s.nil?
+
+ s.dup.force_encoding(Encoding::ASCII_8BIT)
+ end
+
+ def binary_stringio(s)
+ StringIO.new(s || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
end
private
+ def force_encode_utf8(message)
+ raise ArgumentError unless message.respond_to?(:force_encoding)
+ return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
+
+ message = message.dup if message.respond_to?(:frozen?) && message.frozen?
+
+ message.force_encoding("UTF-8")
+ end
+
def clean(message)
message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
.encode("UTF-8")
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 3f7b42456af..dbb8f317afe 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -71,5 +71,16 @@ module Gitlab
redis.exists(@redis_shared_state_key)
end
end
+
+ # Returns the TTL of the Redis key.
+ #
+ # This method will return `nil` if no TTL could be obtained.
+ def ttl
+ Gitlab::Redis::SharedState.with do |redis|
+ ttl = redis.ttl(@redis_shared_state_key)
+
+ ttl if ttl.positive?
+ end
+ end
end
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 1f7c35cafaa..71647099f83 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -11,7 +11,7 @@ module Gitlab
include Gitlab::EncodingHelper
def ref_name(ref)
- encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
+ encode!(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
end
def branch_name(ref)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 228d97a87ab..031fccba92b 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -50,10 +50,19 @@ module Gitlab
# 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: nil)
- blob_size_limit ||= MAX_DATA_DISPLAY_SIZE
- blob_references.map do |sha, path|
- find_by_rugged(repository, sha, path, limit: blob_size_limit)
+ 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
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ blob_references.map do |sha, path|
+ find_by_gitaly(repository, sha, path, limit: blob_size_limit)
+ end
+ end
+ else
+ blob_references.map do |sha, path|
+ find_by_rugged(repository, sha, path, limit: blob_size_limit)
+ end
+ end
end
end
@@ -122,13 +131,23 @@ module Gitlab
)
end
- def find_by_gitaly(repository, sha, path)
+ def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
path = path.sub(/\A\/*/, '')
path = '/' if path.empty?
name = File.basename(path)
- entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE)
+
+ # 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(
@@ -154,8 +173,8 @@ module Gitlab
end
def find_by_rugged(repository, sha, path, limit:)
- commit = repository.lookup(sha)
- root_tree = commit.tree
+ rugged_commit = repository.lookup(sha)
+ root_tree = rugged_commit.tree
blob_entry = find_entry_by_path(repository, root_tree.oid, path)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index e90b158fb34..016437b2419 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -15,8 +15,6 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
- delegate :tree, to: :rugged_commit
-
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
@@ -228,6 +226,19 @@ module Gitlab
end
end
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) do |is_enabled|
+ if is_enabled
+ repo.gitaly_commit_client.list_commits_by_oid(oids)
+ else
+ oids.map { |oid| find(repo, oid) }.compact
+ end
+ end
+ end
end
def initialize(repository, raw_commit, head = nil)
@@ -439,6 +450,11 @@ module Gitlab
)
end
+ # Is this the same as Blob.find_entry_by_path ?
+ def rugged_tree_entry(path)
+ rugged_commit.tree.path(path)
+ end
+
private
def init_from_hash(hash)
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index 6bf49a0af18..8463b1eb794 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -34,13 +34,8 @@ module Gitlab
def rugged_stats(commit)
diff = commit.rugged_diff_from_parent
-
- diff.each_patch do |p|
- # TODO: Use the new Rugged convenience methods when they're released
- @additions += p.stat[0]
- @deletions += p.stat[1]
- @total += p.changes
- end
+ _files_changed, @additions, @deletions = diff.stat
+ @total = @additions + @deletions
end
end
end
diff --git a/lib/gitlab/git/conflict/file.rb b/lib/gitlab/git/conflict/file.rb
index b2a625e08fa..2a9cf10a068 100644
--- a/lib/gitlab/git/conflict/file.rb
+++ b/lib/gitlab/git/conflict/file.rb
@@ -2,7 +2,9 @@ module Gitlab
module Git
module Conflict
class File
- attr_reader :content, :their_path, :our_path, :our_mode, :repository, :commit_oid
+ attr_reader :their_path, :our_path, :our_mode, :repository, :commit_oid
+
+ attr_accessor :content
def initialize(repository, commit_oid, conflict, content)
@repository = repository
diff --git a/lib/gitlab/git/conflict/resolution.rb b/lib/gitlab/git/conflict/resolution.rb
new file mode 100644
index 00000000000..ab9be683e15
--- /dev/null
+++ b/lib/gitlab/git/conflict/resolution.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Git
+ module Conflict
+ class Resolution
+ attr_reader :user, :files, :commit_message
+
+ def initialize(user, files, commit_message)
+ @user = user
+ @files = files
+ @commit_message = commit_message
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
index de8cce41b6d..74c9874d590 100644
--- a/lib/gitlab/git/conflict/resolver.rb
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -5,56 +5,39 @@ module Gitlab
ConflictSideMissing = Class.new(StandardError)
ResolutionError = Class.new(StandardError)
- def initialize(repository, our_commit, target_repository, their_commit)
- @repository = repository
- @our_commit = our_commit.rugged_commit
+ def initialize(target_repository, our_commit_oid, their_commit_oid)
@target_repository = target_repository
- @their_commit = their_commit.rugged_commit
+ @our_commit_oid = our_commit_oid
+ @their_commit_oid = their_commit_oid
end
def conflicts
@conflicts ||= begin
- target_index = @target_repository.rugged.merge_commits(@our_commit, @their_commit)
-
- # We don't need to do `with_repo_branch_commit` here, because the target
- # project always fetches source refs when creating merge request diffs.
- target_index.conflicts.map do |conflict|
- raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
-
- Gitlab::Git::Conflict::File.new(
- @target_repository,
- @our_commit.oid,
- conflict,
- target_index.merge_file(conflict[:ours][:path])[:data]
- )
+ @target_repository.gitaly_migrate(:conflicts_list_conflict_files) do |is_enabled|
+ if is_enabled
+ gitaly_conflicts_client(@target_repository).list_conflict_files
+ else
+ rugged_list_conflict_files
+ end
end
end
+ rescue GRPC::FailedPrecondition => e
+ raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message)
+ rescue Rugged::OdbError, GRPC::BadStatus => e
+ raise Gitlab::Git::CommandError.new(e)
end
- def resolve_conflicts(user, files, source_branch:, target_branch:, commit_message:)
- @repository.with_repo_branch_commit(@target_repository, target_branch) do
- files.each do |file_params|
- conflict_file = conflict_for_path(file_params[:old_path], file_params[:new_path])
-
- write_resolved_file_to_index(conflict_file, file_params)
+ 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
-
- unless index.conflicts.empty?
- missing_files = index.conflicts.map { |file| file[:ours][:path] }
-
- raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
- end
-
- commit_params = {
- message: commit_message,
- parents: [@our_commit, @their_commit].map(&:oid)
- }
-
- @repository.commit_index(user, source_branch, index, commit_params)
end
end
- def conflict_for_path(old_path, new_path)
+ def conflict_for_path(conflicts, old_path, new_path)
conflicts.find do |conflict|
conflict.their_path == old_path && conflict.our_path == new_path
end
@@ -62,15 +45,24 @@ module Gitlab
private
- # We can only write when getting the merge index from the source
- # project, because we will write to that project. We don't use this all
- # the time because this fetches a ref into the source project, which
- # isn't needed for reading.
- def index
- @index ||= @repository.rugged.merge_commits(@our_commit, @their_commit)
+ def conflict_files(repository, index)
+ index.conflicts.map do |conflict|
+ raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
+
+ Gitlab::Git::Conflict::File.new(
+ repository,
+ @our_commit_oid,
+ conflict,
+ index.merge_file(conflict[:ours][:path])[:data]
+ )
+ end
+ end
+
+ def gitaly_conflicts_client(repository)
+ repository.gitaly_conflicts_client(@our_commit_oid, @their_commit_oid)
end
- def write_resolved_file_to_index(file, params)
+ 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")
@@ -82,9 +74,44 @@ module Gitlab
our_path = file.our_path
- index.add(path: our_path, oid: @repository.rugged.write(new_file, :blob), mode: file.our_mode)
+ 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/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
new file mode 100644
index 00000000000..cba638c06db
--- /dev/null
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -0,0 +1,294 @@
+module Gitlab
+ module Git
+ class GitlabProjects
+ include Gitlab::Git::Popen
+ include Gitlab::Utils::StrongMemoize
+
+ ShardNameNotFoundError = Class.new(StandardError)
+
+ # Absolute path to directory where repositories are stored.
+ # Example: /home/git/repositories
+ attr_reader :shard_path
+
+ # Relative path is a directory name for repository with .git at the end.
+ # Example: gitlab-org/gitlab-test.git
+ attr_reader :repository_relative_path
+
+ # Absolute path to the repository.
+ # Example: /home/git/repositorities/gitlab-org/gitlab-test.git
+ attr_reader :repository_absolute_path
+
+ # This is the path at which the gitlab-shell hooks directory can be found.
+ # It's essential for integration between git and GitLab proper. All new
+ # repositories should have their hooks directory symlinked here.
+ attr_reader :global_hooks_path
+
+ attr_reader :logger
+
+ def initialize(shard_path, repository_relative_path, global_hooks_path:, logger:)
+ @shard_path = shard_path
+ @repository_relative_path = repository_relative_path
+
+ @logger = logger
+ @global_hooks_path = global_hooks_path
+ @repository_absolute_path = File.join(shard_path, repository_relative_path)
+ @output = StringIO.new
+ end
+
+ def output
+ io = @output.dup
+ io.rewind
+ io.read
+ end
+
+ def rm_project
+ logger.info "Removing repository <#{repository_absolute_path}>."
+ FileUtils.rm_rf(repository_absolute_path)
+ end
+
+ # Move repository from one directory to another
+ #
+ # Example: gitlab/gitlab-ci.git -> randx/six.git
+ #
+ # Won't work if target namespace directory does not exist
+ #
+ def mv_project(new_path)
+ new_absolute_path = File.join(shard_path, new_path)
+
+ # verify that the source repo exists
+ unless File.exist?(repository_absolute_path)
+ logger.error "mv-project failed: source path <#{repository_absolute_path}> does not exist."
+ return false
+ end
+
+ # ...and that the target repo does not exist
+ if File.exist?(new_absolute_path)
+ logger.error "mv-project failed: destination path <#{new_absolute_path}> already exists."
+ return false
+ end
+
+ logger.info "Moving repository from <#{repository_absolute_path}> to <#{new_absolute_path}>."
+ FileUtils.mv(repository_absolute_path, new_absolute_path)
+ end
+
+ # Import project via git clone --bare
+ # URL must be publicly cloneable
+ def import_project(source, timeout)
+ # Skip import if repo already exists
+ return false if File.exist?(repository_absolute_path)
+
+ masked_source = mask_password_in_url(source)
+
+ logger.info "Importing project from <#{masked_source}> to <#{repository_absolute_path}>."
+ cmd = %W(git clone --bare -- #{source} #{repository_absolute_path})
+
+ success = run_with_timeout(cmd, timeout, nil)
+
+ unless success
+ logger.error("Importing project from <#{masked_source}> to <#{repository_absolute_path}> failed.")
+ FileUtils.rm_rf(repository_absolute_path)
+ return false
+ end
+
+ Gitlab::Git::Repository.create_hooks(repository_absolute_path, global_hooks_path)
+
+ # The project was imported successfully.
+ # Remove the origin URL since it may contain password.
+ remove_origin_in_repo
+
+ true
+ end
+
+ def fork_repository(new_shard_path, new_repository_relative_path)
+ Gitlab::GitalyClient.migrate(:fork_repository) do |is_enabled|
+ if is_enabled
+ gitaly_fork_repository(new_shard_path, new_repository_relative_path)
+ else
+ git_fork_repository(new_shard_path, new_repository_relative_path)
+ end
+ end
+ end
+
+ def fetch_remote(name, timeout, force:, tags:, ssh_key: nil, known_hosts: nil)
+ tags_option = tags ? '--tags' : '--no-tags'
+
+ logger.info "Fetching remote #{name} for repository #{repository_absolute_path}."
+ cmd = %W(git fetch #{name} --prune --quiet)
+ cmd << '--force' if force
+ cmd << tags_option
+
+ 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."
+ end
+
+ success
+ end
+ end
+
+ def push_branches(remote_name, timeout, force, branch_names)
+ logger.info "Pushing branches from #{repository_absolute_path} to remote #{remote_name}: #{branch_names}"
+ cmd = %w(git push)
+ cmd << '--force' if force
+ cmd += %W(-- #{remote_name}).concat(branch_names)
+
+ success = run_with_timeout(cmd, timeout, repository_absolute_path)
+
+ unless success
+ logger.error("Pushing branches to remote #{remote_name} failed.")
+ end
+
+ success
+ end
+
+ def delete_remote_branches(remote_name, branch_names)
+ branches = branch_names.map { |branch_name| ":#{branch_name}" }
+
+ logger.info "Pushing deleted branches from #{repository_absolute_path} to remote #{remote_name}: #{branch_names}"
+ cmd = %W(git push -- #{remote_name}).concat(branches)
+
+ success = run(cmd, repository_absolute_path)
+
+ unless success
+ logger.error("Pushing deleted branches to remote #{remote_name} failed.")
+ end
+
+ success
+ end
+
+ protected
+
+ def run(*args)
+ output, exitstatus = popen(*args)
+ @output << output
+
+ exitstatus&.zero?
+ end
+
+ def run_with_timeout(*args)
+ output, exitstatus = popen_with_timeout(*args)
+ @output << output
+
+ exitstatus&.zero?
+ rescue Timeout::Error
+ @output.puts('Timed out')
+
+ false
+ end
+
+ def mask_password_in_url(url)
+ result = URI(url)
+ result.password = "*****" unless result.password.nil?
+ result.user = "*****" unless result.user.nil? # it's needed for oauth access_token
+ result
+ rescue
+ url
+ end
+
+ def remove_origin_in_repo
+ cmd = %w(git remote rm origin)
+ run(cmd, repository_absolute_path)
+ end
+
+ # Builds a small shell script that can be used to execute SSH with a set of
+ # custom options.
+ #
+ # Options are expanded as `'-oKey="Value"'`, so SSH will correctly interpret
+ # paths with spaces in them. We trust the user not to embed single or double
+ # quotes in the key or value.
+ def custom_ssh_script(options = {})
+ args = options.map { |k, v| %Q{'-o#{k}="#{v}"'} }.join(' ')
+
+ [
+ "#!/bin/sh",
+ "exec ssh #{args} \"$@\""
+ ].join("\n")
+ end
+
+ # Known hosts data and private keys can be passed to gitlab-shell in the
+ # environment. If present, this method puts them into temporary files, writes
+ # a script that can substitute as `ssh`, setting the options to respect those
+ # files, and yields: { "GIT_SSH" => "/tmp/myScript" }
+ def setup_ssh_auth(key, known_hosts)
+ options = {}
+
+ if key
+ key_file = Tempfile.new('gitlab-shell-key-file')
+ key_file.chmod(0o400)
+ key_file.write(key)
+ key_file.close
+
+ options['IdentityFile'] = key_file.path
+ options['IdentitiesOnly'] = 'yes'
+ end
+
+ if known_hosts
+ known_hosts_file = Tempfile.new('gitlab-shell-known-hosts')
+ known_hosts_file.chmod(0o400)
+ known_hosts_file.write(known_hosts)
+ known_hosts_file.close
+
+ options['StrictHostKeyChecking'] = 'yes'
+ options['UserKnownHostsFile'] = known_hosts_file.path
+ end
+
+ return yield({}) if options.empty?
+
+ script = Tempfile.new('gitlab-shell-ssh-wrapper')
+ script.chmod(0o755)
+ script.write(custom_ssh_script(options))
+ script.close
+
+ yield('GIT_SSH' => script.path)
+ ensure
+ key_file&.close!
+ known_hosts_file&.close!
+ script&.close!
+ end
+
+ private
+
+ def shard_name
+ strong_memoize(:shard_name) do
+ shard_name_from_shard_path(shard_path)
+ end
+ end
+
+ def shard_name_from_shard_path(shard_path)
+ Gitlab.config.repositories.storages.find { |_, info| info['path'] == shard_path }&.first ||
+ raise(ShardNameNotFoundError, "no shard found for path '#{shard_path}'")
+ end
+
+ def git_fork_repository(new_shard_path, new_repository_relative_path)
+ from_path = repository_absolute_path
+ to_path = File.join(new_shard_path, new_repository_relative_path)
+
+ # The repository cannot already exist
+ if File.exist?(to_path)
+ logger.error "fork-repository failed: destination repository <#{to_path}> already exists."
+ return false
+ end
+
+ # Ensure the namepsace / hashed storage directory exists
+ FileUtils.mkdir_p(File.dirname(to_path), mode: 0770)
+
+ logger.info "Forking repository from <#{from_path}> to <#{to_path}>."
+ cmd = %W(git clone --bare --no-local -- #{from_path} #{to_path})
+
+ run(cmd, nil) && Gitlab::Git::Repository.create_hooks(to_path, global_hooks_path)
+ end
+
+ def gitaly_fork_repository(new_shard_path, new_repository_relative_path)
+ target_repository = Gitlab::Git::Repository.new(shard_name_from_shard_path(new_shard_path), 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/index.rb b/lib/gitlab/git/index.rb
index db532600d1b..d94082a3e30 100644
--- a/lib/gitlab/git/index.rb
+++ b/lib/gitlab/git/index.rb
@@ -10,6 +10,7 @@ module Gitlab
DEFAULT_MODE = 0o100644
ACTIONS = %w(create create_dir update move delete).freeze
+ ACTION_OPTIONS = %i(file_path previous_path content encoding).freeze
attr_reader :repository, :raw_index
@@ -20,6 +21,11 @@ module Gitlab
delegate :read_tree, :get, to: :raw_index
+ def apply(action, options)
+ validate_action!(action)
+ public_send(action, options.slice(*ACTION_OPTIONS)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def write_tree
raw_index.write_tree(repository.rugged)
end
@@ -140,6 +146,12 @@ module Gitlab
rescue Rugged::IndexError => e
raise IndexError, e.message
end
+
+ def validate_action!(action)
+ unless ACTIONS.include?(action.to_s)
+ raise ArgumentError, "Unknown action '#{action}'"
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index 7e8fe173056..3fb0e2eed93 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -97,6 +97,11 @@ module Gitlab
end
end
+ def update_branch(branch_name, newrev, oldrev)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
private
# Returns [newrev, should_run_after_create, should_run_after_create_branch]
@@ -126,7 +131,7 @@ module Gitlab
oldrev = branch.target
- if oldrev == repository.rugged.merge_base(newrev, branch.target)
+ if oldrev == repository.merge_base(newrev, branch.target)
oldrev
else
raise Gitlab::Git::CommitError.new('Branch diverged')
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index d41fe78daa1..1ccca13ce2f 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -16,8 +16,8 @@ module Gitlab
vars['PWD'] = path
options = { chdir: path }
- @cmd_output = ""
- @cmd_status = 0
+ cmd_output = ""
+ cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given?
stdin.close
@@ -25,14 +25,14 @@ module Gitlab
if lazy_block
return lazy_block.call(stdout.lazy)
else
- @cmd_output << stdout.read
+ cmd_output << stdout.read
end
- @cmd_output << stderr.read
- @cmd_status = wait_thr.value.exitstatus
+ cmd_output << stderr.read
+ cmd_status = wait_thr.value.exitstatus
end
- [@cmd_output, @cmd_status]
+ [cmd_output, cmd_status]
end
def popen_with_timeout(cmd, timeout, path, vars = {})
diff --git a/lib/gitlab/git/remote_mirror.rb b/lib/gitlab/git/remote_mirror.rb
new file mode 100644
index 00000000000..38e9d2a8554
--- /dev/null
+++ b/lib/gitlab/git/remote_mirror.rb
@@ -0,0 +1,75 @@
+module Gitlab
+ module Git
+ class RemoteMirror
+ def initialize(repository, ref_name)
+ @repository = repository
+ @ref_name = ref_name
+ end
+
+ def update(only_branches_matching: [], only_tags_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, only_refs_matching: only_tags_matching)
+ remote_tags = refs_obj(@repository.remote_tags(@ref_name), only_refs_matching: only_tags_matching)
+
+ 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
+
+ private
+
+ 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)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 73889328f36..283134e043e 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -20,6 +20,8 @@ module Gitlab
SEARCH_CONTEXT_LINES = 3
REBASE_WORKTREE_PREFIX = 'rebase'.freeze
SQUASH_WORKTREE_PREFIX = 'squash'.freeze
+ GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
+ GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
NoRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError)
@@ -38,10 +40,32 @@ module Gitlab
repo = Rugged::Repository.init_at(repo_path, bare)
repo.close
- if symlink_hooks_to.present?
- hooks_path = File.join(repo_path, 'hooks')
- FileUtils.rm_rf(hooks_path)
- FileUtils.ln_s(symlink_hooks_to, hooks_path)
+ create_hooks(repo_path, symlink_hooks_to) if symlink_hooks_to.present?
+
+ true
+ end
+
+ def create_hooks(repo_path, global_hooks_path)
+ local_hooks_path = File.join(repo_path, 'hooks')
+ real_local_hooks_path = :not_found
+
+ begin
+ real_local_hooks_path = File.realpath(local_hooks_path)
+ rescue Errno::ENOENT
+ # real_local_hooks_path == :not_found
+ end
+
+ # Do nothing if hooks already exist
+ unless real_local_hooks_path == File.realpath(global_hooks_path)
+ if File.exist?(local_hooks_path)
+ # Move the existing hooks somewhere safe
+ FileUtils.mv(
+ local_hooks_path,
+ "#{local_hooks_path}.old.#{Time.now.to_i}")
+ end
+
+ # Create the hooks symlink
+ FileUtils.ln_sf(global_hooks_path, local_hooks_path)
end
true
@@ -60,7 +84,7 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
- attr_reader :storage, :gl_repository, :relative_path
+ attr_reader :gitlab_projects, :storage, :gl_repository, :relative_path
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
@@ -70,6 +94,12 @@ module Gitlab
@gl_repository = gl_repository
storage_path = Gitlab.config.repositories.storages[@storage]['path']
+ @gitlab_projects = Gitlab::Git::GitlabProjects.new(
+ storage_path,
+ relative_path,
+ global_hooks_path: Gitlab.config.gitlab_shell.hooks_path,
+ logger: Rails.logger
+ )
@path = File.join(storage_path, @relative_path)
@name = @relative_path.split("/").last
@attributes = Gitlab::Git::Attributes.new(path)
@@ -103,7 +133,7 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
+ Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
if enabled
gitaly_repository_client.exists?
else
@@ -165,7 +195,7 @@ module Gitlab
end
def local_branches(sort_by: nil)
- gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ gitaly_migrate(:local_branches) do |is_enabled|
if is_enabled
gitaly_ref_client.local_branches(sort_by: sort_by)
else
@@ -468,11 +498,13 @@ module Gitlab
end
def count_commits(options)
+ count_commits_options = process_count_commits_options(options)
+
gitaly_migrate(:count_commits) do |is_enabled|
if is_enabled
- count_commits_by_gitaly(options)
+ count_commits_by_gitaly(count_commits_options)
else
- count_commits_by_shelling_out(options)
+ count_commits_by_shelling_out(count_commits_options)
end
end
end
@@ -510,14 +542,21 @@ module Gitlab
end
# Counts the amount of commits between `from` and `to`.
- def count_commits_between(from, to)
- count_commits(ref: "#{from}..#{to}")
+ def count_commits_between(from, to, options = {})
+ count_commits(from: from, to: to, **options)
end
# Returns the SHA of the most recent common ancestor of +from+ and +to+
def merge_base_commit(from, to)
- rugged.merge_base(from, to)
+ gitaly_migrate(:merge_base) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.find_merge_base(from, to)
+ else
+ rugged.merge_base(from, to)
+ end
+ end
end
+ alias_method :merge_base, :merge_base_commit
# Gitaly note: JV: check gitlab-ee before removing this method.
def rugged_is_ancestor?(ancestor_id, descendant_id)
@@ -887,30 +926,25 @@ module Gitlab
end
end
- def add_remote(remote_name, url)
- rugged.remotes.create(remote_name, url)
- rescue Rugged::ConfigError
- remote_update(remote_name, url: url)
+ # If `mirror_refmap` is present the remote is set as mirror with that mapping
+ def add_remote(remote_name, url, mirror_refmap: nil)
+ gitaly_migrate(:remote_add_remote) do |is_enabled|
+ if is_enabled
+ gitaly_remote_client.add_remote(remote_name, url, mirror_refmap)
+ else
+ rugged_add_remote(remote_name, url, mirror_refmap)
+ end
+ end
end
def remove_remote(remote_name)
- # When a remote is deleted all its remote refs are deleted too, but in
- # the case of mirrors we map its refs (that would usualy go under
- # [remote_name]/) to the top level namespace. We clean the mapping so
- # those don't get deleted.
- if rugged.config["remote.#{remote_name}.mirror"]
- rugged.config.delete("remote.#{remote_name}.fetch")
+ gitaly_migrate(:remote_remove_remote) do |is_enabled|
+ if is_enabled
+ gitaly_remote_client.remove_remote(remote_name)
+ else
+ rugged_remove_remote(remote_name)
+ end
end
-
- rugged.remotes.delete(remote_name)
- true
- rescue Rugged::ConfigError
- false
- end
-
- # Returns true if a remote exists.
- def remote_exists?(name)
- rugged.remotes[name].present?
end
# Update the specified remote using the values in the +options+ hash
@@ -1128,12 +1162,14 @@ module Gitlab
!has_visible_content?
end
- # Like all public `Gitlab::Git::Repository` methods, this method is part
- # of `Repository`'s interface through `method_missing`.
- # `Repository` has its own `fetch_remote` which uses `gitlab-shell` and
- # takes some extra attributes, so we qualify this method name to prevent confusion.
- def fetch_remote_without_shell(remote = 'origin')
- run_git(['fetch', remote]).last.zero?
+ def fetch_repository_as_mirror(repository)
+ gitaly_migrate(:remote_fetch_internal_remote) do |is_enabled|
+ if is_enabled
+ gitaly_remote_client.fetch_internal_remote(repository)
+ else
+ rugged_fetch_repository_as_mirror(repository)
+ end
+ end
end
def blob_at(sha, path)
@@ -1141,7 +1177,7 @@ module Gitlab
end
# Items should be of format [[commit_id, path], [commit_id1, path1]]
- def batch_blobs(items, blob_size_limit: nil)
+ def batch_blobs(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
end
@@ -1175,9 +1211,16 @@ module Gitlab
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_repository.path} #{remote_branch}),
+ %W(pull --rebase #{remote_repo_path} #{remote_branch}),
chdir: rebase_path, env: env
)
@@ -1229,6 +1272,60 @@ module Gitlab
fresh_worktree?(worktree_path(SQUASH_WORKTREE_PREFIX, squash_id))
end
+ def push_remote_branches(remote_name, branch_names, forced: true)
+ success = @gitlab_projects.push_branches(remote_name, GITLAB_PROJECTS_TIMEOUT, forced, branch_names)
+
+ success || gitlab_projects_error
+ end
+
+ def delete_remote_branches(remote_name, branch_names)
+ success = @gitlab_projects.delete_remote_branches(remote_name, branch_names)
+
+ success || gitlab_projects_error
+ end
+
+ def delete_remote_branches(remote_name, branch_names)
+ success = @gitlab_projects.delete_remote_branches(remote_name, branch_names)
+
+ success || gitlab_projects_error
+ 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)
+
+ 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
+ # rubocop:enable Metrics/ParameterLists
+
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end
@@ -1253,6 +1350,14 @@ module Gitlab
@gitaly_operation_client ||= Gitlab::GitalyClient::OperationService.new(self)
end
+ def gitaly_remote_client
+ @gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self)
+ end
+
+ def gitaly_conflicts_client(our_commit_oid, their_commit_oid)
+ Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid)
+ end
+
def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
@@ -1391,6 +1496,26 @@ module Gitlab
end
end
+ def process_count_commits_options(options)
+ if options[:from] || options[:to]
+ ref =
+ if options[:left_right] # Compare with merge-base for left-right
+ "#{options[:from]}...#{options[:to]}"
+ else
+ "#{options[:from]}..#{options[:to]}"
+ end
+
+ options.merge(ref: ref)
+
+ elsif options[:ref] && options[:left_right]
+ from, to = options[:ref].match(/\A([^\.]*)\.{2,3}([^\.]*)\z/)[1..2]
+
+ options.merge(from: from, to: to)
+ else
+ options
+ end
+ end
+
def log_using_shell?(options)
options[:path].present? ||
options[:disable_walk] ||
@@ -1613,19 +1738,59 @@ module Gitlab
end
def count_commits_by_gitaly(options)
- gitaly_commit_client.commit_count(options[:ref], 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 = IO.popen(cmd) { |io| io.read }
+
+ process_count_commits_raw_output(raw_output, options)
+ end
+
+ def count_commits_shelling_command(options)
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} 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 += %W[--count #{options[:ref]}]
cmd += %W[-- #{options[:path]}] if options[:path].present?
+ cmd
+ end
- raw_output = IO.popen(cmd) { |io| io.read }
+ 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]))
- raw_output.to_i
+ 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)
@@ -1851,7 +2016,7 @@ module Gitlab
end
def gitaly_fetch_ref(source_repository, source_ref:, target_ref:)
- args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref})
+ args = %W(fetch --no-tags -f #{GITALY_INTERNAL_URL} #{source_ref}:#{target_ref})
run_git(args, env: source_repository.fetch_env)
end
@@ -1871,6 +2036,47 @@ module Gitlab
rescue Rugged::ReferenceError
raise ArgumentError, 'Invalid merge source'
end
+
+ def rugged_add_remote(remote_name, url, mirror_refmap)
+ rugged.remotes.create(remote_name, url)
+
+ set_remote_as_mirror(remote_name, refmap: mirror_refmap) if mirror_refmap
+ rescue Rugged::ConfigError
+ remote_update(remote_name, url: url)
+ end
+
+ def rugged_remove_remote(remote_name)
+ # When a remote is deleted all its remote refs are deleted too, but in
+ # the case of mirrors we map its refs (that would usualy go under
+ # [remote_name]/) to the top level namespace. We clean the mapping so
+ # those don't get deleted.
+ if rugged.config["remote.#{remote_name}.mirror"]
+ rugged.config.delete("remote.#{remote_name}.fetch")
+ end
+
+ rugged.remotes.delete(remote_name)
+ true
+ rescue Rugged::ConfigError
+ false
+ end
+
+ def rugged_fetch_repository_as_mirror(repository)
+ remote_name = "tmp-#{SecureRandom.hex}"
+ repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository)
+
+ add_remote(remote_name, GITALY_INTERNAL_URL, mirror_refmap: :all_refs)
+ fetch_remote(remote_name, env: repository.fetch_env)
+ ensure
+ remove_remote(remote_name)
+ end
+
+ def fetch_remote(remote_name = 'origin', env: nil)
+ run_git(['fetch', remote_name], env: env).last.zero?
+ end
+
+ def gitlab_projects_error
+ raise CommandError, @gitlab_projects.output
+ end
end
end
end
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index 392bef69e80..effb1f0ca19 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -17,33 +17,6 @@ module Gitlab
rugged.config["remote.#{remote_name}.prune"] = true
end
- def set_remote_refmap(remote_name, refmap)
- Array(refmap).each_with_index do |refspec, i|
- refspec = REFMAPS[refspec] || refspec
-
- # We need multiple `fetch` entries, but Rugged only allows replacing a config, not adding to it.
- # To make sure we start from scratch, we set the first using rugged, and use `git` for any others
- if i == 0
- rugged.config["remote.#{remote_name}.fetch"] = refspec
- else
- run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
- end
- end
- end
-
- # Like all_refs public `Gitlab::Git::Repository` methods, this method is part
- # of `Repository`'s interface through `method_missing`.
- # `Repository` has its own `fetch_as_mirror` which uses `gitlab-shell` and
- # takes some extra attributes, so we qualify this method name to prevent confusion.
- def fetch_as_mirror_without_shell(url)
- remote_name = "tmp-#{SecureRandom.hex}"
- add_remote(remote_name, url)
- set_remote_as_mirror(remote_name)
- fetch_remote_without_shell(remote_name)
- ensure
- remove_remote(remote_name) if remote_name
- end
-
def remote_tags(remote)
# Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n"
# We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...]
@@ -85,6 +58,20 @@ module Gitlab
private
+ def set_remote_refmap(remote_name, refmap)
+ Array(refmap).each_with_index do |refspec, i|
+ refspec = REFMAPS[refspec] || refspec
+
+ # We need multiple `fetch` entries, but Rugged only allows replacing a config, not adding to it.
+ # To make sure we start from scratch, we set the first using rugged, and use `git` for any others
+ if i == 0
+ rugged.config["remote.#{remote_name}.fetch"] = refspec
+ else
+ run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
+ end
+ end
+ end
+
def list_remote_tags(remote)
tag_list, exit_code, error = nil
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote})
diff --git a/lib/gitlab/git/storage/checker.rb b/lib/gitlab/git/storage/checker.rb
index de63cb4b40c..d3c37f82101 100644
--- a/lib/gitlab/git/storage/checker.rb
+++ b/lib/gitlab/git/storage/checker.rb
@@ -5,6 +5,8 @@ module Gitlab
include CircuitBreakerSettings
attr_reader :storage_path, :storage, :hostname, :logger
+ METRICS_MUTEX = Mutex.new
+ STORAGE_TIMING_BUCKETS = [0.1, 0.15, 0.25, 0.33, 0.5, 1, 1.5, 2.5, 5, 10, 15].freeze
def self.check_all(logger = Rails.logger)
threads = Gitlab.config.repositories.storages.keys.map do |storage_name|
@@ -19,6 +21,17 @@ module Gitlab
end
end
+ def self.check_histogram
+ @check_histogram ||=
+ METRICS_MUTEX.synchronize do
+ @check_histogram || Gitlab::Metrics.histogram(:circuitbreaker_storage_check_duration_seconds,
+ 'Storage check time in seconds',
+ {},
+ STORAGE_TIMING_BUCKETS
+ )
+ end
+ end
+
def initialize(storage, logger = Rails.logger)
@storage = storage
config = Gitlab.config.repositories.storages[@storage]
@@ -45,7 +58,7 @@ module Gitlab
end
def check
- if Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries)
+ if perform_access_check
track_storage_accessible
true
else
@@ -57,6 +70,15 @@ module Gitlab
private
+ def perform_access_check
+ start_time = Gitlab::Metrics::System.monotonic_time
+
+ Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries)
+ ensure
+ execution_time = Gitlab::Metrics::System.monotonic_time - start_time
+ self.class.check_histogram.observe({ storage: storage }, execution_time)
+ end
+
def track_storage_inaccessible
first_failure = current_failure_info.first_failure || Time.now
last_failure = Time.now
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 9d7d921bb9c..56f6febe86d 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -102,18 +102,15 @@ module Gitlab
end
def check_project_moved!
- return unless redirected_path
+ return if redirected_path.nil?
- url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
- message = <<-MESSAGE.strip_heredoc
- Project '#{redirected_path}' was moved to '#{project.full_path}'.
+ project_moved = Checks::ProjectMoved.new(project, user, redirected_path, protocol)
- Please update your Git remote and try again:
-
- git remote set-url origin #{url}
- MESSAGE
-
- raise ProjectMovedError, message
+ if project_moved.permanent_redirect?
+ project_moved.add_redirect_message
+ else
+ raise ProjectMovedError, project_moved.redirect_message(rejected: true)
+ end
end
def check_command_disabled!(cmd)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index b753ac46291..4507ea923b4 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -330,22 +330,6 @@ module Gitlab
Google::Protobuf::Timestamp.new(seconds: t.to_i)
end
- def self.encode(s)
- return "" if s.nil?
-
- s.dup.force_encoding(Encoding::ASCII_8BIT)
- end
-
- def self.binary_stringio(s)
- io = StringIO.new(s || '')
- io.set_encoding(Encoding::ASCII_8BIT)
- io
- end
-
- def self.encode_repeated(a)
- Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } )
- end
-
# The default timeout on all Gitaly calls
def self.default_timeout
return 0 if Sidekiq.server?
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 7985f5b5457..fed05bb6c64 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -1,6 +1,8 @@
module Gitlab
module GitalyClient
class CommitService
+ include Gitlab::EncodingHelper
+
# The ID of empty tree.
# See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
@@ -13,7 +15,7 @@ module Gitlab
def ls_files(revision)
request = Gitaly::ListFilesRequest.new(
repository: @gitaly_repo,
- revision: GitalyClient.encode(revision)
+ revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request, timeout: GitalyClient.medium_timeout)
@@ -73,7 +75,7 @@ module Gitlab
request = Gitaly::TreeEntryRequest.new(
repository: @gitaly_repo,
revision: ref,
- path: GitalyClient.encode(path),
+ path: encode_binary(path),
limit: limit.to_i
)
@@ -98,8 +100,8 @@ module Gitlab
def tree_entries(repository, revision, path)
request = Gitaly::GetTreeEntriesRequest.new(
repository: @gitaly_repo,
- revision: GitalyClient.encode(revision),
- path: path.present? ? GitalyClient.encode(path) : '.'
+ revision: encode_binary(revision),
+ path: path.present? ? encode_binary(path) : '.'
)
response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout)
@@ -112,8 +114,8 @@ module Gitlab
type: gitaly_tree_entry.type.downcase,
mode: gitaly_tree_entry.mode.to_s(8),
name: File.basename(gitaly_tree_entry.path),
- path: GitalyClient.encode(gitaly_tree_entry.path),
- flat_path: GitalyClient.encode(gitaly_tree_entry.flat_path),
+ path: encode_binary(gitaly_tree_entry.path),
+ flat_path: encode_binary(gitaly_tree_entry.flat_path),
commit_id: gitaly_tree_entry.commit_oid
)
end
@@ -128,6 +130,7 @@ module Gitlab
request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present?
request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
request.path = options[:path] if options[:path].present?
+ request.max_count = options[:max_count] if options[:max_count].present?
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count
end
@@ -135,8 +138,8 @@ module Gitlab
def last_commit_for_path(revision, path)
request = Gitaly::LastCommitForPathRequest.new(
repository: @gitaly_repo,
- revision: GitalyClient.encode(revision),
- path: GitalyClient.encode(path.to_s)
+ revision: encode_binary(revision),
+ path: encode_binary(path.to_s)
)
gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request, timeout: GitalyClient.fast_timeout).commit
@@ -169,6 +172,15 @@ module Gitlab
consume_commits_response(response)
end
+ def list_commits_by_oid(oids)
+ 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)
+ consume_commits_response(response)
+ rescue GRPC::Unknown # If no repository is found, happens mainly during testing
+ []
+ end
+
def commits_by_message(query, revision: '', path: '', limit: 1000, offset: 0)
request = Gitaly::CommitsByMessageRequest.new(
repository: @gitaly_repo,
@@ -193,8 +205,8 @@ module Gitlab
def raw_blame(revision, path)
request = Gitaly::RawBlameRequest.new(
repository: @gitaly_repo,
- revision: GitalyClient.encode(revision),
- path: GitalyClient.encode(path)
+ revision: encode_binary(revision),
+ path: encode_binary(path)
)
response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
@@ -204,7 +216,7 @@ module Gitlab
def find_commit(revision)
request = Gitaly::FindCommitRequest.new(
repository: @gitaly_repo,
- revision: GitalyClient.encode(revision)
+ revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
@@ -215,7 +227,7 @@ module Gitlab
def patch(revision)
request = Gitaly::CommitPatchRequest.new(
repository: @gitaly_repo,
- revision: GitalyClient.encode(revision)
+ revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request, timeout: GitalyClient.medium_timeout)
@@ -225,7 +237,7 @@ module Gitlab
def commit_stats(revision)
request = Gitaly::CommitStatsRequest.new(
repository: @gitaly_repo,
- revision: GitalyClient.encode(revision)
+ revision: encode_binary(revision)
)
GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request, timeout: GitalyClient.medium_timeout)
end
@@ -241,9 +253,9 @@ module Gitlab
)
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before]
- request.revision = GitalyClient.encode(options[:ref]) if options[:ref]
+ request.revision = encode_binary(options[:ref]) if options[:ref]
- request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present?
+ request.paths = encode_repeated(Array(options[:path])) if options[:path].present?
response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout)
@@ -255,7 +267,7 @@ module Gitlab
enum = Enumerator.new do |y|
shas.each_slice(20) do |revs|
- request.shas = GitalyClient.encode_repeated(revs)
+ request.shas = encode_repeated(revs)
y.yield request
@@ -294,7 +306,7 @@ module Gitlab
repository: @gitaly_repo,
left_commit_id: from_id,
right_commit_id: to_id,
- paths: options.fetch(:paths, []).compact.map { |path| GitalyClient.encode(path) }
+ paths: options.fetch(:paths, []).compact.map { |path| encode_binary(path) }
}
end
@@ -305,6 +317,10 @@ module Gitlab
end
end
end
+
+ def encode_repeated(a)
+ Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } )
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/conflicts_service.rb b/lib/gitlab/gitaly_client/conflicts_service.rb
new file mode 100644
index 00000000000..40f032cf873
--- /dev/null
+++ b/lib/gitlab/gitaly_client/conflicts_service.rb
@@ -0,0 +1,95 @@
+module Gitlab
+ module GitalyClient
+ class ConflictsService
+ include Gitlab::EncodingHelper
+
+ MAX_MSG_SIZE = 128.kilobytes.freeze
+
+ def initialize(repository, our_commit_oid, their_commit_oid)
+ @gitaly_repo = repository.gitaly_repository
+ @repository = repository
+ @our_commit_oid = our_commit_oid
+ @their_commit_oid = their_commit_oid
+ end
+
+ def list_conflict_files
+ request = Gitaly::ListConflictFilesRequest.new(
+ repository: @gitaly_repo,
+ our_commit_oid: @our_commit_oid,
+ their_commit_oid: @their_commit_oid
+ )
+ response = GitalyClient.call(@repository.storage, :conflicts_service, :list_conflict_files, request)
+
+ files_from_response(response).to_a
+ end
+
+ def resolve_conflicts(target_repository, resolution, source_branch, target_branch)
+ reader = binary_stringio(resolution.files.to_json)
+
+ req_enum = Enumerator.new do |y|
+ header = resolve_conflicts_request_header(target_repository, resolution, source_branch, target_branch)
+ y.yield Gitaly::ResolveConflictsRequest.new(header: header)
+
+ until reader.eof?
+ chunk = reader.read(MAX_MSG_SIZE)
+
+ y.yield Gitaly::ResolveConflictsRequest.new(files_json: chunk)
+ end
+ end
+
+ response = GitalyClient.call(@repository.storage, :conflicts_service, :resolve_conflicts, req_enum, remote_storage: target_repository.storage)
+
+ if response.resolution_error.present?
+ raise Gitlab::Git::Conflict::Resolver::ResolutionError, response.resolution_error
+ end
+ end
+
+ private
+
+ def resolve_conflicts_request_header(target_repository, resolution, source_branch, target_branch)
+ Gitaly::ResolveConflictsRequestHeader.new(
+ repository: @gitaly_repo,
+ our_commit_oid: @our_commit_oid,
+ target_repository: target_repository.gitaly_repository,
+ their_commit_oid: @their_commit_oid,
+ source_branch: source_branch,
+ target_branch: target_branch,
+ commit_message: resolution.commit_message,
+ user: Gitlab::Git::User.from_gitlab(resolution.user).to_gitaly
+ )
+ end
+
+ def files_from_response(response)
+ files = []
+
+ response.each do |msg|
+ msg.files.each do |gitaly_file|
+ if gitaly_file.header
+ files << file_from_gitaly_header(gitaly_file.header)
+ else
+ files.last.content << gitaly_file.content
+ end
+ end
+ end
+
+ files
+ end
+
+ def file_from_gitaly_header(header)
+ Gitlab::Git::Conflict::File.new(
+ Gitlab::GitalyClient::Util.git_repository(header.repository),
+ header.commit_oid,
+ conflict_from_gitaly_file_header(header),
+ ''
+ )
+ end
+
+ def conflict_from_gitaly_file_header(header)
+ {
+ ours: { path: header.our_path, mode: header.our_mode },
+ theirs: { path: header.their_path }
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 400a4af363b..ae1753ff0ae 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -1,6 +1,8 @@
module Gitlab
module GitalyClient
class OperationService
+ include Gitlab::EncodingHelper
+
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@repository = repository
@@ -9,7 +11,7 @@ module Gitlab
def rm_tag(tag_name, user)
request = Gitaly::UserDeleteTagRequest.new(
repository: @gitaly_repo,
- tag_name: GitalyClient.encode(tag_name),
+ tag_name: encode_binary(tag_name),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly
)
@@ -24,9 +26,9 @@ module Gitlab
request = Gitaly::UserCreateTagRequest.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
- tag_name: GitalyClient.encode(tag_name),
- target_revision: GitalyClient.encode(target),
- message: GitalyClient.encode(message.to_s)
+ tag_name: encode_binary(tag_name),
+ target_revision: encode_binary(target),
+ message: encode_binary(message.to_s)
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request)
@@ -44,9 +46,9 @@ module Gitlab
def user_create_branch(branch_name, user, start_point)
request = Gitaly::UserCreateBranchRequest.new(
repository: @gitaly_repo,
- branch_name: GitalyClient.encode(branch_name),
+ branch_name: encode_binary(branch_name),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
- start_point: GitalyClient.encode(start_point)
+ start_point: encode_binary(start_point)
)
response = GitalyClient.call(@repository.storage, :operation_service,
:user_create_branch, request)
@@ -64,7 +66,7 @@ module Gitlab
def user_delete_branch(branch_name, user)
request = Gitaly::UserDeleteBranchRequest.new(
repository: @gitaly_repo,
- branch_name: GitalyClient.encode(branch_name),
+ branch_name: encode_binary(branch_name),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly
)
@@ -89,8 +91,8 @@ module Gitlab
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit_id: source_sha,
- branch: GitalyClient.encode(target_branch),
- message: GitalyClient.encode(message)
+ branch: encode_binary(target_branch),
+ message: encode_binary(message)
)
)
@@ -99,6 +101,7 @@ module Gitlab
request_enum.push(Gitaly::UserMergeBranchRequest.new(apply: true))
branch_update = response_enum.next.branch_update
+ return if branch_update.nil?
raise Gitlab::Git::CommitError.new('failed to apply merge to branch') unless branch_update.commit_id.present?
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
@@ -111,7 +114,7 @@ module Gitlab
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit_id: source_sha,
- branch: GitalyClient.encode(target_branch)
+ branch: encode_binary(target_branch)
)
branch_update = GitalyClient.call(
@@ -152,9 +155,9 @@ module Gitlab
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit: commit.to_gitaly_commit,
- branch_name: GitalyClient.encode(branch_name),
- message: GitalyClient.encode(message),
- start_branch_name: GitalyClient.encode(start_branch_name.to_s),
+ branch_name: encode_binary(branch_name),
+ message: encode_binary(message),
+ start_branch_name: encode_binary(start_branch_name.to_s),
start_repository: start_repository.gitaly_repository
)
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 066e4e183c0..5bce1009878 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -72,7 +72,7 @@ module Gitlab
end
def ref_exists?(ref_name)
- request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: GitalyClient.encode(ref_name))
+ request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: encode_binary(ref_name))
response = GitalyClient.call(@storage, :ref_service, :ref_exists, request)
response.value
rescue GRPC::InvalidArgument => e
@@ -82,7 +82,7 @@ module Gitlab
def find_branch(branch_name)
request = Gitaly::FindBranchRequest.new(
repository: @gitaly_repo,
- name: GitalyClient.encode(branch_name)
+ name: encode_binary(branch_name)
)
response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request)
@@ -96,8 +96,8 @@ module Gitlab
def create_branch(ref, start_point)
request = Gitaly::CreateBranchRequest.new(
repository: @gitaly_repo,
- name: GitalyClient.encode(ref),
- start_point: GitalyClient.encode(start_point)
+ name: encode_binary(ref),
+ start_point: encode_binary(start_point)
)
response = GitalyClient.call(@repository.storage, :ref_service, :create_branch, request)
@@ -121,7 +121,7 @@ module Gitlab
def delete_branch(branch_name)
request = Gitaly::DeleteBranchRequest.new(
repository: @gitaly_repo,
- name: GitalyClient.encode(branch_name)
+ name: encode_binary(branch_name)
)
GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request)
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
new file mode 100644
index 00000000000..559a901b9a3
--- /dev/null
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module GitalyClient
+ class RemoteService
+ def initialize(repository)
+ @repository = repository
+ @gitaly_repo = repository.gitaly_repository
+ @storage = repository.storage
+ end
+
+ def add_remote(name, url, mirror_refmap)
+ request = Gitaly::AddRemoteRequest.new(
+ repository: @gitaly_repo, name: name, url: url,
+ mirror_refmap: mirror_refmap.to_s
+ )
+
+ GitalyClient.call(@storage, :remote_service, :add_remote, request)
+ end
+
+ def remove_remote(name)
+ request = Gitaly::RemoveRemoteRequest.new(repository: @gitaly_repo, name: name)
+
+ response = GitalyClient.call(@storage, :remote_service, :remove_remote, request)
+
+ response.result
+ end
+
+ def fetch_internal_remote(repository)
+ request = Gitaly::FetchInternalRemoteRequest.new(
+ repository: @gitaly_repo,
+ remote_repository: repository.gitaly_repository
+ )
+
+ response = GitalyClient.call(@storage, :remote_service,
+ :fetch_internal_remote, request,
+ remote_storage: repository.storage)
+
+ response.result
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index a477d618f63..d43d80da960 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -1,6 +1,8 @@
module Gitlab
module GitalyClient
class RepositoryService
+ include Gitlab::EncodingHelper
+
def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository
@@ -69,6 +71,32 @@ module Gitlab
response.value
end
+ def find_merge_base(*revisions)
+ request = Gitaly::FindMergeBaseRequest.new(
+ repository: @gitaly_repo,
+ revisions: revisions.map { |r| encode_binary(r) }
+ )
+
+ response = GitalyClient.call(@storage, :repository_service, :find_merge_base, request)
+ response.base.presence
+ end
+
+ def fork_repository(source_repository)
+ request = Gitaly::CreateForkRequest.new(
+ repository: @gitaly_repo,
+ source_repository: source_repository.gitaly_repository
+ )
+
+ GitalyClient.call(
+ @storage,
+ :repository_service,
+ :create_fork,
+ request,
+ remote_storage: source_repository.storage,
+ timeout: GitalyClient.default_timeout
+ )
+ end
+
def fetch_source_branch(source_repository, source_branch, local_ref)
request = Gitaly::FetchSourceBranchRequest.new(
repository: @gitaly_repo,
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index b1a033280b4..a8c6d478de8 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -12,12 +12,18 @@ module Gitlab
Gitaly::Repository.new(
storage_name: repository_storage,
relative_path: relative_path,
- gl_repository: gl_repository,
+ gl_repository: gl_repository.to_s,
git_object_directory: git_object_directory.to_s,
git_alternate_object_directories: git_alternate_object_directories
)
end
+ def git_repository(gitaly_repository)
+ Gitlab::Git::Repository.new(gitaly_repository.storage_name,
+ gitaly_repository.relative_path,
+ gitaly_repository.gl_repository)
+ end
+
def gitlab_tag_from_gitaly_tag(repository, gitaly_tag)
if gitaly_tag.target_commit.present?
commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit)
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 337d225d081..5c5b170a3e0 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -3,6 +3,8 @@ require 'stringio'
module Gitlab
module GitalyClient
class WikiService
+ include Gitlab::EncodingHelper
+
MAX_MSG_SIZE = 128.kilobytes.freeze
def initialize(repository)
@@ -13,12 +15,12 @@ module Gitlab
def write_page(name, format, content, commit_details)
request = Gitaly::WikiWritePageRequest.new(
repository: @gitaly_repo,
- name: GitalyClient.encode(name),
+ name: encode_binary(name),
format: format.to_s,
commit_details: gitaly_commit_details(commit_details)
)
- strio = GitalyClient.binary_stringio(content)
+ strio = binary_stringio(content)
enum = Enumerator.new do |y|
until strio.eof?
@@ -39,13 +41,13 @@ module Gitlab
def update_page(page_path, title, format, content, commit_details)
request = Gitaly::WikiUpdatePageRequest.new(
repository: @gitaly_repo,
- page_path: GitalyClient.encode(page_path),
- title: GitalyClient.encode(title),
+ page_path: encode_binary(page_path),
+ title: encode_binary(title),
format: format.to_s,
commit_details: gitaly_commit_details(commit_details)
)
- strio = GitalyClient.binary_stringio(content)
+ strio = binary_stringio(content)
enum = Enumerator.new do |y|
until strio.eof?
@@ -63,7 +65,7 @@ module Gitlab
def delete_page(page_path, commit_details)
request = Gitaly::WikiDeletePageRequest.new(
repository: @gitaly_repo,
- page_path: GitalyClient.encode(page_path),
+ page_path: encode_binary(page_path),
commit_details: gitaly_commit_details(commit_details)
)
@@ -73,9 +75,9 @@ module Gitlab
def find_page(title:, version: nil, dir: nil)
request = Gitaly::WikiFindPageRequest.new(
repository: @gitaly_repo,
- title: GitalyClient.encode(title),
- revision: GitalyClient.encode(version),
- directory: GitalyClient.encode(dir)
+ title: encode_binary(title),
+ revision: encode_binary(version),
+ directory: encode_binary(dir)
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
@@ -102,8 +104,8 @@ module Gitlab
def find_file(name, revision)
request = Gitaly::WikiFindFileRequest.new(
repository: @gitaly_repo,
- name: GitalyClient.encode(name),
- revision: GitalyClient.encode(revision)
+ name: encode_binary(name),
+ revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request)
@@ -158,9 +160,9 @@ module Gitlab
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
- name: GitalyClient.encode(commit_details.name),
- email: GitalyClient.encode(commit_details.email),
- message: GitalyClient.encode(commit_details.message)
+ name: encode_binary(commit_details.name),
+ email: encode_binary(commit_details.email),
+ message: encode_binary(commit_details.message)
)
end
end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index 9cf2e7fd871..7dd68a0d1cd 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -29,7 +29,7 @@ module Gitlab
# this code, e.g. because we had to retry this job after
# `import_wiki?` raised a rate limit error. In this case we'll skip
# re-importing the main repository.
- if project.repository.empty_repo?
+ if project.empty_repo?
import_repository
else
true
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index dfcdfc307b6..9148d7571f2 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -21,6 +21,7 @@ module Gitlab
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
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 94678b6ec40..3f3f10596c5 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -2,9 +2,8 @@
# key-13 or user-36 or last commit
module Gitlab
module Identifier
- def identify(identifier, project, newrev)
+ def identify(identifier, project = nil, newrev = nil)
if identifier.blank?
- # Local push from gitlab
identify_using_commit(project, newrev)
elsif identifier =~ /\Auser-\d+\Z/
# git push over http
@@ -17,6 +16,8 @@ module Gitlab
# Tries to identify a user based on a commit SHA.
def identify_using_commit(project, ref)
+ return if project.nil? && ref.nil?
+
commit = project.commit(ref)
return if !commit || !commit.author_email
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 2066005dddc..af203ff711d 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.1'.freeze
+ VERSION = '0.2.2'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index 90942774a2e..dd5d35feab9 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -15,6 +15,11 @@ module Gitlab
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
end
+ def git_clone_bundle(repo_path:, bundle_path:)
+ execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path}))
+ Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
+ end
+
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
@@ -32,7 +37,7 @@ module Gitlab
def execute(cmd)
output, status = Gitlab::Popen.popen(cmd)
- @shared.error(Gitlab::ImportExport::Error.new(output.to_s)) unless status.zero?
+ @shared.error(Gitlab::ImportExport::Error.new(output.to_s)) unless status.zero? # rubocop:disable Gitlab/ModuleWithInstanceVariables
status.zero?
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index f2b193c79cb..2daed10f678 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -49,8 +49,8 @@ project_tree:
- :author
- events:
- :push_event_payload
- - :stages
- - :statuses
+ - stages:
+ - :statuses
- :auto_devops
- :triggers
- :pipeline_schedules
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index d7d1b05e8b9..05dbaf6322c 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -62,6 +62,7 @@ module Gitlab
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
@@ -112,9 +113,7 @@ module Gitlab
@relation_hash.delete('trace') # old export files have trace
@relation_hash.delete('token')
- imported_object do |object|
- object.commit_id = nil
- end
+ imported_object
elsif @relation_name == :merge_requests
MergeRequestParser.new(@project, @relation_hash.delete('diff_head_sha'), imported_object, @relation_hash).parse!
else
@@ -182,8 +181,9 @@ module Gitlab
end
def imported_object
- yield(existing_or_new_object) if block_given?
- existing_or_new_object.importing = true if existing_or_new_object.respond_to?(:importing)
+ if existing_or_new_object.respond_to?(:importing)
+ existing_or_new_object.importing = true
+ end
existing_or_new_object
rescue ActiveRecord::RecordNotUnique
@@ -211,6 +211,14 @@ module Gitlab
@relation_hash['diff'] = @relation_hash.delete('utf8_diff')
end
+ def setup_pipeline
+ @relation_hash.fetch('stages').each do |stage|
+ stage.statuses.each do |status|
+ status.pipeline = imported_object
+ end
+ end
+ end
+
def existing_or_new_object
# Only find existing records to avoid mapping tables such as milestones
# Otherwise always create the record, skipping the extra SELECT clause.
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 32ca2809b2f..d0e5cfcfd3e 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -13,7 +13,7 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- gitlab_shell.import_repository(@project.repository_storage_path, @project.disk_path, @path_to_bundle)
+ git_clone_bundle(repo_path: @project.repository.path_to_repo, bundle_path: @path_to_bundle)
rescue => e
@shared.error(e)
false
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index eeb03625479..60d5fa4d29a 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -7,6 +7,7 @@ module Gitlab
module ImportSources
ImportSource = Struct.new(:name, :title, :importer)
+ # We exclude `bare_repository` here as it has no import class associated
ImportTable = [
ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 407cdefc04d..0f0588b8b23 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -1,96 +1,8 @@
module Gitlab
module Kubernetes
- class Helm
+ module Helm
HELM_VERSION = '2.7.0'.freeze
NAMESPACE = 'gitlab-managed-apps'.freeze
- INSTALL_DEPS = <<-EOS.freeze
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- EOS
-
- InstallCommand = Struct.new(:name, :install_helm, :chart) do
- def pod_name
- "install-#{name}"
- end
- end
-
- def initialize(kubeclient)
- @kubeclient = kubeclient
- @namespace = Gitlab::Kubernetes::Namespace.new(NAMESPACE, kubeclient)
- end
-
- def install(command)
- @namespace.ensure_exists!
- @kubeclient.create_pod(pod_resource(command))
- end
-
- ##
- # Returns Pod phase
- #
- # https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
- #
- # values: "Pending", "Running", "Succeeded", "Failed", "Unknown"
- #
- def installation_status(pod_name)
- @kubeclient.get_pod(pod_name, @namespace.name).status.phase
- end
-
- def installation_log(pod_name)
- @kubeclient.get_pod_log(pod_name, @namespace.name).body
- end
-
- def delete_installation_pod!(pod_name)
- @kubeclient.delete_pod(pod_name, @namespace.name)
- end
-
- private
-
- def pod_resource(command)
- labels = { 'gitlab.org/action': 'install', 'gitlab.org/application': command.name }
- metadata = { name: command.pod_name, namespace: @namespace.name, labels: labels }
- container = {
- name: 'helm',
- image: 'alpine:3.6',
- env: generate_pod_env(command),
- command: %w(/bin/sh),
- args: %w(-c $(COMMAND_SCRIPT))
- }
- spec = { containers: [container], restartPolicy: 'Never' }
-
- ::Kubeclient::Resource.new(metadata: metadata, spec: spec)
- end
-
- def generate_pod_env(command)
- {
- HELM_VERSION: HELM_VERSION,
- TILLER_NAMESPACE: @namespace.name,
- COMMAND_SCRIPT: generate_script(command)
- }.map { |key, value| { name: key, value: value } }
- end
-
- def generate_script(command)
- [
- INSTALL_DEPS,
- helm_init_command(command),
- helm_install_command(command)
- ].join("\n")
- end
-
- def helm_init_command(command)
- if command.install_helm
- 'helm init >/dev/null'
- else
- 'helm init --client-only >/dev/null'
- end
- end
-
- def helm_install_command(command)
- return if command.chart.nil?
-
- "helm install #{command.chart} --name #{command.name} --namespace #{@namespace.name} >/dev/null"
- end
end
end
end
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
new file mode 100644
index 00000000000..737081ddc5b
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module Kubernetes
+ module Helm
+ class Api
+ def initialize(kubeclient)
+ @kubeclient = kubeclient
+ @namespace = Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, kubeclient)
+ end
+
+ def install(command)
+ @namespace.ensure_exists!
+ @kubeclient.create_pod(pod_resource(command))
+ end
+
+ ##
+ # Returns Pod phase
+ #
+ # https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
+ #
+ # values: "Pending", "Running", "Succeeded", "Failed", "Unknown"
+ #
+ def installation_status(pod_name)
+ @kubeclient.get_pod(pod_name, @namespace.name).status.phase
+ end
+
+ def installation_log(pod_name)
+ @kubeclient.get_pod_log(pod_name, @namespace.name).body
+ end
+
+ def delete_installation_pod!(pod_name)
+ @kubeclient.delete_pod(pod_name, @namespace.name)
+ end
+
+ private
+
+ def pod_resource(command)
+ Gitlab::Kubernetes::Helm::Pod.new(command, @namespace.name, @kubeclient).generate
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
new file mode 100644
index 00000000000..8d8c441a4b1
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -0,0 +1,53 @@
+module Gitlab
+ module Kubernetes
+ module Helm
+ class InstallCommand
+ attr_reader :name, :install_helm, :chart, :chart_values_file
+
+ def initialize(name, install_helm: false, chart: false, chart_values_file: false)
+ @name = name
+ @install_helm = install_helm
+ @chart = chart
+ @chart_values_file = chart_values_file
+ end
+
+ def pod_name
+ "install-#{name}"
+ end
+
+ def generate_script(namespace_name)
+ [
+ install_dps_command,
+ init_command,
+ complete_command(namespace_name)
+ ].join("\n")
+ end
+
+ private
+
+ def init_command
+ if install_helm
+ 'helm init >/dev/null'
+ else
+ 'helm init --client-only >/dev/null'
+ end
+ end
+
+ def complete_command(namespace_name)
+ return unless chart
+
+ "helm install #{chart} --name #{name} --namespace #{namespace_name} >/dev/null"
+ end
+
+ def install_dps_command
+ <<~HEREDOC
+ set -eo pipefail
+ apk add -U 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
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb
new file mode 100644
index 00000000000..233f6bf6227
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -0,0 +1,69 @@
+module Gitlab
+ module Kubernetes
+ module Helm
+ class Pod
+ def initialize(command, namespace_name, kubeclient)
+ @command = command
+ @namespace_name = namespace_name
+ @kubeclient = kubeclient
+ end
+
+ def generate
+ spec = { containers: [container_specification], restartPolicy: 'Never' }
+ if command.chart_values_file
+ generate_config_map
+ spec['volumes'] = volumes_specification
+ end
+ ::Kubeclient::Resource.new(metadata: metadata, spec: spec)
+ end
+
+ private
+
+ attr_reader :command, :namespace_name, :kubeclient
+
+ def container_specification
+ container = {
+ name: 'helm',
+ image: 'alpine:3.6',
+ env: generate_pod_env(command),
+ command: %w(/bin/sh),
+ args: %w(-c $(COMMAND_SCRIPT))
+ }
+ container[:volumeMounts] = volume_mounts_specification if command.chart_values_file
+ container
+ end
+
+ def labels
+ { 'gitlab.org/action': 'install', 'gitlab.org/application': command.name }
+ end
+
+ def metadata
+ { name: command.pod_name, namespace: namespace_name, labels: labels }
+ end
+
+ def volume_mounts_specification
+ [{ name: 'config-volume', mountPath: '/etc/config' }]
+ end
+
+ def volumes_specification
+ [{ name: 'config-volume', configMap: { name: 'values-config' } }]
+ end
+
+ def generate_pod_env(command)
+ {
+ HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION,
+ TILLER_NAMESPACE: namespace_name,
+ COMMAND_SCRIPT: command.generate_script(namespace_name)
+ }.map { |key, value| { name: key, value: value } }
+ end
+
+ def generate_config_map
+ resource = ::Kubeclient::Resource.new
+ resource.metadata = { name: 'values-config', namespace: namespace_name }
+ resource.data = YAML.load_file(command.chart_values_file)
+ kubeclient.create_config_map(resource)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 0afaa2306b5..76863e77dc3 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -74,7 +74,7 @@ module Gitlab
def user_options(fields, value, limit)
options = {
- attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq,
+ attributes: Gitlab::LDAP::Person.ldap_attributes(config),
base: config.base
}
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index c8f19cd52d5..0d9a554fc18 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -148,7 +148,7 @@ module Gitlab
def default_attributes
{
- 'username' => %w(uid userid sAMAccountName),
+ 'username' => %w(uid sAMAccountName userid),
'email' => %w(mail email userPrincipalName),
'name' => 'cn',
'first_name' => 'givenName',
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 38d7a9ba2f5..e81cec6ba1a 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -6,6 +6,8 @@ module Gitlab
# Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")
+ InvalidEntryError = Class.new(StandardError)
+
attr_accessor :entry, :provider
def self.find_by_uid(uid, adapter)
@@ -29,11 +31,12 @@ module Gitlab
def self.ldap_attributes(config)
[
- 'dn', # Used in `dn`
- config.uid, # Used in `uid`
- *config.attributes['name'], # Used in `name`
- *config.attributes['email'] # Used in `email`
- ]
+ 'dn',
+ config.uid,
+ *config.attributes['name'],
+ *config.attributes['email'],
+ *config.attributes['username']
+ ].compact.uniq
end
def self.normalize_dn(dn)
@@ -60,6 +63,8 @@ module Gitlab
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@provider = provider
+
+ validate_entry
end
def name
@@ -71,7 +76,13 @@ module Gitlab
end
def username
- uid
+ username = attribute_value(:username)
+
+ # Depending on the attribute, multiple values may
+ # be returned. We need only one for username.
+ # Ex. `uid` returns only one value but `mail` may
+ # return an array of multiple email addresses.
+ [username].flatten.first
end
def email
@@ -104,6 +115,19 @@ module Gitlab
entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
end
+
+ def validate_entry
+ allowed_attrs = self.class.ldap_attributes(config).map(&:downcase)
+
+ # Net::LDAP::Entry transforms keys to symbols. Change to strings to compare.
+ entry_attrs = entry.attribute_names.map { |n| n.to_s.downcase }
+ invalid_attrs = entry_attrs - allowed_attrs
+
+ if invalid_attrs.any?
+ raise InvalidEntryError,
+ "#{self.class.name} initialized with Net::LDAP::Entry containing invalid attributes(s): #{invalid_attrs}"
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 3945df27eed..84ee94e38e4 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -36,10 +36,6 @@ module Gitlab
ldap_config.block_auto_created_users
end
- def sync_profile_from_provider?
- true
- end
-
def allowed?
Gitlab::LDAP::Access.allowed?(gl_user)
end
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index bdf7910b7c7..877cebf6786 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -38,6 +38,7 @@ module Gitlab
# This is memoized since this method is called for every instrumented
# method. Loading data from an external cache on every method call slows
# things down too much.
+ # in milliseconds
@method_call_threshold ||= settings[:method_call_threshold]
end
@@ -154,6 +155,7 @@ 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?
@@ -170,6 +172,7 @@ 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 65d55576ac2..c2f9db56824 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -1,7 +1,11 @@
+# rubocop:disable Style/ClassVars
+
module Gitlab
module Metrics
# Class for tracking timing information about method calls
class MethodCall
+ @@measurement_enabled_cache = Concurrent::AtomicBoolean.new(false)
+ @@measurement_enabled_cache_expires_at = Concurrent::AtomicReference.new(Time.now.to_i)
MUTEX = Mutex.new
BASE_LABELS = { module: nil, method: nil }.freeze
attr_reader :real_time, :cpu_time, :call_count, :labels
@@ -18,6 +22,10 @@ module Gitlab
end
end
+ def self.measurement_enabled_cache_expires_at
+ @@measurement_enabled_cache_expires_at
+ end
+
# name - The full name of the method (including namespace) such as
# `User#sign_in`.
#
@@ -27,8 +35,8 @@ module Gitlab
@transaction = transaction
@name = name
@labels = { module: @module_name, method: @method_name }
- @real_time = 0
- @cpu_time = 0
+ @real_time = 0.0
+ @cpu_time = 0.0
@call_count = 0
end
@@ -46,7 +54,7 @@ module Gitlab
@call_count += 1
if call_measurement_enabled? && above_threshold?
- self.class.call_duration_histogram.observe(@transaction.labels.merge(labels), real_time / 1000.0)
+ self.class.call_duration_histogram.observe(@transaction.labels.merge(labels), real_time)
end
retval
@@ -57,8 +65,8 @@ module Gitlab
Metric.new(
Instrumentation.series,
{
- duration: real_time,
- cpu_duration: cpu_time,
+ duration: real_time.in_milliseconds.to_i,
+ cpu_duration: cpu_time.in_milliseconds.to_i,
call_count: call_count
},
method: @name
@@ -68,11 +76,18 @@ module Gitlab
# Returns true if the total runtime of this method exceeds the method call
# threshold.
def above_threshold?
- real_time >= Metrics.method_call_threshold
+ real_time.in_milliseconds >= Metrics.method_call_threshold
end
def call_measurement_enabled?
- Feature.get(:prometheus_metrics_method_instrumentation).enabled?
+ expires_at = @@measurement_enabled_cache_expires_at.value
+ if expires_at < Time.now.to_i
+ if @@measurement_enabled_cache_expires_at.compare_and_set(expires_at, 1.minute.from_now.to_i)
+ @@measurement_enabled_cache.value = Feature.get(:prometheus_metrics_method_instrumentation).enabled?
+ end
+ end
+
+ @@measurement_enabled_cache.value
end
end
end
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index 09103b4ca2d..b0b8e8436db 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -4,6 +4,7 @@ module Gitlab
module Metrics
module Prometheus
include Gitlab::CurrentSettings
+ include Gitlab::Utils::StrongMemoize
REGISTRY_MUTEX = Mutex.new
PROVIDER_MUTEX = Mutex.new
@@ -17,16 +18,18 @@ module Gitlab
end
def prometheus_metrics_enabled?
- return @prometheus_metrics_enabled if defined?(@prometheus_metrics_enabled)
-
- @prometheus_metrics_enabled = prometheus_metrics_enabled_unmemoized
+ strong_memoize(:prometheus_metrics_enabled) do
+ prometheus_metrics_enabled_unmemoized
+ end
end
def registry
- return @registry if @registry
-
- REGISTRY_MUTEX.synchronize do
- @registry ||= ::Prometheus::Client.registry
+ strong_memoize(:registry) do
+ REGISTRY_MUTEX.synchronize do
+ strong_memoize(:registry) do
+ ::Prometheus::Client.registry
+ end
+ end
end
end
diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb
index f4f9b5ca792..5a0f7f28fc8 100644
--- a/lib/gitlab/metrics/samplers/influx_sampler.rb
+++ b/lib/gitlab/metrics/samplers/influx_sampler.rb
@@ -27,7 +27,6 @@ module Gitlab
def sample
sample_memory_usage
sample_file_descriptors
- sample_objects
sample_gc
flush
@@ -48,29 +47,6 @@ module Gitlab
add_metric('file_descriptors', value: System.file_descriptor_count)
end
- if Metrics.mri?
- def sample_objects
- sample = Allocations.to_hash
- counts = sample.each_with_object({}) do |(klass, count), hash|
- name = klass.name
-
- next unless name
-
- hash[name] = count
- end
-
- # Symbols aren't allocated so we'll need to add those manually.
- counts['Symbol'] = Symbol.all_symbols.length
-
- counts.each do |name, count|
- add_metric('object_counts', { count: count }, type: name)
- end
- end
- else
- def sample_objects
- end
- end
-
def sample_gc
time = GC::Profiler.total_time * 1000.0
stats = GC.stat.merge(total_time: time)
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index f4901be9581..4e1ea62351f 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -48,12 +48,11 @@ module Gitlab
def sample
start_time = System.monotonic_time
sample_gc
- sample_objects
metrics[:memory_usage].set(labels, System.memory_usage)
metrics[:file_descriptors].set(labels, System.file_descriptor_count)
- metrics[:sampler_duration].observe(labels.merge(worker_label), (System.monotonic_time - start_time) / 1000.0)
+ metrics[:sampler_duration].observe(labels.merge(worker_label), System.monotonic_time - start_time)
ensure
GC::Profiler.clear
end
@@ -68,32 +67,6 @@ module Gitlab
end
end
- def sample_objects
- list_objects.each do |name, count|
- metrics[:objects_total].set(labels.merge(class: name), count)
- end
- end
-
- if Metrics.mri?
- def list_objects
- sample = Allocations.to_hash
- counts = sample.each_with_object({}) do |(klass, count), hash|
- name = klass.name
-
- next unless name
-
- hash[name] = count
- end
-
- # Symbols aren't allocated so we'll need to add those manually.
- counts['Symbol'] = Symbol.all_symbols.length
- counts
- end
- else
- def list_objects
- end
- end
-
def worker_label
return {} unless defined?(Unicorn::Worker)
diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb
index efd3c9daf79..250897a79c2 100644
--- a/lib/gitlab/metrics/subscribers/rails_cache.rb
+++ b/lib/gitlab/metrics/subscribers/rails_cache.rb
@@ -66,7 +66,7 @@ module Gitlab
:gitlab_cache_operation_duration_seconds,
'Cache access time',
Transaction::BASE_LABELS.merge({ action: nil }),
- [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ [0.001, 0.01, 0.1, 1, 10]
)
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index c2cbd3c16a1..e60e245cf89 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -35,27 +35,27 @@ module Gitlab
if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID)
def self.cpu_time
Process
- .clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond)
+ .clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second)
end
else
def self.cpu_time
Process
- .clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond)
+ .clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
end
end
# Returns the current real time in a given precision.
#
- # Returns the time as a Fixnum.
- def self.real_time(precision = :millisecond)
+ # Returns the time as a Float for precision = :float_second.
+ def self.real_time(precision = :float_second)
Process.clock_gettime(Process::CLOCK_REALTIME, precision)
end
- # Returns the current monotonic clock time in a given precision.
+ # Returns the current monotonic clock time as seconds with microseconds precision.
#
- # Returns the time as a Fixnum.
- def self.monotonic_time(precision = :millisecond)
- Process.clock_gettime(Process::CLOCK_MONOTONIC, precision)
+ # Returns the time as a Float.
+ def self.monotonic_time
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
end
end
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index ee3afc5ffdb..e7975c023a9 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -35,6 +35,10 @@ module Gitlab
@finished_at ? (@finished_at - @started_at) : 0.0
end
+ def duration_milliseconds
+ duration.in_milliseconds.to_i
+ end
+
def allocated_memory
@memory_after - @memory_before
end
@@ -50,7 +54,7 @@ module Gitlab
@memory_after = System.memory_usage
@finished_at = System.monotonic_time
- self.class.metric_transaction_duration_seconds.observe(labels, duration * 1000)
+ self.class.metric_transaction_duration_seconds.observe(labels, duration)
self.class.metric_transaction_allocated_memory_bytes.observe(labels, allocated_memory * 1024.0)
Thread.current[THREAD_KEY] = nil
@@ -97,7 +101,7 @@ module Gitlab
end
def track_self
- values = { duration: duration, allocated_memory: allocated_memory }
+ values = { duration: duration_milliseconds, allocated_memory: allocated_memory }
@values.each do |name, value|
values[name] = value
diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb
index ac9d66c836d..657db29c85a 100644
--- a/lib/gitlab/o_auth/provider.rb
+++ b/lib/gitlab/o_auth/provider.rb
@@ -19,6 +19,18 @@ module Gitlab
name.to_s.start_with?('ldap')
end
+ def self.sync_profile_from_provider?(provider)
+ return true if ldap_provider?(provider)
+
+ providers = Gitlab.config.omniauth.sync_profile_from_provider
+
+ if providers.is_a?(Array)
+ providers.include?(provider)
+ else
+ providers
+ end
+ end
+
def self.config_for(name)
name = name.to_s
if ldap_provider?(name)
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 552133234a3..d33f33d192f 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -12,7 +12,7 @@ module Gitlab
def initialize(auth_hash)
self.auth_hash = auth_hash
- update_profile if sync_profile_from_provider?
+ update_profile
add_or_update_user_identities
end
@@ -195,29 +195,31 @@ module Gitlab
end
def sync_profile_from_provider?
- providers = Gitlab.config.omniauth.sync_profile_from_provider
-
- if providers.is_a?(Array)
- providers.include?(auth_hash.provider)
- else
- providers
- end
+ Gitlab::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
end
def update_profile
- user_synced_attributes_metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata
-
- UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
- if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
- gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
- user_synced_attributes_metadata.set_attribute_synced(key, true)
- else
- user_synced_attributes_metadata.set_attribute_synced(key, false)
+ return unless sync_profile_from_provider? || creating_linked_ldap_user?
+
+ metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata
+
+ if sync_profile_from_provider?
+ UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
+ if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
+ gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
+ metadata.set_attribute_synced(key, true)
+ else
+ metadata.set_attribute_synced(key, false)
+ end
end
+
+ metadata.provider = auth_hash.provider
end
- user_synced_attributes_metadata.provider = auth_hash.provider
- gl_user.user_synced_attributes_metadata = user_synced_attributes_metadata
+ if creating_linked_ldap_user? && gl_user.email == ldap_person.email.first
+ metadata.set_attribute_synced(:email, true)
+ metadata.provider = ldap_person.provider
+ end
end
def log
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index fef9d3e31d4..ca48c6df602 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -27,10 +27,17 @@ module Gitlab
# It allows us to search only for projects user has access to
attr_reader :limit_projects
- def initialize(current_user, limit_projects, query)
+ # Whether a custom filter is used to restrict scope of projects.
+ # If the default filter (which lists all projects user has access to)
+ # is used, we can skip it when filtering merge requests and optimize the
+ # query
+ attr_reader :default_project_filter
+
+ def initialize(current_user, limit_projects, query, default_project_filter: false)
@current_user = current_user
@limit_projects = limit_projects || Project.all
@query = query
+ @default_project_filter = default_project_filter
end
def objects(scope, page = nil)
@@ -75,7 +82,10 @@ module Gitlab
end
def issues
- issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
+ issues = IssuesFinder.new(current_user).execute
+ unless default_project_filter
+ issues = issues.where(project_id: project_ids_relation)
+ end
issues =
if query =~ /#(\d+)\z/
@@ -94,7 +104,11 @@ module Gitlab
end
def merge_requests
- merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation)
+ merge_requests = MergeRequestsFinder.new(current_user).execute
+ unless default_project_filter
+ merge_requests = merge_requests.in_projects(project_ids_relation)
+ end
+
merge_requests =
if query =~ /[#!](\d+)\z/
merge_requests.where(iid: $1)
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 30df7e4a831..94a481a0f2e 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -8,7 +8,7 @@ end
module Gitlab
class Seeder
def self.quiet
- mute_mailer unless Rails.env.test?
+ mute_mailer
SeedFu.quiet = true
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
new file mode 100644
index 00000000000..d01213bb6e0
--- /dev/null
+++ b/lib/gitlab/setup_helper.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module SetupHelper
+ class << self
+ # We cannot create config.toml files for all possible Gitaly configuations.
+ # For instance, if Gitaly is running on another machine then it makes no
+ # sense to write a config.toml file on the current machine. This method will
+ # only generate a configuration for the most common and simplest case: when
+ # we have exactly one Gitaly process and we are sure it is running locally
+ # because it uses a Unix socket.
+ # For development and testing purposes, an extra storage is added to gitaly,
+ # which is not known to Rails, but must be explicitly stubbed.
+ def gitaly_configuration_toml(gitaly_dir, gitaly_ruby: true)
+ storages = []
+ address = nil
+
+ Gitlab.config.repositories.storages.each do |key, val|
+ if address
+ if address != val['gitaly_address']
+ raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
+ end
+ elsif URI(val['gitaly_address']).scheme != 'unix'
+ raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
+ else
+ address = val['gitaly_address']
+ end
+
+ storages << { name: key, path: val['path'] }
+ end
+
+ if Rails.env.test?
+ storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
+ end
+
+ config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
+ config[:auth] = { token: 'secret' } if Rails.env.test?
+ config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
+ config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
+ config[:bin_dir] = Gitlab.config.gitaly.client_path
+
+ TOML.dump(config)
+ end
+
+ # rubocop:disable Rails/Output
+ def create_gitaly_configuration(dir, force: false)
+ config_path = File.join(dir, 'config.toml')
+ FileUtils.rm_f(config_path) if force
+
+ File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f|
+ f.puts gitaly_configuration_toml(dir)
+ end
+ rescue Errno::EEXIST
+ puts "Skipping config.toml generation:"
+ puts "A configuration file already exists."
+ rescue ArgumentError => e
+ puts "Skipping config.toml generation:"
+ puts e.message
+ end
+ # rubocop:enable Rails/Output
+ end
+ end
+end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index a22a63665be..564047bbd34 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -66,12 +66,11 @@ module Gitlab
# Init new repository
#
# storage - project's storage name
- # name - project path with namespace
+ # name - project disk path
#
# Ex.
# add_repository("/path/to/storage", "gitlab/gitlab-ci")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def add_repository(storage, name)
relative_path = name.dup
relative_path << '.git' unless relative_path.end_with?('.git')
@@ -94,30 +93,38 @@ module Gitlab
# Import repository
#
# storage - project's storage path
- # name - project path with namespace
+ # name - project disk path
+ # url - URL to import from
#
# Ex.
- # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git")
+ # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
+ # 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_shell_projects_path, 'import-project',
- storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"]
- gitlab_shell_fast_execute_raise_error(cmd)
+ cmd = gitlab_projects(storage, "#{name}.git")
+ success = cmd.import_project(url, git_timeout)
+
+ raise Error, cmd.output unless success
+
+ success
end
# Fetch remote for repository
#
# repository - an instance of Git::Repository
# remote - remote name
+ # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication
# forced - should we use --force flag?
# no_tags - should we use --no-tags flag?
#
# Ex.
# fetch_remote(my_repo, "upstream")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false)
gitaly_migrate(:fetch_remote) do |is_enabled|
if is_enabled
@@ -131,16 +138,15 @@ module Gitlab
# Move repository
# storage - project's storage path
- # path - project path with namespace
- # new_path - new project path with namespace
+ # path - project disk path
+ # new_path - new project disk path
#
# Ex.
# mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def mv_repository(storage, path, new_path)
- gitlab_shell_fast_execute([gitlab_shell_projects_path, 'mv-project',
- storage, "#{path}.git", "#{new_path}.git"])
+ gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git")
end
# Fork repository to new path
@@ -152,32 +158,23 @@ module Gitlab
# Ex.
# fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "new-namespace/gitlab-ci")
#
- # Gitaly note: JV: not easy to migrate because this involves two Gitaly servers, not one.
+ # 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_shell_fast_execute(
- [
- gitlab_shell_projects_path,
- 'fork-repository',
- forked_from_storage,
- "#{forked_from_disk_path}.git",
- forked_to_storage,
- "#{forked_to_disk_path}.git"
- ]
- )
+ gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git")
+ .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git")
end
# Remove repository from file system
#
# storage - project's storage path
- # name - project path with namespace
+ # name - project disk path
#
# Ex.
# remove_repository("/path/to/storage", "gitlab/gitlab-ci")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def remove_repository(storage, name)
- gitlab_shell_fast_execute([gitlab_shell_projects_path,
- 'rm-project', storage, "#{name}.git"])
+ gitlab_projects(storage, "#{name}.git").rm_project
end
# Add new key to gitlab-shell
@@ -226,7 +223,6 @@ module Gitlab
# Ex.
# add_namespace("/path/to/storage", "gitlab")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def add_namespace(storage, name)
Gitlab::GitalyClient.migrate(:add_namespace) do |enabled|
if enabled
@@ -248,7 +244,6 @@ module Gitlab
# Ex.
# rm_namespace("/path/to/storage", "gitlab")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def rm_namespace(storage, name)
Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled|
if enabled
@@ -266,7 +261,6 @@ module Gitlab
# Ex.
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def mv_namespace(storage, old_name, new_name)
Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled|
if enabled
@@ -341,24 +335,35 @@ module Gitlab
private
- def local_fetch_remote(storage, name, remote, ssh_auth: nil, forced: false, no_tags: false)
- args = [gitlab_shell_projects_path, 'fetch-remote', storage, name, remote, "#{Gitlab.config.gitlab_shell.git_timeout}"]
- args << '--force' if forced
- args << '--no-tags' if no_tags
+ def gitlab_projects(shard_path, disk_path)
+ Gitlab::Git::GitlabProjects.new(
+ shard_path,
+ disk_path,
+ global_hooks_path: Gitlab.config.gitlab_shell.hooks_path,
+ logger: Rails.logger
+ )
+ end
- vars = {}
+ def local_fetch_remote(storage_path, repository_relative_path, remote, ssh_auth: nil, forced: false, no_tags: false)
+ vars = { force: forced, tags: !no_tags }
if ssh_auth&.ssh_import?
if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present?
- vars['GITLAB_SHELL_SSH_KEY'] = ssh_auth.ssh_private_key
+ vars[:ssh_key] = ssh_auth.ssh_private_key
end
if ssh_auth.ssh_known_hosts.present?
- vars['GITLAB_SHELL_KNOWN_HOSTS'] = ssh_auth.ssh_known_hosts
+ vars[:known_hosts] = ssh_auth.ssh_known_hosts
end
end
- gitlab_shell_fast_execute_raise_error(args, vars)
+ cmd = gitlab_projects(storage_path, 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)
@@ -394,6 +399,10 @@ module Gitlab
Gitlab::GitalyClient::NamespaceService.new(storage)
end
+ def git_timeout
+ Gitlab.config.gitlab_shell.git_timeout
+ end
+
def gitaly_migrate(method, &block)
Gitlab::GitalyClient.migrate(method, &block)
rescue GRPC::NotFound, GRPC::BadStatus => e
diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb
index dc9886732b5..c3d7814551c 100644
--- a/lib/gitlab/sidekiq_config.rb
+++ b/lib/gitlab/sidekiq_config.rb
@@ -1,16 +1,35 @@
require 'yaml'
+require 'set'
module Gitlab
module SidekiqConfig
- def self.redis_queues
- @redis_queues ||= Sidekiq::Queue.all.map(&:name)
+ # This method is called by `bin/sidekiq-cluster` in EE, which runs outside
+ # of bundler/Rails context, so we cannot use any gem or Rails methods.
+ def self.worker_queues(rails_path = Rails.root.to_s)
+ @worker_queues ||= {}
+ @worker_queues[rails_path] ||= YAML.load_file(File.join(rails_path, 'app/workers/all_queues.yml'))
end
# This method is called by `bin/sidekiq-cluster` in EE, which runs outside
# of bundler/Rails context, so we cannot use any gem or Rails methods.
- def self.config_queues(rails_path = Rails.root.to_s)
+ def self.expand_queues(queues, all_queues = self.worker_queues)
+ return [] if queues.empty?
+
+ queues_set = all_queues.to_set
+
+ queues.flat_map do |queue|
+ [queue, *queues_set.grep(/\A#{queue}:/)]
+ end
+ end
+
+ def self.redis_queues
+ # Not memoized, because this can change during the life of the application
+ Sidekiq::Queue.all.map(&:name)
+ end
+
+ def self.config_queues
@config_queues ||= begin
- config = YAML.load_file(File.join(rails_path, 'config', 'sidekiq_queues.yml'))
+ config = YAML.load_file(Rails.root.join('config/sidekiq_queues.yml'))
config[:queues].map(&:first)
end
end
@@ -23,14 +42,6 @@ module Gitlab
@workers ||= find_workers(Rails.root.join('app', 'workers'))
end
- def self.default_queues
- [ActionMailer::DeliveryJob.queue_name, 'default']
- end
-
- def self.worker_queues
- @worker_queues ||= (workers.map(&:queue) + default_queues).uniq
- end
-
def self.find_workers(root)
concerns = root.join('concerns').to_s
@@ -43,7 +54,7 @@ module Gitlab
ns.camelize.constantize
end
- # Skip concerns
+ # Skip things that aren't workers
workers.select { |w| w < Sidekiq::Worker }
end
end
diff --git a/lib/gitlab/sidekiq_versioning.rb b/lib/gitlab/sidekiq_versioning.rb
new file mode 100644
index 00000000000..9683214ec18
--- /dev/null
+++ b/lib/gitlab/sidekiq_versioning.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module SidekiqVersioning
+ def self.install!
+ Sidekiq::Manager.prepend SidekiqVersioning::Manager
+
+ # The Sidekiq client API always adds the queue to the Sidekiq queue
+ # list, but mail_room and gitlab-shell do not. This is only necessary
+ # for monitoring.
+ begin
+ queues = SidekiqConfig.worker_queues
+
+ if queues.any?
+ Sidekiq.redis do |conn|
+ conn.pipelined do
+ queues.each do |queue|
+ conn.sadd('queues', queue)
+ end
+ end
+ end
+ end
+ rescue ::Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_versioning/manager.rb b/lib/gitlab/sidekiq_versioning/manager.rb
new file mode 100644
index 00000000000..308be0fdf76
--- /dev/null
+++ b/lib/gitlab/sidekiq_versioning/manager.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ module SidekiqVersioning
+ module Manager
+ def initialize(options = {})
+ options[:strict] = false
+ options[:queues] = SidekiqConfig.expand_queues(options[:queues])
+ Sidekiq.logger.info "Listening on queues #{options[:queues].uniq.sort}"
+ super
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb
index 341f2aabdd0..31c1e97efba 100644
--- a/lib/gitlab/slash_commands/presenters/issue_base.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_base.rb
@@ -11,32 +11,36 @@ module Gitlab
end
def project
- @resource.project
+ resource.project
end
def author
- @resource.author
+ resource.author
end
def fields
[
{
title: "Assignee",
- value: @resource.assignees.any? ? @resource.assignees.first.name : "_None_",
+ value: resource.assignees.any? ? resource.assignees.first.name : "_None_",
short: true
},
{
title: "Milestone",
- value: @resource.milestone ? @resource.milestone.title : "_None_",
+ value: resource.milestone ? resource.milestone.title : "_None_",
short: true
},
{
title: "Labels",
- value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_",
+ value: resource.labels.any? ? resource.label_names.join(', ') : "_None_",
short: true
}
]
end
+
+ private
+
+ attr_reader :resource
end
end
end
diff --git a/lib/gitlab/tcp_checker.rb b/lib/gitlab/tcp_checker.rb
new file mode 100644
index 00000000000..6e24e46d0ea
--- /dev/null
+++ b/lib/gitlab/tcp_checker.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ class TcpChecker
+ attr_reader :remote_host, :remote_port, :local_host, :local_port, :error
+
+ def initialize(remote_host, remote_port, local_host = nil, local_port = nil)
+ @remote_host = remote_host
+ @remote_port = remote_port
+ @local_host = local_host
+ @local_port = local_port
+ end
+
+ def local
+ join_host_port(local_host, local_port)
+ end
+
+ def remote
+ join_host_port(remote_host, remote_port)
+ end
+
+ def check(timeout: 10)
+ Socket.tcp(
+ remote_host, remote_port,
+ local_host, local_port,
+ connect_timeout: timeout
+ ) do |sock|
+ @local_port, @local_host = Socket.unpack_sockaddr_in(sock.local_address)
+ @remote_port, @remote_host = Socket.unpack_sockaddr_in(sock.remote_address)
+ end
+
+ true
+ rescue => err
+ @error = err
+
+ false
+ end
+
+ private
+
+ def join_host_port(host, port)
+ host = "[#{host}]" if host.include?(':')
+
+ "#{host}:#{port}"
+ end
+ end
+end
diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb
index a2ac9285b56..fe091f4611b 100644
--- a/lib/gitlab/utils/strong_memoize.rb
+++ b/lib/gitlab/utils/strong_memoize.rb
@@ -11,6 +11,8 @@ module Gitlab
#
# We could write it like:
#
+ # include Gitlab::Utils::StrongMemoize
+ #
# def trigger_from_token
# strong_memoize(:trigger) do
# Ci::Trigger.find_by_token(params[:token].to_s)
@@ -18,14 +20,22 @@ module Gitlab
# end
#
def strong_memoize(name)
- ivar_name = "@#{name}"
-
- if instance_variable_defined?(ivar_name)
- instance_variable_get(ivar_name)
+ if instance_variable_defined?(ivar(name))
+ instance_variable_get(ivar(name))
else
- instance_variable_set(ivar_name, yield)
+ instance_variable_set(ivar(name), yield)
end
end
+
+ def clear_memoization(name)
+ remove_instance_variable(ivar(name)) if instance_variable_defined?(ivar(name))
+ end
+
+ private
+
+ def ivar(name)
+ "@#{name}"
+ end
end
end
end
diff --git a/lib/gitlab/view/presenter/factory.rb b/lib/gitlab/view/presenter/factory.rb
index d172d61e2c9..570f0723e39 100644
--- a/lib/gitlab/view/presenter/factory.rb
+++ b/lib/gitlab/view/presenter/factory.rb
@@ -16,7 +16,7 @@ module Gitlab
attr_reader :subject, :attributes
def presenter_class
- "#{subject.class.name}Presenter".constantize
+ attributes.delete(:presenter_class) { "#{subject.class.name}Presenter".constantize }
end
end
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 11472ce6cce..6ced06a863d 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -57,11 +57,17 @@ module Gitlab
}
end
- def highest_allowed_level
+ def allowed_levels
restricted_levels = current_application_settings.restricted_visibility_levels
- allowed_levels = self.values - restricted_levels
- allowed_levels.max || PRIVATE
+ self.values - restricted_levels
+ end
+
+ def closest_allowed_level(target_level)
+ highest_allowed_level = allowed_levels.select { |level| level <= target_level }.max
+
+ # If all levels are restricted, fall back to PRIVATE
+ highest_allowed_level || PRIVATE
end
def allowed_for?(user, level)
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index b0563fb2d69..f05d001fd02 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -1,4 +1,6 @@
require 'google/apis/container_v1'
+require 'google/apis/cloudbilling_v1'
+require 'google/apis/cloudresourcemanager_v1'
module GoogleApi
module CloudPlatform
@@ -40,6 +42,22 @@ 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)
+ end
+ end
+
+ def projects_get_billing_info(project_name)
+ service = Google::Apis::CloudbillingV1::CloudbillingService.new
+ service.authorization = access_token
+
+ service.get_project_billing_info("projects/#{project_name}")
+ 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/tasks/gettext.rake b/lib/tasks/gettext.rake
index 35ba729c156..247d7be7d78 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -23,6 +23,7 @@ namespace :gettext do
desc 'Lint all po files in `locale/'
task lint: :environment do
require 'simple_po_parser'
+ require 'gitlab/utils'
FastGettext.silence_errors
files = Dir.glob(Rails.root.join('locale/*/gitlab.po'))
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index dfade1f3885..903e84359cd 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -387,14 +387,8 @@ namespace :gitlab do
namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*'))
-
- namespace_dirs.each do |namespace_dir|
- repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
- repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
- end
- end
+ puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red)
+ Rake::Task["gitlab:git:fsck"].execute
end
end
@@ -461,35 +455,4 @@ namespace :gitlab do
puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red)
end
end
-
- def check_repo_integrity(repo_dir)
- puts "\nChecking repo at #{repo_dir.color(:yellow)}"
-
- git_fsck(repo_dir)
- check_config_lock(repo_dir)
- check_ref_locks(repo_dir)
- end
-
- def git_fsck(repo_dir)
- puts "Running `git fsck`".color(:yellow)
- system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir)
- end
-
- def check_config_lock(repo_dir)
- config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
- config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
- puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
- end
-
- def check_ref_locks(repo_dir)
- lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
- if lock_files.present?
- puts "Ref lock files exist:".color(:red)
- lock_files.each do |lock_file|
- puts " #{lock_file}"
- end
- else
- puts "No ref lock files exist".color(:green)
- end
- end
end
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index cf82134d97e..3f5dd2ae3b3 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -30,6 +30,20 @@ namespace :gitlab do
end
end
+ desc 'GitLab | Git | Check all repos integrity'
+ task fsck: :environment do
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo|
+ check_config_lock(repo)
+ check_ref_locks(repo)
+ end
+
+ if failures.empty?
+ puts "Done".color(:green)
+ else
+ output_failures(failures)
+ end
+ end
+
def perform_git_cmd(cmd, message)
puts "Starting #{message} on all repositories"
@@ -40,6 +54,8 @@ namespace :gitlab do
else
failures << repo
end
+
+ yield(repo) if block_given?
end
failures
@@ -49,5 +65,24 @@ namespace :gitlab do
puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" }
end
+
+ def check_config_lock(repo_dir)
+ config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
+ config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
+
+ puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
+ end
+
+ def check_ref_locks(repo_dir)
+ lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
+
+ if lock_files.present?
+ puts "Ref lock files exist:".color(:red)
+
+ lock_files.each { |lock_file| puts " #{lock_file}" }
+ else
+ puts "No ref lock files exist".color(:green)
+ end
+ end
end
end
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 4d880c05f99..4507b841964 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -21,8 +21,8 @@ namespace :gitlab do
command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test?
+ Gitlab::SetupHelper.create_gitaly_configuration(args.dir)
Dir.chdir(args.dir) do
- create_gitaly_configuration
# In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present?
Bundler.with_original_env { run_command!(command) }
@@ -39,60 +39,7 @@ namespace :gitlab do
# Exclude gitaly-ruby configuration because that depends on the gitaly
# installation directory.
- puts gitaly_configuration_toml(gitaly_ruby: false)
- end
-
- private
-
- # We cannot create config.toml files for all possible Gitaly configuations.
- # For instance, if Gitaly is running on another machine then it makes no
- # sense to write a config.toml file on the current machine. This method will
- # only generate a configuration for the most common and simplest case: when
- # we have exactly one Gitaly process and we are sure it is running locally
- # because it uses a Unix socket.
- # For development and testing purposes, an extra storage is added to gitaly,
- # which is not known to Rails, but must be explicitly stubbed.
- def gitaly_configuration_toml(gitaly_ruby: true)
- storages = []
- address = nil
-
- Gitlab.config.repositories.storages.each do |key, val|
- if address
- if address != val['gitaly_address']
- raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
- end
- elsif URI(val['gitaly_address']).scheme != 'unix'
- raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
- else
- address = val['gitaly_address']
- end
-
- storages << { name: key, path: val['path'] }
- end
-
- if Rails.env.test?
- storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
- end
-
- config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
- config[:auth] = { token: 'secret' } if Rails.env.test?
- config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
- config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
- config[:bin_dir] = Gitlab.config.gitaly.client_path
-
- TOML.dump(config)
- end
-
- def create_gitaly_configuration
- File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f|
- f.puts gitaly_configuration_toml
- end
- rescue Errno::EEXIST
- puts "Skipping config.toml generation:"
- puts "A configuration file already exists."
- rescue ArgumentError => e
- puts "Skipping config.toml generation:"
- puts e.message
+ puts Gitlab::SetupHelper.gitaly_configuration_toml('', gitaly_ruby: false)
end
end
end
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index 8a63f486fa3..c1182af1014 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -1,10 +1,13 @@
require 'rainbow/ext/string'
+require 'gitlab/utils/strong_memoize'
module Gitlab
TaskFailedError = Class.new(StandardError)
TaskAbortedByUserError = Class.new(StandardError)
module TaskHelpers
+ include Gitlab::Utils::StrongMemoize
+
extend self
# Ask if the user wants to continue
@@ -105,16 +108,16 @@ module Gitlab
end
def gitlab_user?
- return @is_gitlab_user unless @is_gitlab_user.nil?
-
- current_user = run_command(%w(whoami)).chomp
- @is_gitlab_user = current_user == gitlab_user
+ strong_memoize(:is_gitlab_user) do
+ current_user = run_command(%w(whoami)).chomp
+ current_user == gitlab_user
+ end
end
def warn_user_is_not_gitlab
- return if @warned_user_not_gitlab
+ return if gitlab_user?
- unless gitlab_user?
+ strong_memoize(:warned_user_not_gitlab) do
current_user = run_command(%w(whoami)).chomp
puts " Warning ".color(:black).background(:yellow)
@@ -122,14 +125,12 @@ module Gitlab
puts " Things may work\/fail for the wrong reasons."
puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
puts ""
-
- @warned_user_not_gitlab = true
end
end
def all_repos
Gitlab.config.repositories.storages.each_value do |repository_storage|
- IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
+ IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -type d -name *.git)) do |find|
find.each_line do |path|
yield path.chomp
end
diff --git a/lib/tasks/gitlab/tcp_check.rake b/lib/tasks/gitlab/tcp_check.rake
new file mode 100644
index 00000000000..1400f57d6b9
--- /dev/null
+++ b/lib/tasks/gitlab/tcp_check.rake
@@ -0,0 +1,20 @@
+namespace :gitlab do
+ desc "GitLab | Check TCP connectivity to a specific host and port"
+ task :tcp_check, [:host, :port] => :environment do |_t, args|
+ unless args.host && args.port
+ puts "Please specify a host and port: `rake gitlab:tcp_check[example.com,80]`".color(:red)
+ exit 1
+ end
+
+ checker = Gitlab::TcpChecker.new(args.host, args.port)
+
+ if checker.check
+ puts "TCP connection from #{checker.local} to #{checker.remote} succeeded".color(:green)
+ else
+ puts "TCP connection to #{checker.remote} failed: #{checker.error}".color(:red)
+ puts
+ puts 'Check that host and port are correct, and that the traffic is permitted through any firewalls.'
+ exit 1
+ end
+ end
+end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index 9cc986535e1..c9e3eed82f2 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -6,6 +6,7 @@ require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_route
require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
+require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
@@ -15,4 +16,5 @@ task setup_postgresql: :environment do
IndexRoutesPathForLike.new.up
AddLowerPathIndexToRedirectRoutes.new.up
IndexRedirectRoutesPathForLike.new.up
+ AddIndexOnNamespacesLowerName.new.up
end
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 374164cbe65..8432914d6a7 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-03 12:29-0400\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:42-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -56,6 +56,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] ""
msgstr[1] ""
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
@@ -115,9 +118,6 @@ msgstr ""
msgid "Add License"
msgstr "ДобавÑне на лиценз"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Добавете SSH ключ в профила Ñи, за да можете да изтеглÑте или изпращате промени чрез SSH."
-
msgid "Add new directory"
msgstr "ДобавÑне на нова папка"
@@ -130,6 +130,15 @@ msgstr ""
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -139,6 +148,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "Ðрхивиран проект! Хранилището е Ñамо за четене"
@@ -166,6 +181,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикачете файл чрез влачене и пуÑкане или %{upload_link}"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -199,6 +220,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -256,7 +280,7 @@ msgstr ""
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Клон"
-msgstr[1] "Клонове"
+msgstr[1] "Клони"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "Клонът <strong>%{branch_name}</strong> беше Ñъздаден. За да наÑтроите автоматичното внедрÑване, изберете Yaml шаблон за GitLab CI и подайте промените Ñи. %{link_to_autodeploy_doc}"
@@ -264,14 +288,20 @@ msgstr "Клонът <strong>%{branch_name}</strong> беше Ñъздаден.
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
-msgstr "ТърÑете в клоновете"
+msgstr "ТърÑете в клоните"
msgid "BranchSwitcherTitle|Switch branch"
msgstr "Превключване на клона"
msgid "Branches"
-msgstr "Клонове"
+msgstr "Клони"
msgid "Branches|Cant find HEAD commit for this branch"
msgstr ""
@@ -411,6 +441,12 @@ msgstr "Графики"
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Подбиране на това подаване"
@@ -486,7 +522,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -510,21 +579,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -534,27 +636,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -567,7 +717,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -582,15 +738,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -606,9 +780,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -623,11 +803,6 @@ msgid_plural "Commits"
msgstr[0] "Подаване"
msgstr[1] "ПодаваниÑ"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
msgstr ""
@@ -709,6 +884,15 @@ msgstr "РъководÑтво за ÑътрудничеÑтво"
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -736,6 +920,9 @@ msgstr "Създаване на папка"
msgid "Create empty bare repository"
msgstr "Създаване на празно хранилище"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -763,6 +950,9 @@ msgstr "Етикет"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Ñи Ñъздадете личен жетон за доÑтъп"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "ЧаÑова зона за „Cron“"
@@ -808,6 +998,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Задайте потребителÑки шаблон, използвайки ÑинтакÑиÑа на „Cron“"
@@ -882,6 +1078,72 @@ msgstr "Редактиране на плана %{id} за Ñхема"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -921,6 +1183,12 @@ msgstr "СобÑтвеникът не може да бъде променен"
msgid "Failed to remove the pipeline schedule"
msgstr "Планът за Ñхема не може да бъде премахнат"
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -968,6 +1236,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1031,9 +1314,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1064,6 +1344,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -1123,9 +1406,6 @@ msgstr "ПредÑтавÑме Ви анализа на циклите"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr ""
@@ -1138,6 +1418,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Изключено"
@@ -1211,9 +1509,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "Медиана"
@@ -1258,9 +1565,15 @@ msgstr "Ðов план за Ñхема"
msgid "New branch"
msgstr "Ðов клон"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "Ðова папка"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Ðов файл"
@@ -1297,6 +1610,9 @@ msgstr "ÐÑма хранилище"
msgid "No schedules"
msgstr "ÐÑма планове"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1363,18 +1679,33 @@ msgstr "Ðаблюдение"
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Филтър"
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Отворен"
@@ -1507,6 +1838,9 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿"
msgid "Pipeline|with stages"
msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1612,9 +1946,15 @@ msgstr "Графика"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1651,6 +1991,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1673,7 +2046,7 @@ msgid "Readme"
msgstr "ПрочетиМе"
msgid "RefSwitcher|Branches"
-msgstr "Клонове"
+msgstr "Клони"
msgid "RefSwitcher|Tags"
msgstr "Етикети"
@@ -1747,8 +2120,11 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Планиране на Ñхемите"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
-msgstr "ТърÑете в клоновете и етикетите"
+msgstr "ТърÑете в клоните и етикетите"
msgid "Seconds before reseting failure information"
msgstr ""
@@ -1768,6 +2144,12 @@ msgstr "Изберете чаÑова зона"
msgid "Select target branch"
msgstr "Изберете целеви клон"
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1800,13 +2182,28 @@ msgid_plural "Showing %d events"
msgstr[0] "Показване на %d Ñъбитие"
msgstr[1] "Показване на %d ÑъбитиÑ"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1914,9 +2311,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "Изходен код"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1935,6 +2338,9 @@ msgstr "Създайте %{new_merge_request} Ñ Ñ‚ÐµÐ·Ð¸ промени"
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1955,6 +2361,75 @@ msgstr[1] "Етикети"
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 "Целеви клон"
@@ -1995,7 +2470,7 @@ msgid "The phase of the development lifecycle."
msgstr "Етапът от цикъла на разработка"
msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
-msgstr "Планът за Ñхемата ще изпълнÑва Ñхемите в бъдеще, периодично, за определени клонове или етикети. Тези планирани Ñхеми ще наÑледÑÑ‚ ограничениÑта на доÑтъпа до проекта на ÑÐ²ÑŠÑ€Ð·Ð°Ð½Ð¸Ñ Ñ Ñ‚ÑÑ… потребител."
+msgstr "Планът за Ñхемата ще изпълнÑва Ñхемите в бъдеще, периодично, за определени клони или етикети. Тези планирани Ñхеми ще наÑледÑÑ‚ ограничениÑта на доÑтъпа до проекта на ÑÐ²ÑŠÑ€Ð·Ð°Ð½Ð¸Ñ Ñ Ñ‚ÑÑ… потребител."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "Етапът на планиране показва колко е времето от преходната Ñтъпка до изпращането на първото подаване. Това време ще бъде добавено автоматично Ñлед като изпратите първото Ñи подаване."
@@ -2036,6 +2511,9 @@ msgstr "СтойноÑтта, коÑто Ñе намира в Ñредата нÐ
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2057,6 +2535,9 @@ msgstr "Това означава, че нÑма да можете да изпр
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Време преди един проблем да бъде планиран за работа"
@@ -2205,15 +2686,27 @@ msgstr[1] "мин"
msgid "Time|s"
msgstr "Ñек"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "Общо време"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "Общо време за теÑтване на вÑички подаваниÑ/ÑливаниÑ"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2250,6 +2743,9 @@ msgstr "Качване на файл"
msgid "UploadLink|click to upload"
msgstr "щракнете за качване"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -2283,6 +2779,9 @@ 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 "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 ""
@@ -2412,12 +2911,6 @@ msgstr "Ðа път Ñте да премахнете връзката на раÐ
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Ðа път Ñте да прехвърлите „%{project_name_with_namespace}“ към друг ÑобÑтвеник. ÐÐИСТИÐРли иÑкате това?"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "Можете да добавÑте файлове Ñамо когато Ñе намирате в клон"
@@ -2457,6 +2950,9 @@ msgstr "ÐÑма да можете да изтеглÑте или изпраща
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез SSH, докато не %{add_ssh_key_link} в профила Ñи"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2469,6 +2965,12 @@ msgstr "Вашето име"
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2494,6 +2996,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index a79a7d1a353..db7f41c5476 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-28 11:32-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:41-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -56,6 +56,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] "%{storage_name}: fehlgeschlagener Speicherzugriff auf Host:"
msgstr[1] "%{storage_name}: %{failed_attempts} fehlgeschlagene Speicherzugriffe:"
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(beachte die Informationen zur Installation auf %{link})."
@@ -115,9 +118,6 @@ msgstr ""
msgid "Add License"
msgstr "Lizenz hinzufügen"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Füge einen SSH Schlüssel zu deinem Profil hinzu, um mittels SSH zu übertragen (push) oder abzurufen (pull)."
-
msgid "Add new directory"
msgstr "Erstelle eine neues Verzeichnis"
@@ -130,6 +130,15 @@ msgstr ""
msgid "All"
msgstr "Alle"
+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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -139,6 +148,12 @@ msgstr ""
msgid "Applications"
msgstr "Anwendungen"
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "Archiviertes Projekt! Repository ist nicht änderbar."
@@ -166,6 +181,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Datei mittels Drag &amp; Drop oder %{upload_link} hinzufügen"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -199,8 +220,11 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
-msgstr "Abrechnung"
+msgstr ""
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
msgstr ""
@@ -264,6 +288,12 @@ msgstr "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatisc
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Branches durchsuchen"
@@ -411,6 +441,12 @@ msgstr "Diagramme"
msgid "Chat"
msgstr "Chat"
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Diesen Commit herauspicken "
@@ -481,12 +517,45 @@ msgid "Clone repository"
msgstr ""
msgid "Close"
-msgstr "Schließen"
+msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -510,21 +579,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -534,27 +636,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -567,7 +717,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -582,15 +738,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -606,9 +780,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -623,11 +803,6 @@ msgid_plural "Commits"
msgstr[0] "Commit"
msgstr[1] "Commits"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
msgstr ""
@@ -709,6 +884,15 @@ msgstr "Mitarbeitsanleitung"
msgid "Contributors"
msgstr "Mitarbeiter"
+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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -716,7 +900,7 @@ msgid "Control the maximum concurrency of repository backfill for this secondary
msgstr ""
msgid "Copy SSH public key to clipboard"
-msgstr "Öffentlichen SSH-Schlüssel in die Zwischenablage kopieren"
+msgstr ""
msgid "Copy URL to clipboard"
msgstr "Kopiere URL in die Zwischenablage"
@@ -736,6 +920,9 @@ msgstr "Erstelle Verzeichnis"
msgid "Create empty bare repository"
msgstr "Erstelle leeres Repository"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -763,6 +950,9 @@ msgstr "Tag "
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Erstelle einen persönlichen Zugriffstoken"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Cron Zeitzone"
@@ -808,6 +998,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Erstelle ein individuelles Muster mittels Cron Syntax"
@@ -882,6 +1078,72 @@ msgstr "Pipeline Zeitplan bearbeiten %{id}"
msgid "Emails"
msgstr "E-Mails"
+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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "Filtere alle"
@@ -921,6 +1183,12 @@ msgstr "Wechsel des Besitzers fehlgeschlagen"
msgid "Failed to remove the pipeline schedule"
msgstr "Entfernung der Pipelineplanung fehlgeschlagen"
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -968,6 +1236,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1031,9 +1314,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1064,6 +1344,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr "Systemzustand"
@@ -1123,9 +1406,6 @@ msgstr "Arbeitsablaufsanalysen vorgestellt"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr "Ticketereignisse"
@@ -1138,6 +1418,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Deaktiviert"
@@ -1192,7 +1490,7 @@ msgid "Leave project"
msgstr "Verlasse das Projekt"
msgid "License"
-msgstr "Lizenz"
+msgstr ""
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
@@ -1206,14 +1504,23 @@ msgid "Locked"
msgstr ""
msgid "Locked Files"
-msgstr "Gesperrte Dateien"
+msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "Median"
@@ -1258,9 +1565,15 @@ msgstr "Neuer Pipeline Zeitplan"
msgid "New branch"
msgstr "Neuer Branch"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "Neues Verzeichnis"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Neue Datei"
@@ -1297,6 +1610,9 @@ msgstr "Kein Repository"
msgid "No schedules"
msgstr "Keine Zeitpläne"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1363,18 +1679,33 @@ msgstr "Beobachten"
msgid "Notifications"
msgstr "Benachrichtigungen"
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filter"
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Ungelöst"
@@ -1507,6 +1838,9 @@ msgstr "mit Stage"
msgid "Pipeline|with stages"
msgstr "mit Stages"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1612,9 +1946,15 @@ msgstr "Diagramm"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1651,6 +1991,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1747,6 +2120,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Pipelines planen"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Suche nach Branches und Tags"
@@ -1768,6 +2144,12 @@ msgstr "Zeitzone auswählen"
msgid "Select target branch"
msgstr "Zielbranch auswählen"
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1800,13 +2182,28 @@ msgid_plural "Showing %d events"
msgstr[0] "Zeige %d Ereignis"
msgstr[1] "Zeige %d Ereignisse"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1914,9 +2311,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "Quellcode"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr "Spam-Protokolle"
@@ -1935,6 +2338,9 @@ msgstr "Beginne einen %{new_merge_request} mit diesen Änderungen"
msgid "Start the Runner!"
msgstr "Starte den Runner!"
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1955,6 +2361,75 @@ msgstr[1] ""
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 "Zielbranch"
@@ -2036,6 +2511,9 @@ msgstr "Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Me
msgid "There are problems accessing Git storage: "
msgstr "Es gibt ein Problem beim Zugriff auf den Gitspeicher:"
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2057,6 +2535,9 @@ msgstr "Dies bedeutet, dass Du keinen Code übertragen kannst, bevor Du kein lee
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Zeit bis ein Ticket geplant wird"
@@ -2205,15 +2686,27 @@ msgstr[1] "Min."
msgid "Time|s"
msgstr "Sek."
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "Gesamtzeit"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "Gesamte Testzeit für alle Commits/Merges"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2250,6 +2743,9 @@ msgstr "Eine Datei hochladen"
msgid "UploadLink|click to upload"
msgstr "Zum Upload klicken"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr "Benutze den folgenden Registrierungstoken während des Setups:"
@@ -2283,6 +2779,9 @@ msgstr "Du möchtest diese Daten sehen? Bitte frage einen Administrator nach dem
msgid "We don't have enough data to show this stage."
msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."
+msgid "We want to be sure it is you, please confirm you are not a robot."
+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 ""
@@ -2412,12 +2911,6 @@ msgstr "Du bist dabei, die Beziehung des Ablegers zum Ursprungsprojekt %{forked_
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Du bist dabei %{project_name_with_namespace} einem andere Besitzer zu übergeben. Bist Du dir WIRKLICH sicher?"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "Du kannst Dateien nur hinzufügen, wenn Du dich auf einem Branch befindest."
@@ -2457,6 +2950,9 @@ msgstr "Du kannst erst mittels '%{protocol}' übertragen (push) oder abrufen (pu
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Du kannst erst mittels SSH übertragen (push) oder abrufen (pull), nachdem Du Deinem Konto '%{add_ssh_key_link}'."
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2469,8 +2965,14 @@ msgstr "Dein Name"
msgid "Your projects"
msgstr "Deine Projekte"
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
-msgstr "Commit"
+msgstr ""
msgid "day"
msgid_plural "days"
@@ -2494,6 +2996,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index f7be343c4e1..be7cfa6e4b5 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-03 12:30-0400\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:42-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -56,6 +56,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] ""
msgstr[1] ""
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
@@ -115,9 +118,6 @@ msgstr ""
msgid "Add License"
msgstr "Aldoni rajtigilon"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Aldonu SSH-Ålosilon al via profilo por ebligi al vi eltiri kaj alpuÅi per SSH."
-
msgid "Add new directory"
msgstr "Aldoni novan dosierujon"
@@ -130,6 +130,15 @@ msgstr ""
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -139,6 +148,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "Arkivita projekto! La deponejo permesas nur legadon"
@@ -166,6 +181,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Alkroĉu dosieron per Åovmetado aÅ­ %{upload_link}"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -199,6 +220,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -264,6 +288,12 @@ msgstr "La branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aŭt
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Serĉu branĉon"
@@ -411,6 +441,12 @@ msgstr "Diagramoj"
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Precize elekti ĉi tiun kunmetadon"
@@ -486,7 +522,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -510,21 +579,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -534,27 +636,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -567,7 +717,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -582,15 +738,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -606,9 +780,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -623,11 +803,6 @@ msgid_plural "Commits"
msgstr[0] "Enmetado"
msgstr[1] "Enmetadoj"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
msgstr ""
@@ -709,6 +884,15 @@ msgstr "Gvidlinioj por kontribuado"
msgid "Contributors"
msgstr "Kontribuantoj"
+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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -736,6 +920,9 @@ msgstr "Krei dosierujon"
msgid "Create empty bare repository"
msgstr "Krei malplenan deponejon"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -763,6 +950,9 @@ msgstr "Etikedo"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "kreos propran atingoĵetonon"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Horzono por Cron"
@@ -808,6 +998,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Difini propran Åablonon, uzante la sintakson de Cron"
@@ -882,6 +1078,72 @@ msgstr "Redakti ĉenstablan planon %{id}"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -921,6 +1183,12 @@ msgstr "Ne eblas ÅanÄi la posedanton"
msgid "Failed to remove the pipeline schedule"
msgstr "Ne eblas forigi la ĉenstablan planon"
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -968,6 +1236,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1031,9 +1314,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1064,6 +1344,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -1123,9 +1406,6 @@ msgstr "Ni prezentas al vi la ciklan analizon"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr ""
@@ -1138,6 +1418,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "MalÅaltita"
@@ -1211,9 +1509,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
@@ -1258,9 +1565,15 @@ msgstr "Nova ĉenstabla plano"
msgid "New branch"
msgstr "Nova branĉo"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "Nova dosierujo"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Nova dosiero"
@@ -1297,6 +1610,9 @@ msgstr "Ne estas deponejo"
msgid "No schedules"
msgstr "Ne estas planoj"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1363,18 +1679,33 @@ msgstr "Rigardado"
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrilo"
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Malfermita"
@@ -1507,6 +1838,9 @@ msgstr "kun etapo"
msgid "Pipeline|with stages"
msgstr "kun etapoj"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1612,9 +1946,15 @@ msgstr "Grafeo"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1651,6 +1991,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1747,6 +2120,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Planado de la ĉenstabloj"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Serĉu branĉon aŭ etikedon"
@@ -1768,6 +2144,12 @@ msgstr "Elektu horzonon"
msgid "Select target branch"
msgstr "Elektu celan branĉon"
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1800,13 +2182,28 @@ msgid_plural "Showing %d events"
msgstr[0] "Estas montrata %d evento"
msgstr[1] "Estas montrataj %d eventoj"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1914,9 +2311,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "Kodo"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1935,6 +2338,9 @@ msgstr "Kreu %{new_merge_request} kun ĉi tiuj ÅanÄoj"
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1955,6 +2361,75 @@ msgstr[1] "Etikedoj"
msgid "Tags"
msgstr "Etikedoj"
+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 "Cela branĉo"
@@ -2036,6 +2511,9 @@ msgstr "La valoro, kiu troviÄas en la mezo de aro da rigardataj valoroj. Ekzemp
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2057,6 +2535,9 @@ msgstr "Ĉi tiu signifas, ke vi ne povos alpuÅi kodon, antaÅ­ ol vi kreos malpl
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Tempo antaÅ­ problemo estas planita por ellabori"
@@ -2205,15 +2686,27 @@ msgstr[1] "min"
msgid "Time|s"
msgstr "s"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "Totala tempo"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "Totala tempo por la testado de ĉiuj enmetadoj/kunfandoj"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2250,6 +2743,9 @@ msgstr "AlÅuti dosieron"
msgid "UploadLink|click to upload"
msgstr "alklaku por alÅuti"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -2283,6 +2779,9 @@ msgstr "Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto.
msgid "We don't have enough data to show this stage."
msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon."
+msgid "We want to be sure it is you, please confirm you are not a robot."
+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 ""
@@ -2412,12 +2911,6 @@ msgstr "Vi forigos la rilaton de la disbranĉigo al la originala projekto, „%{
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Vi estas transigonta „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas ABSOLUTE certa?"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "Oni povas aldoni dosierojn nur kiam oni estas en branĉo"
@@ -2457,6 +2950,9 @@ msgstr "Vi ne povos eltiri aÅ­ alpuÅi kodon per %{protocol} antaÅ­ ol vi %{set_
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Vi ne povos eltiri aÅ­ alpuÅi kodon per SSH antaÅ­ ol vi %{add_ssh_key_link} al via profilo"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2469,6 +2965,12 @@ msgstr "Via nomo"
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2494,6 +2996,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index c35a3503019..44ad3d4633a 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-03 12:31-0400\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:41-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -56,6 +56,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] ""
msgstr[1] ""
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
@@ -115,9 +118,6 @@ msgstr ""
msgid "Add License"
msgstr "Agregar Licencia"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."
-
msgid "Add new directory"
msgstr "Agregar nuevo directorio"
@@ -130,6 +130,15 @@ msgstr ""
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -139,6 +148,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "¡Proyecto archivado! El repositorio es de solo lectura"
@@ -166,6 +181,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Adjunte un archivo arrastrando &amp; soltando o %{upload_link}"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -199,6 +220,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -264,6 +288,12 @@ msgstr "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el a
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Buscar ramas"
@@ -411,6 +441,12 @@ msgstr "Gráficos"
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Escoger este cambio"
@@ -486,7 +522,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -510,21 +579,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -534,27 +636,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -567,7 +717,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -582,15 +738,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -606,9 +780,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -623,11 +803,6 @@ msgid_plural "Commits"
msgstr[0] "Cambio"
msgstr[1] "Cambios"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
msgstr ""
@@ -709,6 +884,15 @@ msgstr "Guía de contribución"
msgid "Contributors"
msgstr "Contribuidores"
+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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -736,6 +920,9 @@ msgstr "Crear directorio"
msgid "Create empty bare repository"
msgstr "Crear repositorio vacío"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -763,6 +950,9 @@ msgstr "Etiqueta"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "crear un token de acceso personal"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Zona horaria del Cron"
@@ -808,6 +998,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Definir un patrón personalizado con la sintaxis de cron"
@@ -882,6 +1078,72 @@ msgstr "Editar Programación del Pipeline %{id}"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -921,6 +1183,12 @@ msgstr "Error al cambiar el propietario"
msgid "Failed to remove the pipeline schedule"
msgstr "Error al eliminar la programación del pipeline"
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -968,6 +1236,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1031,9 +1314,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1064,6 +1344,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -1123,9 +1406,6 @@ msgstr "Introducción a Cycle Analytics"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr ""
@@ -1138,6 +1418,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Deshabilitado"
@@ -1211,9 +1509,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "Mediana"
@@ -1258,9 +1565,15 @@ msgstr "Nueva Programación del Pipeline"
msgid "New branch"
msgstr "Nueva rama"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "Nuevo directorio"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Nuevo archivo"
@@ -1297,6 +1610,9 @@ msgstr "No hay repositorio"
msgid "No schedules"
msgstr "No hay programaciones"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1363,18 +1679,33 @@ msgstr "Vigilancia"
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Abierto"
@@ -1507,6 +1838,9 @@ msgstr "con etapa"
msgid "Pipeline|with stages"
msgstr "con etapas"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1612,9 +1946,15 @@ msgstr "Historial gráfico"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1651,6 +1991,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1747,6 +2120,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Programación de Pipelines"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Buscar ramas y etiquetas"
@@ -1768,6 +2144,12 @@ msgstr "Selecciona una zona horaria"
msgid "Select target branch"
msgstr "Selecciona una rama de destino"
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1800,13 +2182,28 @@ msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1914,9 +2311,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "Código fuente"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1935,6 +2338,9 @@ msgstr "Iniciar una %{new_merge_request} con estos cambios"
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1955,6 +2361,75 @@ msgstr[1] "Etiquetas"
msgid "Tags"
msgstr "Etiquetas"
+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 "Rama de destino"
@@ -2036,6 +2511,9 @@ msgstr "El valor en el punto medio de una serie de valores observados. Por ejemp
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2057,6 +2535,9 @@ msgstr "Esto significa que no puede enviar código hasta que cree un repositorio
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Tiempo antes de que una incidencia sea programada"
@@ -2205,15 +2686,27 @@ msgstr[1] "mins"
msgid "Time|s"
msgstr "s"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "Tiempo Total"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "Tiempo total de pruebas para todos los cambios o integraciones"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2250,6 +2743,9 @@ msgstr "Subir archivo"
msgid "UploadLink|click to upload"
msgstr "Hacer clic para subir"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -2283,6 +2779,9 @@ msgstr "¿Quieres ver los datos? Por favor pide acceso al administrador."
msgid "We don't have enough data to show this stage."
msgstr "No hay suficientes datos para mostrar en esta etapa."
+msgid "We want to be sure it is you, please confirm you are not a robot."
+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 ""
@@ -2412,12 +2911,6 @@ msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{f
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "Solo puedes agregar archivos cuando estás en una rama"
@@ -2457,6 +2950,9 @@ msgstr "No podrás actualizar o enviar código al proyecto a través de %{protoc
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2469,6 +2965,12 @@ msgstr "Tu nombre"
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2494,6 +2996,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index a0e523339db..ace6a5d2f66 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-21 16:43-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:40-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -56,6 +56,9 @@ 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 :"
+msgid "%{text} is available"
+msgstr "%{text} est disponible"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(Lisez %{link} pour savoir comment l'installer)."
@@ -115,9 +118,6 @@ msgstr "Ajouter des Webhooks de groupe et GitLab Enterprise Edition."
msgid "Add License"
msgstr "Ajouter une licence"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser par SSH."
-
msgid "Add new directory"
msgstr "Ajouter un nouveau dossier"
@@ -130,6 +130,15 @@ msgstr "Paramètres avancés"
msgid "All"
msgstr "Tous"
+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 fetching sidebar data"
+msgstr "Une erreur s'est produite lors de la récupération des données de la barre latérale"
+
msgid "An error occurred. Please try again."
msgstr "Une erreur est survenue. Merci de réessayer."
@@ -139,6 +148,12 @@ msgstr "Apparence"
msgid "Applications"
msgstr "Applications"
+msgid "Apr"
+msgstr "Avr."
+
+msgid "April"
+msgstr "Avril"
+
msgid "Archived project! Repository is read-only"
msgstr "Projet archivé ! Le dépôt est en lecture seule"
@@ -166,6 +181,12 @@ msgstr "Artéfacts"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Attachez un fichier par glisser &amp; déposer ou %{upload_link}"
+msgid "Aug"
+msgstr "Août"
+
+msgid "August"
+msgstr "Août"
+
msgid "Authentication Log"
msgstr "Journal d'authentification"
@@ -199,6 +220,9 @@ msgstr "En savoir plus dans %{link_to_documentation}"
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "Vous pouvez activer %{link_to_settings} pour ce projet."
+msgid "Available"
+msgstr "Disponible"
+
msgid "Billing"
msgstr "Facturation"
@@ -218,7 +242,7 @@ msgid "BillingPlans|Downgrade"
msgstr "Retour à un forfait inférieur"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "En savoir plus sur chaque abonnement en lisant nos %{faq_link}."
+msgstr "En savoir plus sur chaque forfait en lisant nos %{faq_link}."
msgid "BillingPlans|Manage plan"
msgstr "Gérer l'abonnement"
@@ -227,13 +251,13 @@ msgid "BillingPlans|Please contact %{customer_support_link} in that case."
msgstr "Merci de contacter %{customer_support_link} à ce sujet."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr "Voir toutes les fonctionnalités de l’abonnement %{plan_name}"
+msgstr "Voir toutes les fonctionnalités du forfait %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Ce groupe utilise l’abonnement associé à son groupe parent."
+msgstr "Ce groupe utilise le forfait associé à son groupe parent."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Pour gérer le plan de ce groupe, visitez la section facturation de %{parent_billing_page_link}."
+msgstr "Pour gérer l‘abonnement de ce groupe, visitez la section facturation de %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
msgstr "Mise à niveau"
@@ -242,13 +266,13 @@ msgid "BillingPlans|You are currently on the %{plan_link} plan."
msgstr "Vous êtes actuellement abonné·e au forfait %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr "Foire aux questions"
+msgstr "foire aux questions"
msgid "BillingPlans|monthly"
-msgstr "Mensuellement"
+msgstr "mensuel"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "au prix annuel de %{price_per_year}"
+msgstr "payé annuellement pour %{price_per_year}"
msgid "BillingPlans|per user"
msgstr "par utilisateur"
@@ -264,6 +288,12 @@ msgstr "La branche <strong>%{branch_name}</strong> a été créée. Pour mettre
msgid "Branch has changed"
msgstr "La branche a été modifiée"
+msgid "Branch is already taken"
+msgstr "Ce nom de branche existe déjà"
+
+msgid "Branch name"
+msgstr "Nom de la branche"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Rechercher la branche"
@@ -411,6 +441,12 @@ msgstr "Statistiques"
msgid "Chat"
msgstr "Chat"
+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..."
+
msgid "Cherry-pick this commit"
msgstr "Picorer cette validation"
@@ -486,8 +522,41 @@ msgstr "Fermer"
msgid "Cluster"
msgstr "Cluster"
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr "Un %{link_to_container_project} doit avoir été créé pour ce compte"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr "%{appList} a été installé avec succès sur votre cluster"
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr "%{boldNotice} Cela va ajouter des ressources supplémentaires comme un répartiteur de charge, qui engendrent des coûts supplémentaires. Voir %{pricingLink}"
+
+msgid "ClusterIntegration|API URL"
+msgstr "URL de l'API"
+
+msgid "ClusterIntegration|Active"
+msgstr "Actif"
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr "Ajouter un cluster existant"
+
+msgid "ClusterIntegration|Add cluster"
+msgstr "Ajoutez le cluster"
+
+msgid "ClusterIntegration|All"
+msgstr "Tous"
+
+msgid "ClusterIntegration|Applications"
+msgstr "Applications"
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr "Certificat d‘autorité de certification"
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr "Paquet de l‘Autorité de certification (format PEM)"
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr "Choisissez comment configurer l‘intégration de cluster"
+
+msgid "ClusterIntegration|Cluster"
+msgstr ""
msgid "ClusterIntegration|Cluster details"
msgstr "Détails du cluster"
@@ -505,56 +574,137 @@ msgid "ClusterIntegration|Cluster integration is enabled for this project. Disab
msgstr "L'intégration de cluster est activée pour ce projet. La désactivation de cette intégration n’affectera pas votre cluster, il coupera temporairement la connexion de GitLab à celui-ci."
msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
-msgstr "Le cluster est en cours de création sur Google Kubernetes Engine…"
+msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr "Nom du cluster"
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
-msgstr "Le cluster a été correctement créé sur Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
+msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr "Copier le nom du cluster"
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr "Créer le cluster"
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
-msgstr "Créer un nouveau cluster sur Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
+msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Activer l’intégration du cluster"
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "ID de projet Google Cloud Platform"
msgid "ClusterIntegration|Google Kubernetes Engine"
-msgstr "Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
-msgstr "Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "En savoir plus sur %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr "Type de machine"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "Assurez-vous que votre compte %{link_to_requirements} pour créer des clusters"
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
-msgstr "Gérer l’intégration du cluster sur votre projet GitLab"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
+msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "Gérer votre cluster en visitant le lien %{link_gke}"
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr "Nombre de nœuds"
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
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|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "Espace de noms du projet (facultatif, unique)"
@@ -567,8 +717,14 @@ msgstr "Retirer l’intégration du cluster"
msgid "ClusterIntegration|Remove integration"
msgstr "Retirer l’intégration"
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr "Supprimer l'intégration du cluster supprimera sa configuration que vous avez ajoutée pour ce projet. Cela ne supprimera pas votre projet."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
+msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "Voir et modifier les détails de votre cluster"
@@ -582,33 +738,57 @@ msgstr "Voir vos projets"
msgid "ClusterIntegration|See zones"
msgstr "Voir les zones"
+msgid "ClusterIntegration|Service token"
+msgstr ""
+
+msgid "ClusterIntegration|Show"
+msgstr ""
+
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "Un problème est survenu de notre côté."
msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
-msgstr "Un problème est survenu lors de la création de votre cluster sur Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Activer/désactiver le cluster"
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "Avec un cluster associé à ce projet, vous pouvez utiliser des applications de revue, déployer vos applications, exécuter vos pipelines et bien plus encore, de manière très simple."
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
-msgstr "Votre compte doit disposer de %{link_to_kubernetes_engine}"
+msgstr ""
msgid "ClusterIntegration|Zone"
msgstr "Zone"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
-msgstr "Accéder à Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|cluster"
msgstr "cluster"
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr "page d’aide"
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr "répond aux exigences"
@@ -623,11 +803,6 @@ msgid_plural "Commits"
msgstr[0] "Validation"
msgstr[1] "Validations"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] "Valider %d fichier"
-msgstr[1] "Valider %d fichiers"
-
msgid "Commit Message"
msgstr "Message de validation"
@@ -709,11 +884,20 @@ msgstr "Guide de contribution"
msgid "Contributors"
msgstr "Contributeurs"
+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 "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"
+msgstr ""
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"
+msgstr ""
msgid "Copy SSH public key to clipboard"
msgstr "Copier la clé publique SSH dans le presse-papier"
@@ -736,6 +920,9 @@ msgstr "Créer un dossier"
msgid "Create empty bare repository"
msgstr "Créer un dépôt vide"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr "Créer un fichier"
@@ -763,6 +950,9 @@ msgstr "Tag"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Créer un jeton d'accès personnel"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Fuseau horaire de Cron"
@@ -808,6 +998,12 @@ msgstr "Tous"
msgid "DashboardProjects|Personal"
msgstr "Personnels"
+msgid "Dec"
+msgstr "Déc."
+
+msgid "December"
+msgstr "Décembre"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Définir un schéma personnalisé avec une syntaxe Cron"
@@ -882,6 +1078,72 @@ msgstr "Éditer le pipeline programmé %{id}"
msgid "Emails"
msgstr "Courriels"
+msgid "Environments|An error occurred while fetching the environments."
+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."
+
+msgid "Environments|Commit"
+msgstr "Validation"
+
+msgid "Environments|Deployment"
+msgstr "Déploiement"
+
+msgid "Environments|Environment"
+msgstr "Environnement"
+
+msgid "Environments|Environments"
+msgstr "Environnements"
+
+msgid "Environments|Environments are places where code gets deployed, such as staging or production."
+msgstr "Les environnements sont des lieux où le code est déployé, comme staging ou production."
+
+msgid "Environments|Job"
+msgstr "Tâche"
+
+msgid "Environments|New environment"
+msgstr "Nouvel environnement"
+
+msgid "Environments|No deployments yet"
+msgstr "Aucun déploiement pour le moment"
+
+msgid "Environments|Open"
+msgstr "Ouvert"
+
+msgid "Environments|Re-deploy"
+msgstr "Re-déployer"
+
+msgid "Environments|Read more about environments"
+msgstr "En savoir plus sur les environnements"
+
+msgid "Environments|Rollback"
+msgstr "Revenir en arrière"
+
+msgid "Environments|Show all"
+msgstr "Afficher tous"
+
+msgid "Environments|Updated"
+msgstr "Mise à 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 ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+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"
+
msgid "EventFilterBy|Filter by all"
msgstr "Aucun filtre"
@@ -921,6 +1183,12 @@ msgstr "Échec du changement de propriétaire"
msgid "Failed to remove the pipeline schedule"
msgstr "Échec de la suppression du pipeline programmé"
+msgid "Feb"
+msgstr "Févr."
+
+msgid "February"
+msgstr "Février"
+
msgid "File name"
msgstr "Nom du fichier"
@@ -968,6 +1236,21 @@ msgstr "Clés GPG"
msgid "Geo Nodes"
msgstr "NÅ“uds Geo"
+msgid "GeoNodeSyncStatus|Failed"
+msgstr "Échec"
+
+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 "GeoNodeSyncStatus|Out of sync"
+msgstr "Désynchronisé"
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr "Synchronisé"
+
msgid "Geo|File sync capacity"
msgstr "Capacité de synchronisation de fichier"
@@ -1031,9 +1314,6 @@ msgstr "Aucun groupe trouvé"
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "Vous pouvez gérer les autorisations des membres de votre groupe et accéder à chacun de ses projets."
-msgid "GroupsTreeRole|as"
-msgstr "comme"
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr "Êtes-vous sûr·e de vouloir quitter le groupe « ${this.group.fullName} » ?"
@@ -1064,6 +1344,9 @@ 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 ""
+
msgid "Health Check"
msgstr "État des services"
@@ -1092,7 +1375,7 @@ msgid "Import repository"
msgstr "Importer un dépôt"
msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr "Améliorer le tableau de tickets avec GitLab Entreprise Edition."
+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."
@@ -1123,9 +1406,6 @@ msgstr "Introduction à l'analyseur de cycle"
msgid "Issue board focus mode"
msgstr "Mode focus du tableau de tickets"
-msgid "Issue boards with milestones"
-msgstr "Tableaux des tickets avec leurs jalons"
-
msgid "Issue events"
msgstr "Événements du ticket"
@@ -1138,6 +1418,24 @@ msgstr "Tableaux"
msgid "Issues"
msgstr "Tickets"
+msgid "Jan"
+msgstr "Janv."
+
+msgid "January"
+msgstr "Janvier"
+
+msgid "Jul"
+msgstr "Juill."
+
+msgid "July"
+msgstr "Juillet"
+
+msgid "Jun"
+msgstr "Juin"
+
+msgid "June"
+msgstr "Juin"
+
msgid "LFSStatus|Disabled"
msgstr "Désactivé"
@@ -1211,9 +1509,18 @@ msgstr "Fichiers verrouillés"
msgid "Login"
msgstr "Se connecter"
+msgid "Mar"
+msgstr "Mars"
+
+msgid "March"
+msgstr "Mars"
+
msgid "Maximum git storage failures"
msgstr "Nombre maximum d’échecs du stockage git"
+msgid "May"
+msgstr "Mai"
+
msgid "Median"
msgstr "Médian"
@@ -1258,9 +1565,15 @@ msgstr "Nouveau pipeline programmé"
msgid "New branch"
msgstr "Nouvelle branche"
+msgid "New branch unavailable"
+msgstr "Nouvelle branche indisponible"
+
msgid "New directory"
msgstr "Nouveau dossier"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Nouveau fichier"
@@ -1297,6 +1610,9 @@ msgstr "Pas de dépôt"
msgid "No schedules"
msgstr "Aucun programme"
+msgid "No time spent"
+msgstr "Pas de temps passé"
+
msgid "None"
msgstr "Aucun·e"
@@ -1363,18 +1679,33 @@ msgstr "Surveillé"
msgid "Notifications"
msgstr "Notifications"
+msgid "Nov"
+msgstr "Nov."
+
+msgid "November"
+msgstr "Novembre"
+
msgid "Number of access attempts"
msgstr "Nombre de tentatives d'accès"
msgid "Number of failures before backing off"
msgstr "Nombre d'échecs avant annulation"
+msgid "Oct"
+msgstr "Oct."
+
+msgid "October"
+msgstr "Octobre"
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtre"
msgid "Only project members can comment."
msgstr "Seuls les membres du projet peuvent commenter."
+msgid "Opened"
+msgstr "Ouvert"
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Ouvert"
@@ -1507,6 +1838,9 @@ msgstr "avec l'étape"
msgid "Pipeline|with stages"
msgstr "avec les étapes"
+msgid "Please solve the reCAPTCHA"
+msgstr "Veuillez résoudre le reCAPTCHA"
+
msgid "Preferences"
msgstr "Préférences"
@@ -1612,9 +1946,15 @@ msgstr "Graphes"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr "Contactez un administrateur pour modifier ce paramètre."
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr "Exécuter immédiatement un pipeline sur la branche par défaut"
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr "Seules les validations signées peuvent être poussées sur ce dépôt."
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr "Problème lors de la configuration des paramètres CI/CD JavaScript"
+
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."
@@ -1622,7 +1962,7 @@ msgid "ProjectSettings|This setting is applied on the server level but has been
msgstr "Ce paramètre est appliqué au niveau du serveur mais il a été modifié pour ce projet."
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr "Ce paramètre s’appliquera à tous les projets à moins qu’un administrateur ne le modifie."
+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 validations qui ont été validées avec une de leurs adresses courriels vérifiées."
@@ -1651,6 +1991,39 @@ msgstr "Désolé, aucun projet ne correspond à votre recherche"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Cette fonctionnalité requiert le support du localStorage par votre navigateur"
+msgid "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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
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."
@@ -1747,6 +2120,9 @@ msgstr "Programmes"
msgid "Scheduling Pipelines"
msgstr "Programmer des pipelines"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Rechercher dans les branches et les étiquettes"
@@ -1768,6 +2144,12 @@ msgstr "Sélectionnez un fuseau horaire"
msgid "Select target branch"
msgstr "Sélectionnez une branche cible"
+msgid "Sep"
+msgstr "Sept."
+
+msgid "September"
+msgstr "Septembre"
+
msgid "Service Templates"
msgstr "Modèles de service"
@@ -1800,14 +2182,29 @@ 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|Edit"
+msgstr "Modifier"
+
+msgid "Sidebar|No"
+msgstr "Non"
+
+msgid "Sidebar|None"
+msgstr "Aucun"
+
+msgid "Sidebar|Weight"
+msgstr "Poids"
+
msgid "Snippets"
msgstr "Extraits de code"
msgid "Something went wrong on our end."
msgstr "Une erreur est survenue de notre côté."
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "Quelque chose ne s'est pas bien passé en essayant de changer l’état de verrouillage de cet ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgstr "Quelque chose ne s‘est pas bien passé en essayant de changer l’état de verrouillage de cette ${this.issuableDisplayName}"
msgid "Something went wrong while fetching the projects."
msgstr "Une erreur s'est produite lors de la récupération des projets."
@@ -1914,9 +2311,15 @@ msgstr "Commence bientôt"
msgid "SortOptions|Weight"
msgstr "Poids"
+msgid "Source"
+msgstr "Source"
+
msgid "Source code"
msgstr "Code source"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr "Journaux des messages indésirables"
@@ -1935,6 +2338,9 @@ msgstr "Créer une %{new_merge_request} avec ces changements"
msgid "Start the Runner!"
msgstr "Démarrer l'Exécuteur !"
+msgid "Stopped"
+msgstr "Arrêté"
+
msgid "Subgroups"
msgstr "Sous-groupes"
@@ -1955,6 +2361,75 @@ msgstr[1] "Tags"
msgid "Tags"
msgstr "Tags"
+msgid "TagsPage|Browse commits"
+msgstr "Parcourir les validations"
+
+msgid "TagsPage|Browse files"
+msgstr "Parcourir les fichiers"
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr "Impossible de trouver la validation HEAD pour ce tag"
+
+msgid "TagsPage|Cancel"
+msgstr "Annuler"
+
+msgid "TagsPage|Create tag"
+msgstr "Créer le tag"
+
+msgid "TagsPage|Delete tag"
+msgstr "Supprimer le tag"
+
+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 ?"
+
+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 validation existant"
+
+msgid "TagsPage|Filter by tag name"
+msgstr "Filtrer par nom de tag"
+
+msgid "TagsPage|New Tag"
+msgstr "Nouveau tag"
+
+msgid "TagsPage|New tag"
+msgstr "Nouveau tag"
+
+msgid "TagsPage|Optionally, add a message to the tag."
+msgstr "Éventuellement, ajoutez un message au tag."
+
+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."
+
+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."
+
+msgid "TagsPage|Sort by"
+msgstr "Trier par"
+
+msgid "TagsPage|Tags"
+msgstr "Tags"
+
+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"
+
+msgid "TagsPage|This tag has no release notes."
+msgstr "Ce tag 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 :"
+
+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|protected"
+msgstr "protégé"
+
msgid "Target Branch"
msgstr "Branche cible"
@@ -1962,10 +2437,10 @@ msgid "Team"
msgstr "Équipe"
msgid "Thanks! Don't show me this again"
-msgstr "Merci de ne plus afficher ce message"
+msgstr "Merci ! Ne plus afficher ce message"
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
-msgstr "La Recherche Globale Avancée de GitLab est un outils qui vous fait gagner du temps. Au lieu de créer du code similaire et perdre du temps, vous pouvez maintenant chercher dans le code d'autres équipes pour vous aider sur votre projet."
+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 "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "Le seuil d’interruption du disjoncteur devrait être inférieur au seuil de nombre de défaillance"
@@ -2036,6 +2511,9 @@ msgstr "La valeur située au point médian d’une série de valeur observée. C
msgid "There are problems accessing Git storage: "
msgstr "Il y a des difficultés à accéder aux données Git : "
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "Cette branche a changé depuis le début de l’édition. Souhaitez-vous créer une nouvelle branche ?"
@@ -2057,6 +2535,9 @@ 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 "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Temps avant qu’un ticket ne soit planifié"
@@ -2205,15 +2686,27 @@ msgstr[1] "mins"
msgid "Time|s"
msgstr "s"
+msgid "Title"
+msgstr "Titre"
+
msgid "Total Time"
msgstr "Temps total"
+msgid "Total issue time spent"
+msgstr "Temps total passé sur les tickets"
+
msgid "Total test time for all commits/merges"
msgstr "Temps total de test pour toutes les validations/fusions"
msgid "Track activity with Contribution Analytics."
msgstr "Suivre l’activité avec Contribution Analytics."
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr "Déverrouiller"
@@ -2239,7 +2732,7 @@ msgid "Upgrade your plan to activate Issue weight."
msgstr "Mettez à niveau votre abonnement pour activer les poids de ticket."
msgid "Upgrade your plan to improve Issue boards."
-msgstr "Mettez à niveau votre abonnement pour améliorer les tableaux de tickets."
+msgstr "Mettez à niveau votre forfait pour améliorer les tableaux de tickets."
msgid "Upload New File"
msgstr "Téléverser un nouveau fichier"
@@ -2250,6 +2743,9 @@ msgstr "Téléverser un fichier"
msgid "UploadLink|click to upload"
msgstr "Cliquez pour envoyer"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr "Utiliser le jeton d’inscription suivant pendant l’installation :"
@@ -2283,6 +2779,9 @@ msgstr "Vous voulez voir les données ? Merci de contacter un administrateur pou
msgid "We don't have enough data to show this stage."
msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape."
+msgid "We want to be sure it is you, please confirm you are not a robot."
+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."
@@ -2412,12 +2911,6 @@ msgstr "Vous allez supprimer la relation de fourche avec le projet source %{fork
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire. Êtes vous ABSOLUMENT sûr·e ?"
-msgid "You are on a read-only GitLab instance."
-msgstr "Vous êtes sur une instance GitLab en lecture seule."
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr "Vous êtes sur une instance GitLab en lecture seule. Si vous souhaitez apporter des modifications, vous devez aller sur %{link_to_primary_node}."
-
msgid "You can only add files when you are on a branch"
msgstr "Vous ne pouvez ajouter de fichier que dans une branche"
@@ -2457,6 +2950,9 @@ msgstr "Vous ne pourrez pas récupérer ou pousser de code par %{protocol} tant
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous n’aurez pas %{add_ssh_key_link} dans votre profil"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr "Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous n’aurez pas ajouté de clé SSH à votre profil"
+
msgid "Your comment will not be visible to the public."
msgstr "Votre commentaire ne sera pas visible publiquement."
@@ -2469,6 +2965,12 @@ msgstr "Votre nom"
msgid "Your projects"
msgstr "Vos projets"
+msgid "branch name"
+msgstr "nom de la branche"
+
+msgid "by"
+msgstr "par"
+
msgid "commit"
msgstr "validation"
@@ -2494,6 +2996,9 @@ msgstr "mot de passe"
msgid "personal access token"
msgstr "jeton d’accès personnel"
+msgid "source"
+msgstr "source"
+
msgid "to help your contributors communicate effectively!"
msgstr "pour aider vos contributeurs à communiquer efficacement !"
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 8a987129452..52bbc28ac10 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-20 03:59-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:42-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -18,13 +18,13 @@ msgstr ""
msgid "%d commit"
msgid_plural "%d commits"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d commit"
+msgstr[1] "%d commits"
msgid "%d layer"
msgid_plural "%d layers"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d livello"
+msgstr[1] "%d livelli"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -36,45 +36,48 @@ msgstr "%{commit_author_link} ha committato %{commit_timeago}"
msgid "%{count} participant"
msgid_plural "%{count} participants"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%{count} partecipante"
+msgstr[1] "%{count} partecipanti"
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "%{number_commits_behind} commits precedenti %{default_branch}, %{number_commits_ahead} commits avanti"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
-msgstr ""
+msgstr "%{number_of_failures} di %{maximum_failures} fallimenti. GitLab consentirà l'accesso al prossimo tentativo."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
-msgstr ""
+msgstr "%{number_of_failures} di %{maximum_failures} fallimenti. Gitlab bloccherà l'accesso per %{number_of_seconds} secondi."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "%{number_of_failures} di %{maximum_failures} fallimenti. Gitlab non ritenterà automaticamente. Ripristina l'informazioni d'archiviazione quando il problema è risolto."
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%{storage_name}: tentativo d'accesso all'archiviazione fallito da parte dell'host:"
+msgstr[1] "%{storage_name}: %{failed_attempts} tentativi d'accesso all'archiviazione falliti:"
+
+msgid "%{text} is available"
+msgstr "%{text} è disponibile"
msgid "(checkout the %{link} for information on how to install it)."
-msgstr ""
+msgstr "(vedi il %{link} su come installarlo)."
msgid "+ %{moreCount} more"
-msgstr ""
+msgstr "+ %{moreCount} più"
msgid "- show less"
-msgstr ""
+msgstr "- riduci"
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "1 pipeline"
+msgstr[1] "%d pipeline"
msgid "1st contribution!"
-msgstr ""
+msgstr "Primo contributo!"
msgid "2FA enabled"
-msgstr ""
+msgstr "2FA abilitata"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un insieme di grafici riguardo la Continuous Integration"
@@ -83,16 +86,16 @@ msgid "About auto deploy"
msgstr "Riguardo il rilascio automatico"
msgid "Abuse Reports"
-msgstr ""
+msgstr "Segnalazioni di abuso"
msgid "Access Tokens"
-msgstr ""
+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 ""
+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 "Account"
-msgstr ""
+msgstr "Account"
msgid "Active"
msgstr "Attivo"
@@ -115,29 +118,41 @@ msgstr ""
msgid "Add License"
msgstr "Aggiungi Licenza"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Aggiungi una chiave SSH al tuo profilo per eseguire pull o push tramite SSH"
-
msgid "Add new directory"
msgstr "Aggiungi una directory (cartella)"
msgid "AdminHealthPageLink|health page"
-msgstr ""
+msgstr "Pagina di stato"
msgid "Advanced settings"
-msgstr ""
+msgstr "Impostazioni Avanzate"
msgid "All"
+msgstr "Tutto"
+
+msgid "An error occurred when toggling the notification subscription"
+msgstr "Errore durante l'attivazione/disattivazione della sottoscrizione per l'iscrizione"
+
+msgid "An error occurred when updating the issue weight"
msgstr ""
+msgid "An error occurred while fetching sidebar data"
+msgstr "Errore durante il recupero dei dati della barra laterale"
+
msgid "An error occurred. Please try again."
-msgstr ""
+msgstr "Si è verificato un errore. Riprova."
msgid "Appearance"
-msgstr ""
+msgstr "Aspetto"
msgid "Applications"
-msgstr ""
+msgstr "Applicazioni"
+
+msgid "Apr"
+msgstr "Apr"
+
+msgid "April"
+msgstr "Aprile"
msgid "Archived project! Repository is read-only"
msgstr "Progetto archiviato! La Repository è sola-lettura"
@@ -146,58 +161,67 @@ msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Sei sicuro di voler cancellare questa pipeline programmata?"
msgid "Are you sure you want to discard your changes?"
-msgstr ""
+msgstr "Vuoi davvero ignorare le modifiche?"
msgid "Are you sure you want to leave this group?"
-msgstr ""
+msgstr "Vuoi davvero lasciare questo gruppo?"
msgid "Are you sure you want to reset registration token?"
-msgstr ""
+msgstr "Sei sicuro di voler ripristinare il token di registrazione?"
msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgstr "Confermi di voler resettare il token di controllo di stato?"
msgid "Are you sure?"
-msgstr ""
+msgstr "Sei sicuro?"
msgid "Artifacts"
-msgstr ""
+msgstr "Artefatti"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Aggiungi un file tramite trascina &amp; rilascia ( drag &amp; drop) o %{upload_link}"
+msgid "Aug"
+msgstr "Ago"
+
+msgid "August"
+msgstr "Agosto"
+
msgid "Authentication Log"
-msgstr ""
+msgstr "Log di autenticazione"
msgid "Author"
-msgstr ""
+msgstr "Autore"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Le app d'auto-review e l'Auto Deploy (rilascio automatico) necessita di un nome dominio e il servizio %{kubernetes} per funzionare correttamente."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "Le app d'auto-review e l'Auto Deploy (rilascio automatico) necessita di un nome dominio per funzionare correttamente."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Le app d'auto-review e l'Auto Deploy (rilascio automatico) necessita del servizio %{kubernetes} per funzionare correttamente."
msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr ""
+msgstr "Auto DevOps (Béta)"
msgid "AutoDevOps|Auto DevOps documentation"
-msgstr ""
+msgstr "Documentazione Auto DevOps"
msgid "AutoDevOps|Enable in settings"
-msgstr ""
+msgstr "Attiva in impostazioni"
msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
+msgstr "Farà automaticamente le build, i test e i rilasci della tua applicazione basato sulla tua configurazione CI/CD."
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
-msgstr ""
+msgstr "Approfondisci: %{link_to_documentation}"
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
-msgstr ""
+msgstr "Puoi attivare %{link_to_settings} per questo progetto."
+
+msgid "Available"
+msgstr "Disponibile"
msgid "Billing"
msgstr ""
@@ -262,7 +286,13 @@ msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy
msgstr "La branch <strong>%{branch_name}</strong> è stata creata. Per impostare un rilascio automatico scegli un template CI di Gitlab e committa le tue modifiche %{link_to_autodeploy_doc}"
msgid "Branch has changed"
-msgstr ""
+msgstr "La branche è cambiata"
+
+msgid "Branch is already taken"
+msgstr "La Branch esiste già"
+
+msgid "Branch name"
+msgstr "Nome Branch"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Cerca branches"
@@ -271,91 +301,91 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Cambia branch"
msgid "Branches"
-msgstr ""
+msgstr "Branch"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "Impossibile trovare l'HEAD commit per questa branch"
msgid "Branches|Compare"
-msgstr ""
+msgstr "Confronta"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "Elimina tutte le branches che sono state mergiate in '%{default_branch}'"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "Elimina Branch"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "Elimina branch mergiate"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "Elimina la branch protetta"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "Eliminare la branch protetta %{branch_name}?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Eliminando la branch %{branch_name} è un'operazione irreversibile. Sicuro di voler procedere?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Eliminare le branch mergiate è un'operazione irreversibile. Sicuro di voler procedere?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "Filtra per nome branch"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "Mergiata in %{default_branch}"
msgid "Branches|New branch"
-msgstr ""
+msgstr "Nuova branch"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "Nessuna branch da mostrare"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+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 ""
+msgstr "Solo gli Owner e i Master possono eliminare una branch protetta"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "Le branch protette possono esser gestite in %{project_settings_link}"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "Ordina per"
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 "La branch predefinita non può esser eliminata"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "Questa branch non è stata mergiata in %{default_branch}."
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "Per evitare perdita di dati considera di mergiare questa branch prima di eliminarla."
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+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 ""
+msgstr "Stai per eliminare la branch protetta (%{branch_name}) in maniera permanente."
msgid "Branches|diverged from upstream"
msgstr ""
msgid "Branches|merged"
-msgstr ""
+msgstr "mergiata"
msgid "Branches|project settings"
-msgstr ""
+msgstr "Impostazioni"
msgid "Branches|protected"
-msgstr ""
+msgstr "Protetta"
msgid "Browse Directory"
msgstr "Naviga direttori"
@@ -373,19 +403,19 @@ msgid "ByAuthor|by"
msgstr "per"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "Configurazione CI (Integrazione Continua)"
msgid "CICD|Jobs"
-msgstr ""
+msgstr "Jobs"
msgid "Cancel"
msgstr "Cancella"
msgid "Cancel edit"
-msgstr ""
+msgstr "Annulla modifica"
msgid "Change Weight"
msgstr ""
@@ -403,16 +433,22 @@ msgid "ChangeTypeAction|Revert"
msgstr "Ripristina"
msgid "Changelog"
-msgstr ""
+msgstr "Changelog"
msgid "Charts"
msgstr "Grafici"
msgid "Chat"
-msgstr ""
+msgstr "Chat"
+
+msgid "Checking %{text} availability…"
+msgstr "Controllo disponibilità per %{text}…"
+
+msgid "Checking branch availability..."
+msgstr "Controllo disponibilità branch..."
msgid "Cherry-pick this commit"
-msgstr ""
+msgstr "Cherry-pick di questo commit"
msgid "Cherry-pick this merge request"
msgstr "Cherry-pick questa richiesta di merge"
@@ -475,58 +511,124 @@ msgid "CiStatus|running"
msgstr "in corso"
msgid "CircuitBreakerApiLink|circuitbreaker api"
-msgstr ""
+msgstr "api circuitbreaker"
msgid "Clone repository"
-msgstr ""
+msgstr "Clona repository"
msgid "Close"
msgstr ""
msgid "Cluster"
-msgstr ""
+msgstr "Cluster"
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr ""
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr "%{appList} è stata installata con successo nel tuo cluster"
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr "%{boldNotice} Ciò richiederà delle risorse extra come un load balancer, dove si applicheranno costi aggiuntivi. Consulta %{pricingLink}"
+
+msgid "ClusterIntegration|API URL"
+msgstr "API URL"
+
+msgid "ClusterIntegration|Active"
+msgstr "Attivo"
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr "Aggiungi un cluster esistente"
+
+msgid "ClusterIntegration|Add cluster"
+msgstr "Aggiungi cluster"
+
+msgid "ClusterIntegration|All"
+msgstr "Tutti"
+
+msgid "ClusterIntegration|Applications"
+msgstr "Applicazioni"
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr "Certificato CA"
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr "Certificate Authority bundle (formato PEM)"
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr "Scegli come impostare l'integrazione cluster"
+
+msgid "ClusterIntegration|Cluster"
+msgstr "Cluster"
msgid "ClusterIntegration|Cluster details"
-msgstr ""
+msgstr "Dettagli Cluster"
msgid "ClusterIntegration|Cluster integration"
-msgstr ""
+msgstr "Integrazione Cluster"
msgid "ClusterIntegration|Cluster integration is disabled for this project."
-msgstr ""
+msgstr "L'integrazione dei cluster è disabilitata per questo progetto."
msgid "ClusterIntegration|Cluster integration is enabled for this project."
-msgstr ""
+msgstr "L'integrazione dei cluster è abilitata per questo progetto."
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr ""
+msgstr "L'integrazione dei cluster è abilitata per questo progetto. Se la disabiliti il tuo cluster non sarà modificato, sarà solo spenta la connessione a Gitlab."
msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
-msgstr ""
+msgstr "Nome Cluster"
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
msgstr ""
+msgid "ClusterIntegration|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 "I cluster di consentono di revisionare le app, effettuare rilasci, eseguire pipelines, e molto altro in modo semplice. %{link_to_help_page}"
+
+msgid "ClusterIntegration|Copy API URL"
+msgstr "Copia URL API"
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr "Copia Certificato CA"
+
+msgid "ClusterIntegration|Copy Token"
+msgstr "Copia Token"
+
msgid "ClusterIntegration|Copy cluster name"
-msgstr ""
+msgstr "Copia nome del cluster"
+
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr "Crea un nuovo cluster su Google Engine direttamente da Gitlab"
msgid "ClusterIntegration|Create cluster"
-msgstr ""
+msgstr "Crea cluster"
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Create on GKE"
+msgstr "Crea su GKE"
+
msgid "ClusterIntegration|Enable cluster integration"
-msgstr ""
+msgstr "Abilita integrazione cluster"
+
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr "Inserisci i dettagli per un cluster Kubernetes esistente"
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr "Inserisci i dettagli per il tuo cluster"
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr "Environment pattern"
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr "Prezzi GKE"
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr "Gitlab Runner"
msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr ""
+msgstr "ID Progetto di Google Cloud Platform"
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
@@ -534,27 +636,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr "Helm Tiller"
+
+msgid "ClusterIntegration|Inactive"
+msgstr "Inattivo"
+
+msgid "ClusterIntegration|Ingress"
+msgstr "Ingresso"
+
+msgid "ClusterIntegration|Install"
+msgstr "Installa"
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr "Installa applicazioni sul tuo cluster. Leggi di più a riguardo %{helpLink}"
+
+msgid "ClusterIntegration|Installed"
+msgstr "Installato"
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr "Approfondisci riguardo i Clusters"
+
msgid "ClusterIntegration|Machine type"
-msgstr ""
+msgstr "Tipo di macchina"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
-msgstr ""
+msgstr "Assicurati che il tuo account %{link_to_requirements} per creare i cluster"
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
-msgstr ""
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
+msgstr "Gestisci l'integrazione dei cluster nel tuo progetto GitLab"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "Gestisci i tuoi cluster visitando %{link_gke}"
+
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr "I cluster multipli sono disponibili nell'edizione Enterprise di Gitlab (Premium e Ultimate)"
+
+msgid "ClusterIntegration|Note:"
+msgstr "Nota:"
msgid "ClusterIntegration|Number of nodes"
+msgstr "Numero di nodi"
+
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -567,7 +717,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -582,15 +738,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -606,9 +780,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -616,20 +796,15 @@ msgid "ClusterIntegration|properly configured"
msgstr ""
msgid "Comments"
-msgstr ""
+msgstr "Commenti"
msgid "Commit"
msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
-msgstr ""
+msgstr "Messaggio di commit"
msgid "Commit duration in minutes for last 30 commits"
msgstr "Durata del commit (in minuti) per gli ultimi 30 commit"
@@ -644,7 +819,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Aggiungi %{file_name}"
msgid "Commits"
-msgstr ""
+msgstr "Commits"
msgid "Commits feed"
msgstr "Feed dei Commits"
@@ -689,19 +864,19 @@ msgid "ContainerRegistry|Remove tag"
msgstr ""
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "Dimensione"
msgid "ContainerRegistry|Tag"
-msgstr ""
+msgstr "Tag"
msgid "ContainerRegistry|Tag ID"
-msgstr ""
+msgstr "Tag ID"
msgid "ContainerRegistry|Use different image names"
-msgstr ""
+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 ""
+msgstr "Con il Docker Container Registry integrato in Gitlab, ogni progetto può avere il suo spazio d'archiviazione sulle immagini Docker."
msgid "Contribution guide"
msgstr "Guida per contribuire"
@@ -709,6 +884,15 @@ msgstr "Guida per contribuire"
msgid "Contributors"
msgstr "Collaboratori"
+msgid "ContributorsPage|Building repository graph."
+msgstr "Genero grafico della repository."
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+msgstr "Esegui i commit su %{branch_name}, escludendo i commit di merge. Limitati a 6000 commits."
+
+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 ""
@@ -736,20 +920,23 @@ msgstr "Crea cartella"
msgid "Create empty bare repository"
msgstr "Crea una repository vuota"
-msgid "Create file"
+msgid "Create epic"
msgstr ""
+msgid "Create file"
+msgstr "Crea file"
+
msgid "Create merge request"
msgstr "Crea una richiesta di merge"
msgid "Create new branch"
-msgstr ""
+msgstr "Crea un nuova branch"
msgid "Create new directory"
-msgstr ""
+msgstr "Crea una nuova cartella"
msgid "Create new file"
-msgstr ""
+msgstr "Crea un nuovo File"
msgid "Create new..."
msgstr "Crea nuovo..."
@@ -763,9 +950,12 @@ msgstr "Tag"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Crea token d'accesso personale"
-msgid "Cron Timezone"
+msgid "Creating epic"
msgstr ""
+msgid "Cron Timezone"
+msgstr "Timezone del Cron"
+
msgid "Cron syntax"
msgstr "Sintassi Cron"
@@ -803,10 +993,16 @@ msgid "CycleAnalyticsStage|Test"
msgstr "Test"
msgid "DashboardProjects|All"
-msgstr ""
+msgstr "Tutti"
msgid "DashboardProjects|Personal"
-msgstr ""
+msgstr "Personale"
+
+msgid "Dec"
+msgstr "Dic"
+
+msgid "December"
+msgstr "Dicembre"
msgid "Define a custom pattern with cron syntax"
msgstr "Definisci un patter personalizzato mediante la sintassi cron"
@@ -820,7 +1016,7 @@ msgstr[0] "Rilascio"
msgstr[1] "Rilasci"
msgid "Deploy Keys"
-msgstr ""
+msgstr "Chiavi di Deploy (rilascio)"
msgid "Description"
msgstr "Descrizione"
@@ -829,16 +1025,16 @@ msgid "Description templates allow you to define context-specific templates for
msgstr ""
msgid "Details"
-msgstr ""
+msgstr "Dettagli"
msgid "Directory name"
msgstr "Nome cartella"
msgid "Discard changes"
-msgstr ""
+msgstr "Annulla modifiche"
msgid "Dismiss Cycle Analytics introduction box"
-msgstr ""
+msgstr "Chiudi l'introduzione alle Analisi Cicliche"
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -880,25 +1076,91 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Cambia programmazione della pipeline %{id}"
msgid "Emails"
+msgstr "E-mail"
+
+msgid "Environments|An error occurred while fetching the environments."
+msgstr "Errore durante il fetch degli ambienti."
+
+msgid "Environments|An error occurred while making the request."
+msgstr "Errore durante l'esecuzione della richiesta."
+
+msgid "Environments|Commit"
+msgstr "Commit"
+
+msgid "Environments|Deployment"
+msgstr "Rilascio"
+
+msgid "Environments|Environment"
+msgstr "Ambiente"
+
+msgid "Environments|Environments"
+msgstr "Ambienti"
+
+msgid "Environments|Environments are places where code gets deployed, such as staging or production."
+msgstr "Gli ambienti sono gli spazi dove il codice viene rilasciato, come staging o produzione."
+
+msgid "Environments|Job"
+msgstr "Job"
+
+msgid "Environments|New environment"
+msgstr "Nuovo ambiente"
+
+msgid "Environments|No deployments yet"
+msgstr "Ancora nessuna chiave di rilascio"
+
+msgid "Environments|Open"
+msgstr "Apri"
+
+msgid "Environments|Re-deploy"
+msgstr "Rilascia di nuovo"
+
+msgid "Environments|Read more about environments"
+msgstr "Leggi di più sugli ambienti"
+
+msgid "Environments|Rollback"
+msgstr "Rollback (ripristina)"
+
+msgid "Environments|Show all"
+msgstr "Mostra tutti"
+
+msgid "Environments|Updated"
+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 "EventFilterBy|Filter by all"
+msgid "Epics"
msgstr ""
-msgid "EventFilterBy|Filter by comments"
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
-msgid "EventFilterBy|Filter by issue events"
+msgid "Error creating epic"
msgstr ""
+msgid "Error occurred when toggling the notification subscription"
+msgstr "Errore durante l'attivazione/disattivazione della sottoscrizione per l'iscrizione"
+
+msgid "EventFilterBy|Filter by all"
+msgstr "Filtra per tutti"
+
+msgid "EventFilterBy|Filter by comments"
+msgstr "Filtra per commenti"
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr "Filtra per eventi di issue"
+
msgid "EventFilterBy|Filter by merge events"
-msgstr ""
+msgstr "Filtra per eventi di merge"
msgid "EventFilterBy|Filter by push events"
-msgstr ""
+msgstr "Filtra per eventi di push"
msgid "EventFilterBy|Filter by team"
-msgstr ""
+msgstr "Filtra per team"
msgid "Every day (at 4:00am)"
msgstr "Ogni giorno (alle 4 del mattino)"
@@ -910,10 +1172,10 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "Ogni settimana (Di domenica alle 4 del mattino)"
msgid "Explore projects"
-msgstr ""
+msgstr "Esplora progetti"
msgid "Explore public groups"
-msgstr ""
+msgstr "Esplora gruppi pubblici"
msgid "Failed to change the owner"
msgstr "Impossibile cambiare owner"
@@ -921,11 +1183,17 @@ msgstr "Impossibile cambiare owner"
msgid "Failed to remove the pipeline schedule"
msgstr "Impossibile rimuovere la pipeline pianificata"
+msgid "Feb"
+msgstr "Feb"
+
+msgid "February"
+msgstr "Febbraio"
+
msgid "File name"
-msgstr ""
+msgstr "Nome file"
msgid "Files"
-msgstr ""
+msgstr "Files"
msgid "Filter by commit message"
msgstr "Filtra per messaggio di commit"
@@ -944,17 +1212,17 @@ msgstr "Push di"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Fork"
+msgstr[1] "Forks"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Fork da"
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
-msgstr ""
+msgstr "Fork da %{project_name} (eliminato)"
msgid "Format"
-msgstr ""
+msgstr "Formato"
msgid "From issue creation until deploy to production"
msgstr "Dalla creazione di un issue fino al rilascio in produzione"
@@ -963,11 +1231,26 @@ msgid "From merge request merge until deploy to production"
msgstr "Dalla richiesta di merge fino effettua il merge fino al rilascio in produzione"
msgid "GPG Keys"
-msgstr ""
+msgstr "Chiavi GPG"
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -981,10 +1264,10 @@ msgid "Geo|Select groups to replicate."
msgstr ""
msgid "Git storage health information has been reset"
-msgstr ""
+msgstr "Le informazioni sullo stato dell'archiviazione Git è stata ripristinata"
msgid "GitLab Runner section"
-msgstr ""
+msgstr "Sezione Gitlab Runner"
msgid "Go to your fork"
msgstr "Vai il tuo fork"
@@ -993,22 +1276,22 @@ msgid "GoToYourFork|Fork"
msgstr "Fork"
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
-msgstr ""
+msgstr "L'autenticazione Google non è %{link_to_documentation}. Richiedi al tuo amministratore Gitlab se desideri utilizzare il servizio."
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Blocca la condivisione di un progetto di %{group} con altri gruppi"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Condividi con il gruppo chiuso"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Questa modifica è stata applicata su %{ancestor_group} ed è stata ignorata nel sottogruppo."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Questa impostazione è stata applicata a %{ancestor_group}. Per condividere i progetti con altri gruppi chiedi all'owner di eseguire l'override delle impostazioni oppure %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Questa impostazione è stata applicata a %{ancestor_group}. Puoi eseguire l'override delle impostazioni o %{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 ""
@@ -1031,9 +1314,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1064,9 +1344,12 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
-msgid "Health Check"
+msgid "Have your users email"
msgstr ""
+msgid "Health Check"
+msgstr "Verifica stato"
+
msgid "Health information can be retrieved from the following endpoints. More information is available"
msgstr ""
@@ -1083,7 +1366,7 @@ msgid "HealthCheck|Unhealthy"
msgstr ""
msgid "History"
-msgstr ""
+msgstr "Cronologia"
msgid "Housekeeping successfully started"
msgstr "Housekeeping iniziato con successo"
@@ -1123,9 +1406,6 @@ msgstr "Introduzione delle Analisi Cicliche"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr ""
@@ -1138,6 +1418,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr "Gen"
+
+msgid "January"
+msgstr "Gennaio"
+
+msgid "Jul"
+msgstr "Lug"
+
+msgid "July"
+msgstr "Luglio"
+
+msgid "Jun"
+msgstr "Giu"
+
+msgid "June"
+msgstr "Giugno"
+
msgid "LFSStatus|Disabled"
msgstr "Disabilitato"
@@ -1159,16 +1457,16 @@ msgid "Last commit"
msgstr "Ultimo Commit"
msgid "Last edited %{date}"
-msgstr ""
+msgstr "Ultima modifica %{date}"
msgid "Last edited by %{name}"
-msgstr ""
+msgstr "Modificato da %{name}"
msgid "Last update"
-msgstr ""
+msgstr "Ultimo aggiornamento"
msgid "Last updated"
-msgstr ""
+msgstr "Ultimo aggiornamento"
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -1203,49 +1501,58 @@ msgid "Lock"
msgstr ""
msgid "Locked"
-msgstr ""
+msgstr "Bloccato"
msgid "Locked Files"
msgstr ""
msgid "Login"
-msgstr ""
+msgstr "Login"
+
+msgid "Mar"
+msgstr "Mar"
+
+msgid "March"
+msgstr "Marzo"
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
msgid "Members"
-msgstr ""
+msgstr "Membri"
msgid "Merge Requests"
-msgstr ""
+msgstr "Richieste di merge"
msgid "Merge events"
msgstr ""
msgid "Merge request"
-msgstr ""
+msgstr "Richiesta di merge"
msgid "Messages"
-msgstr ""
+msgstr "Messaggi"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "aggiungi una chiave SSH"
msgid "Monitoring"
-msgstr ""
+msgstr "Monitoraggio"
msgid "More information is available|here"
-msgstr ""
+msgstr "Ulteriori informazioni sono disponibili | qui"
msgid "Multiple issue boards"
msgstr ""
msgid "New Cluster"
-msgstr ""
+msgstr "Nuovo Cluster"
msgid "New Issue"
msgid_plural "New Issues"
@@ -1258,14 +1565,20 @@ msgstr "Nuova pianificazione Pipeline"
msgid "New branch"
msgstr "Nuova Branch"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "Nuova directory"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Nuovo file"
msgid "New group"
-msgstr ""
+msgstr "Nuovo gruppo"
msgid "New issue"
msgstr "Nuovo Issue"
@@ -1274,7 +1587,7 @@ msgid "New merge request"
msgstr "Nuova richiesta di merge"
msgid "New project"
-msgstr ""
+msgstr "Nuovo progetto"
msgid "New schedule"
msgstr "Nuova pianficazione"
@@ -1283,7 +1596,7 @@ msgid "New snippet"
msgstr "Nuovo snippet"
msgid "New subgroup"
-msgstr ""
+msgstr "Nuovo sottogruppo"
msgid "New tag"
msgstr "Nuovo tag"
@@ -1297,9 +1610,12 @@ msgstr "Nessuna Repository"
msgid "No schedules"
msgstr "Nessuna pianificazione"
-msgid "None"
+msgid "No time spent"
msgstr ""
+msgid "None"
+msgstr "Nessuno"
+
msgid "Not available"
msgstr "Non disponibile"
@@ -1361,55 +1677,70 @@ msgid "NotificationLevel|Watch"
msgstr "Osserva"
msgid "Notifications"
-msgstr ""
+msgstr "Notifiche"
+
+msgid "Nov"
+msgstr "Nov"
+
+msgid "November"
+msgstr "Novembre"
msgid "Number of access attempts"
-msgstr ""
+msgstr "Numero di tentativi di accesso raggiunto"
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr "Ott"
+
+msgid "October"
+msgstr "Ottobre"
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtra"
msgid "Only project members can comment."
+msgstr "Solo i membri del progetto possono commentare."
+
+msgid "Opened"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
msgstr "Aperto"
msgid "Opens in a new window"
-msgstr ""
+msgstr "Si apre in una nuova finestra"
msgid "Options"
msgstr "Opzioni"
msgid "Overview"
-msgstr ""
+msgstr "Panoramica"
msgid "Owner"
-msgstr ""
+msgstr "Proprietario"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "Ultima »"
msgid "Pagination|Next"
-msgstr ""
+msgstr "Successiva"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "Precedente"
msgid "Pagination|« First"
-msgstr ""
+msgstr "« Prima"
msgid "Password"
-msgstr ""
+msgstr "Password"
msgid "People without permission will never get a notification and won\\'t be able to comment."
-msgstr ""
+msgstr "Le persone che non hanno il permesso non saranno notificate e non potranno commentare."
msgid "Pipeline"
-msgstr ""
+msgstr "Pipeline"
msgid "Pipeline Health"
msgstr "Stato della Pipeline"
@@ -1487,13 +1818,13 @@ msgid "Pipelines charts"
msgstr "Grafici pipeline"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "Pipeline per il mese scorso"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "Pipeline per la settimana scorsa"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "Pipeline per l'ultimo anno"
msgid "Pipeline|all"
msgstr "tutto"
@@ -1507,56 +1838,59 @@ msgstr "con stadio"
msgid "Pipeline|with stages"
msgstr "con più stadi"
-msgid "Preferences"
+msgid "Please solve the reCAPTCHA"
msgstr ""
+msgid "Preferences"
+msgstr "Preferenze"
+
msgid "Private - Project access must be granted explicitly to each user."
-msgstr ""
+msgstr "Privato - L'accesso al progetto deve essere fornito esplicitamente ad ogni utente."
msgid "Private - The group and its projects can only be viewed by members."
-msgstr ""
+msgstr "Privato - Il gruppo e i suoi progetti possono essere visualizzati solo dai membri."
msgid "Profile"
-msgstr ""
+msgstr "Profilo"
msgid "Profiles|Account scheduled for removal."
-msgstr ""
+msgstr "Account pianificato per la rimozione."
msgid "Profiles|Delete Account"
-msgstr ""
+msgstr "Elimina account"
msgid "Profiles|Delete account"
-msgstr ""
+msgstr "Elimina account"
msgid "Profiles|Delete your account?"
-msgstr ""
+msgstr "Eliminare il tuo account?"
msgid "Profiles|Deleting an account has the following effects:"
msgstr ""
msgid "Profiles|Invalid password"
-msgstr ""
+msgstr "Password non valida"
msgid "Profiles|Invalid username"
-msgstr ""
+msgstr "Username non valido"
msgid "Profiles|Type your %{confirmationValue} to confirm:"
-msgstr ""
+msgstr "Inserisci il tuo %{confirmationValue} per confermare:"
msgid "Profiles|You don't have access to delete this user."
-msgstr ""
+msgstr "Non hai i permessi per eliminare questo utente."
msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
-msgstr ""
+msgstr "Devi trasferire la proprietà o eliminare questi gruppi prima che tu possa eliminare l'account."
msgid "Profiles|Your account is currently an owner in these groups:"
-msgstr ""
+msgstr "Il tuo account è attualmente proprietario in questi gruppi:"
msgid "Profiles|your account"
-msgstr ""
+msgstr "il tuo account"
msgid "Project '%{project_name}' is in the process of being deleted."
-msgstr ""
+msgstr "Il progetto '%{project_name}' è in fase di eliminazione."
msgid "Project '%{project_name}' queued for deletion."
msgstr "Il Progetto '%{project_name}' in coda di eliminazione."
@@ -1571,7 +1905,7 @@ msgid "Project access must be granted explicitly to each user."
msgstr "L'accesso al progetto dev'esser fornito esplicitamente ad ogni utente"
msgid "Project details"
-msgstr ""
+msgstr "Dettagli del progetto"
msgid "Project export could not be deleted."
msgstr "L'esportazione del progetto non può essere eliminata."
@@ -1586,7 +1920,7 @@ msgid "Project export started. A download link will be sent by email."
msgstr "Esportazione del progetto iniziata. Un link di download sarà inviato via email."
msgid "ProjectActivityRSS|Subscribe"
-msgstr ""
+msgstr "Iscriviti"
msgid "ProjectFeature|Disabled"
msgstr "Disabilitato"
@@ -1612,9 +1946,15 @@ msgstr "Grafico"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr "Esegui subito una pipeline sulla branch di default"
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr "Problemi durante l'impostazione delle CI/CD JavaScript settings"
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1628,35 +1968,68 @@ msgid "ProjectSettings|Users can only push commits to this repository that were
msgstr ""
msgid "Projects"
-msgstr ""
+msgstr "Progetti"
msgid "ProjectsDropdown|Frequently visited"
-msgstr ""
+msgstr "Visitati di frequente"
msgid "ProjectsDropdown|Loading projects"
-msgstr ""
+msgstr "Caricamento progetti"
msgid "ProjectsDropdown|Projects you visit often will appear here"
-msgstr ""
+msgstr "I progetti che visiti spesso appariranno qui"
msgid "ProjectsDropdown|Search your projects"
-msgstr ""
+msgstr "Cerca tra i tuoi progetti"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "Qualcosa è andato storto dalla nostra parte."
msgid "ProjectsDropdown|Sorry, no projects matched your search"
-msgstr ""
+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 "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 "Di default, Prometheus è in ascolto su ‘http://localhost:9090‘. Non è consigliabile cambiare l'indirizzo e la porta di default in quanto ciò potrebbe influenzare o causare conflitto con altri servizi in esecuzione sul server GitLab."
+
+msgid "PrometheusService|Finding and configuring metrics..."
+msgstr "Ricerco e configuro le metriche..."
+
+msgid "PrometheusService|Metrics"
+msgstr "Metriche"
+
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr "Le metriche sono configurate automaticamente e monitorate sulla base di una libreria di metriche di esportatori popolari."
+
+msgid "PrometheusService|Missing environment variable"
+msgstr "Variabile d'ambiente mancante"
+
+msgid "PrometheusService|Monitored"
+msgstr "Monitorato"
+
+msgid "PrometheusService|More information"
+msgstr "Ulteriori informazioni"
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr "Nessuna metrica è stata monitorata. Per iniziare a monitorare, rilascia su un ambiente."
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
-msgid "Public - The group and any public projects can be viewed without any authentication."
+msgid "PrometheusService|Prometheus monitoring"
msgstr ""
-msgid "Public - The project can be accessed without any authentication."
+msgid "PrometheusService|View environments"
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"
msgstr ""
@@ -1727,13 +2100,13 @@ msgid "Revert this merge request"
msgstr "Ripristina questa richiesta di merge"
msgid "SSH Keys"
-msgstr ""
+msgstr "Chiavi SSH"
msgid "Save"
-msgstr ""
+msgstr "Salva"
msgid "Save changes"
-msgstr ""
+msgstr "Salva modifiche"
msgid "Save pipeline schedule"
msgstr "Salva pianificazione pipeline"
@@ -1747,6 +2120,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "Pianificazione pipelines"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Ricerca branches e tags"
@@ -1768,6 +2144,12 @@ msgstr "Seleziona una timezone"
msgid "Select target branch"
msgstr "Seleziona una branch di destinazione"
+msgid "Sep"
+msgstr "Set"
+
+msgid "September"
+msgstr "Settembre"
+
msgid "Service Templates"
msgstr ""
@@ -1787,7 +2169,7 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "imposta una password"
msgid "Settings"
-msgstr ""
+msgstr "Impostazioni"
msgid "Show parent pages"
msgstr ""
@@ -1800,24 +2182,39 @@ msgid_plural "Showing %d events"
msgstr[0] "Visualizza %d evento"
msgstr[1] "Visualizza %d eventi"
-msgid "Snippets"
+msgid "Sidebar|Change weight"
msgstr ""
-msgid "Something went wrong on our end."
+msgid "Sidebar|Edit"
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sidebar|No"
msgstr ""
-msgid "Something went wrong while fetching the projects."
+msgid "Sidebar|None"
msgstr ""
-msgid "Something went wrong while fetching the registry list."
+msgid "Sidebar|Weight"
msgstr ""
-msgid "Sort by"
+msgid "Snippets"
+msgstr "Snippet"
+
+msgid "Something went wrong on our end."
+msgstr "Si è verificato un problema con il nostro server."
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
+msgid "Something went wrong while fetching the projects."
+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 "Sort by"
+msgstr "Ordina per"
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1849,7 +2246,7 @@ msgid "SortOptions|Last created"
msgstr ""
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "Ultimo che ha Joinato"
msgid "SortOptions|Last updated"
msgstr ""
@@ -1888,7 +2285,7 @@ msgid "SortOptions|Oldest created"
msgstr ""
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "Dal primo Joinato"
msgid "SortOptions|Oldest sign in"
msgstr ""
@@ -1914,9 +2311,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "Codice Sorgente"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1935,6 +2338,9 @@ msgstr "inizia una %{new_merge_request} con queste modifiche"
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1955,6 +2361,75 @@ msgstr[1] ""
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 "Branch di destinazione"
@@ -2036,6 +2511,9 @@ msgstr "Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2057,6 +2535,9 @@ msgstr "Questo significa che non è possibile effettuare push di codice fino a c
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Il tempo che impiega un issue per esser pianificato"
@@ -2205,15 +2686,27 @@ msgstr[1] "mins"
msgid "Time|s"
msgstr "s"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "Tempo Totale"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "Tempo totale di test per tutti i commits/merges"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2250,6 +2743,9 @@ msgstr "Carica file"
msgid "UploadLink|click to upload"
msgstr "clicca per caricare"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -2283,6 +2779,9 @@ msgstr "Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazi
msgid "We don't have enough data to show this stage."
msgstr "Non ci sono sufficienti dati da mostrare su questo stadio"
+msgid "We want to be sure it is you, please confirm you are not a robot."
+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 ""
@@ -2412,12 +2911,6 @@ msgstr "Stai per rimuovere la relazione con il progetto sorgente %{forked_from_p
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Stai per trasferire %{project_name_with_namespace} ad un altro owner. Sei ASSOLUTAMENTE sicuro?"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "Puoi aggiungere files solo quando sei in una branch"
@@ -2457,6 +2950,9 @@ msgstr "Non sarai in grado di eseguire pull o push di codice tramite %{protocol}
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Non sarai in grado di effettuare push o pull tramite SSH fino a che %{add_ssh_key_link} al tuo profilo"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2469,6 +2965,12 @@ msgstr "Il tuo nome"
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2494,6 +2996,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 8d93a936be9..1314bad87fe 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-03 12:31-0400\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:40-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -51,6 +51,9 @@ msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
@@ -109,9 +112,6 @@ msgstr ""
msgid "Add License"
msgstr "ライセンスを追加"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "SSHã§ãƒ—ルやプッシュã™ã‚‹å ´åˆã¯ã€ãƒ—ロフィールã«SSHéµã‚’追加ã—ã¦ãã ã•ã„。"
-
msgid "Add new directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’追加"
@@ -124,6 +124,15 @@ msgstr ""
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -133,6 +142,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "アーカイブ済ã¿ãƒ—ロジェクトï¼ï¼ˆãƒ¬ãƒã‚¸ãƒˆãƒªãƒ¼ã¯èª­ã¿å–り専用ã§ã™ï¼‰"
@@ -160,6 +175,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "ドラッグ&ドロップã¾ãŸã¯ %{upload_link} ã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -193,6 +214,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -257,6 +281,12 @@ msgstr "<strong>%{branch_name}</strong> ブランãƒãŒä½œæˆã•ã‚Œã¾ã—ãŸã€‚è
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ブランãƒã‚’検索"
@@ -404,6 +434,12 @@ msgstr "ãƒãƒ£ãƒ¼ãƒˆ"
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’ãƒã‚§ãƒªãƒ¼ãƒ”ック"
@@ -479,7 +515,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -503,21 +572,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -527,27 +629,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -560,7 +710,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -575,15 +731,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -599,9 +773,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -615,10 +795,6 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "コミット"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-
msgid "Commit Message"
msgstr ""
@@ -700,6 +876,15 @@ msgstr "貢献者å‘ã‘ガイド"
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -727,6 +912,9 @@ msgstr "ディレクトリを作æˆ"
msgid "Create empty bare repository"
msgstr "空ã®bareレãƒã‚¸ãƒˆãƒªãƒ¼ã‚’作æˆ"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -754,6 +942,9 @@ msgstr "ã‚¿ã‚°"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "個人用アクセストークンを作æˆ"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Cron ã®ã‚¿ã‚¤ãƒ ã‚¾ãƒ¼ãƒ³"
@@ -799,6 +990,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Cron 構文ã§ã‚«ã‚¹ã‚¿ãƒ ãªãƒ‘ターンを指定ã™ã‚‹"
@@ -872,6 +1069,72 @@ msgstr "パイプラインスケジュール %{id} を編集"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -911,6 +1174,12 @@ msgstr "オーナーを変更ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Failed to remove the pipeline schedule"
msgstr "パイプラインスケジュールを削除ã§ãã¾ã›ã‚“ã§ã—ãŸ"
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -957,6 +1226,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1020,9 +1304,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1053,6 +1334,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -1111,9 +1395,6 @@ msgstr "サイクル分æžã®ã”紹介"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr ""
@@ -1126,6 +1407,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "無効"
@@ -1197,9 +1496,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "中央値"
@@ -1243,9 +1551,15 @@ msgstr "æ–°è¦ãƒ‘イプラインスケジュール"
msgid "New branch"
msgstr "æ–°è¦ãƒ–ランãƒ"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "æ–°è¦ãƒ•ã‚¡ã‚¤ãƒ«"
@@ -1282,6 +1596,9 @@ msgstr "レãƒã‚¸ãƒˆãƒªãƒ¼ã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "No schedules"
msgstr "スケジュールãªã—"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1348,18 +1665,33 @@ msgstr "ã™ã¹ã¦é€šçŸ¥"
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "フィルター"
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "オープンã•ã‚ŒãŸã®ã¯"
@@ -1492,6 +1824,9 @@ msgstr "ステージã‚ã‚Š"
msgid "Pipeline|with stages"
msgstr "ステージã‚ã‚Š"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1597,9 +1932,15 @@ msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚°ãƒ©ãƒ•"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1636,6 +1977,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1732,6 +2106,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "パイプラインスケジューリング"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "ブランãƒã¾ãŸã¯ã‚¿ã‚°ã‚’検索"
@@ -1753,6 +2130,12 @@ msgstr "タイムゾーンをé¸æŠž"
msgid "Select target branch"
msgstr "ターゲットブランãƒã‚’é¸æŠž"
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1784,13 +2167,28 @@ msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示中"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1898,9 +2296,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "ソースコード"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1919,6 +2323,9 @@ msgstr "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1938,6 +2345,75 @@ msgstr[0] "ã‚¿ã‚°"
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 "ターゲットブランãƒ"
@@ -2019,6 +2495,9 @@ msgstr "得られãŸä¸€é€£ã®ãƒ‡ãƒ¼ã‚¿ã‚’å°ã•ã„é †ã«ä¸¦ã¹ãŸã¨ãã«ä¸­å¤®
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2040,6 +2519,9 @@ msgstr "空レãƒã‚¸ãƒˆãƒªãƒ¼ã‚’作æˆã¾ãŸã¯æ—¢å­˜ãƒ¬ãƒã‚¸ãƒˆãƒªãƒ¼ã‚’イン
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "課題ãŒè¨ˆç”»ã•ã‚Œã‚‹ã¾ã§ã®æ™‚é–“"
@@ -2186,15 +2668,27 @@ msgstr[0] "分"
msgid "Time|s"
msgstr "秒"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "åˆè¨ˆæ™‚é–“"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "ã™ã¹ã¦ã®ã‚³ãƒŸãƒƒãƒˆ/マージã®åˆè¨ˆãƒ†ã‚¹ãƒˆæ™‚é–“"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2231,6 +2725,9 @@ msgstr "ファイルをアップロード"
msgid "UploadLink|click to upload"
msgstr "クリックã—ã¦ã‚¢ãƒƒãƒ—ロード"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -2264,6 +2761,9 @@ 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 "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 ""
@@ -2393,12 +2893,6 @@ msgstr "å…ƒã®ãƒ—ロジェクト (%{forked_from_project}) ã¨ã®ãƒªãƒ¬ãƒ¼ã‚·ãƒ§ã
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "%{project_name_with_namespace} プロジェクトを別ã®ã‚ªãƒ¼ãƒŠãƒ¼ã«ç§»è­²ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "ファイルを追加ã™ã‚‹ã«ã¯ã€ã©ã“ã‹ã®ãƒ–ランãƒã«ã„ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“"
@@ -2438,6 +2932,9 @@ msgstr "%{set_password_link} ã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ‘スワードãŒã‚»ãƒƒãƒˆã•
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "%{add_ssh_key_link} をプロファイルã«è¿½åŠ ã—ã¦ã„ãªã„ã®ã§ã€ãƒ—ロジェクトã«ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã‚’プッシュã€ãƒ—ルã§ãã¾ã›ã‚“"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2450,6 +2947,12 @@ msgstr "åå‰"
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2473,6 +2976,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index d6c1ff2deeb..9ec3d395c15 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-03 12:31-0400\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:41-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -51,6 +51,9 @@ msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "설치 ë°©ë²•ì— ëŒ€í•œ 정보를 얻기 위해 %{link} 를 ì²´í¬ì•„웃하세요."
@@ -109,9 +112,6 @@ msgstr ""
msgid "Add License"
msgstr "ë¼ì´ì„ ìŠ¤ 추가"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "í”„ë¡œí•„ì— SSH 키를 추가하여 SSH를 통해 Pull 하거나 Push합니다."
-
msgid "Add new directory"
msgstr "새 디렉토리 추가"
@@ -124,6 +124,15 @@ msgstr ""
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -133,6 +142,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "프로ì íŠ¸ê°€ ë³´ê´€ë˜ì—ˆìŠµë‹ˆë‹¤! 저장소는 ì½ê¸°ë§Œ 가능합니다."
@@ -160,6 +175,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "드래그 &amp; 드롭 ë˜ëŠ” %{upload_link}"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -193,6 +214,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -257,6 +281,12 @@ msgstr "<strong>%{branch_name}</strong> 브랜치가 ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. ìžë
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "브랜치 검색"
@@ -404,6 +434,12 @@ msgstr "차트"
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "ì´ ì»¤ë°‹ì„ Cherry-pick"
@@ -479,7 +515,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -503,21 +572,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -527,27 +629,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -560,7 +710,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -575,15 +731,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -599,9 +773,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -615,10 +795,6 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "커밋"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-
msgid "Commit Message"
msgstr ""
@@ -700,6 +876,15 @@ msgstr "ê¸°ì—¬ì— ëŒ€í•œ 안내"
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -727,6 +912,9 @@ msgstr "디렉토리 만들기"
msgid "Create empty bare repository"
msgstr "빈 bare 저장소 만들기"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -754,6 +942,9 @@ msgstr "태그"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "ê°œì¸ ì•¡ì„¸ìŠ¤ í† í° ë§Œë“¤ê¸°"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Cron 시간대"
@@ -799,6 +990,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "cron êµ¬ë¬¸ì„ ì‚¬ìš©í•˜ì—¬ ì‚¬ìš©ìž ì •ì˜ íŒ¨í„´ ì •ì˜"
@@ -872,6 +1069,72 @@ msgstr "파ì´í”„ë¼ì¸ 스케줄 편집 %{id}"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "모든 ê°’ì„ ê¸°ì¤€ìœ¼ë¡œ í•„í„°"
@@ -911,6 +1174,12 @@ msgstr "소유ìžë¥¼ 변경하지 못했습니다"
msgid "Failed to remove the pipeline schedule"
msgstr "파ì´í”„ë¼ì¸ ìŠ¤ì¼€ì¤„ì„ ì œê±°í•˜ì§€ 못했습니다."
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -957,6 +1226,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1020,9 +1304,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1053,6 +1334,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr "헬스 ì²´í¬"
@@ -1111,9 +1395,6 @@ msgstr "Cycle Analytics 소개"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr "ì´ìŠˆ ì´ë²¤íŠ¸"
@@ -1126,6 +1407,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "Disabled"
@@ -1197,9 +1496,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "중앙값"
@@ -1243,9 +1551,15 @@ msgstr "새로운 파ì´í”„ë¼ì¸ ì¼ì •"
msgid "New branch"
msgstr "새 브랜치"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "새 디렉토리"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "새 파ì¼"
@@ -1282,6 +1596,9 @@ msgstr "저장소 ì—†ìŒ"
msgid "No schedules"
msgstr "ì¼ì • ì—†ìŒ"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1348,18 +1665,33 @@ msgstr "Watch"
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "í•„í„°"
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "열린"
@@ -1492,6 +1824,9 @@ msgstr "스테ì´ì§•"
msgid "Pipeline|with stages"
msgstr "스테ì´ì§•"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1597,9 +1932,15 @@ msgstr "그래프"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1636,6 +1977,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1732,6 +2106,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "파ì´í”„ë¼ì¸ 스케줄ë§"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "브랜치 ë° íƒœê·¸ 검색"
@@ -1753,6 +2130,12 @@ msgstr "시간대 ì„ íƒ"
msgid "Select target branch"
msgstr "ëŒ€ìƒ ë¸Œëžœì¹˜ ì„ íƒ"
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1784,13 +2167,28 @@ msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ê°œì˜ ì´ë²¤íŠ¸ 표시 중"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1898,9 +2296,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "소스 코드"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1919,6 +2323,9 @@ msgstr "ì´ ë³€ê²½ 사항으로 %{new_merge_request} ì„ ì‹œìž‘í•˜ì‹­ì‹œì˜¤."
msgid "Start the Runner!"
msgstr "Runner 시작!"
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1938,6 +2345,75 @@ msgstr[0] "태그"
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 "ëŒ€ìƒ ë¸Œëžœì¹˜"
@@ -2019,6 +2495,9 @@ msgstr "ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— 있습니다. 예를 들어, 3, 5,
msgid "There are problems accessing Git storage: "
msgstr "git storageì— ì ‘ê·¼í•˜ëŠ”ë° ë¬¸ì œê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. "
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2040,6 +2519,9 @@ msgstr "즉, 빈 저장소를 만들거나 기존 저장소를 가져올 때까ì
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "ì´ìŠˆê°€ 스케줄ë˜ê¸° ì „ì˜ ì‹œê°„"
@@ -2186,15 +2668,27 @@ msgstr[0] "분"
msgid "Time|s"
msgstr "ì´ˆ"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "시간 합계:"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "모든 커밋 / ë¨¸ì§€ì˜ ì´ í…ŒìŠ¤íŠ¸ 시간"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2231,6 +2725,9 @@ msgstr "íŒŒì¼ ì—…ë¡œë“œ"
msgid "UploadLink|click to upload"
msgstr "업로드하려면 í´ë¦­í•˜ì‹­ì‹œì˜¤."
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr "설정 ì¤‘ì— ë‹¤ìŒ ë“±ë¡ í† í° ì´ìš© : "
@@ -2264,6 +2761,9 @@ 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 "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 ""
@@ -2393,12 +2893,6 @@ msgstr "í¬í¬ 관계를 소스 프로ì íŠ¸ %{forked_from_project}ì— ëŒ€í•´ ì 
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "%{project_name_with_namespace}ì„ ë‹¤ë¥¸ 소유ìžì—게 ì´ì „하려고합니다. \"ì •ë§ë¡œ\" 확실합니까?"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "ë¸Œëžœì¹˜ì— ìžˆì„ ë•Œì—만 파ì¼ì„ 추가 í•  수 있습니다."
@@ -2438,6 +2932,9 @@ msgstr "ë‹¹ì‹ ì˜ ê³„ì •ì— %{set_password_link} ì„ í•˜ê¸° ì „ì—는 %{protocol
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "ë‹¹ì‹ ì˜ í”„ë¡œí•„ì— %{add_ssh_key_link} 를 하기 ì „ì—는 SSH를 통해 프로ì íŠ¸ 코드를 Pull 하거나 Push í•  수 없습니다"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2450,6 +2947,12 @@ msgstr "ê·€í•˜ì˜ ì´ë¦„"
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2473,6 +2976,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index 68d1f809bb4..0abb727037c 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-03 12:31-0400\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:42-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -56,6 +56,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] ""
msgstr[1] ""
+msgid "%{text} is available"
+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)."
@@ -101,7 +104,7 @@ msgid "Activity"
msgstr "Activiteit"
msgid "Add"
-msgstr "Voeg toe"
+msgstr ""
msgid "Add Changelog"
msgstr "Changelog toevoegen"
@@ -115,9 +118,6 @@ msgstr ""
msgid "Add License"
msgstr "Licentie toevoegen"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-
msgid "Add new directory"
msgstr "Nieuwe map toevoegen"
@@ -130,6 +130,15 @@ msgstr ""
msgid "All"
msgstr "Alles"
+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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -139,6 +148,12 @@ msgstr "Uiterlijk"
msgid "Applications"
msgstr "Applicaties"
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr ""
@@ -166,6 +181,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -199,8 +220,11 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
-msgstr "Facturatie"
+msgstr ""
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
msgstr ""
@@ -264,6 +288,12 @@ msgstr ""
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "BranchSwitcherPlaceholder|Zoek branches"
@@ -411,6 +441,12 @@ msgstr "Grafieken"
msgid "Chat"
msgstr "Chat"
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Cherry-pick deze commit"
@@ -486,7 +522,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -510,21 +579,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -534,27 +636,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -567,7 +717,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -582,15 +738,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -606,9 +780,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -623,11 +803,6 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
msgstr ""
@@ -709,6 +884,15 @@ msgstr ""
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -736,6 +920,9 @@ msgstr "Maak map aan"
msgid "Create empty bare repository"
msgstr ""
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -763,6 +950,9 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -808,6 +998,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr ""
@@ -882,6 +1078,72 @@ msgstr ""
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -921,6 +1183,12 @@ msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -968,6 +1236,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1031,9 +1314,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1064,6 +1344,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -1123,9 +1406,6 @@ msgstr ""
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr ""
@@ -1138,6 +1418,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -1211,9 +1509,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr ""
@@ -1258,9 +1565,15 @@ msgstr ""
msgid "New branch"
msgstr ""
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr ""
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr ""
@@ -1297,6 +1610,9 @@ msgstr ""
msgid "No schedules"
msgstr ""
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1363,18 +1679,33 @@ msgstr ""
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr ""
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Geopend"
@@ -1507,6 +1838,9 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1612,9 +1946,15 @@ msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1651,6 +1991,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1747,6 +2120,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
@@ -1768,6 +2144,12 @@ msgstr ""
msgid "Select target branch"
msgstr ""
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1800,13 +2182,28 @@ msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1914,9 +2311,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr ""
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1935,6 +2338,9 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1955,6 +2361,75 @@ msgstr[1] ""
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 ""
@@ -2036,6 +2511,9 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2057,6 +2535,9 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -2205,15 +2686,27 @@ msgstr[1] ""
msgid "Time|s"
msgstr "s"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr ""
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr ""
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2250,6 +2743,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -2283,6 +2779,9 @@ 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 "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 ""
@@ -2412,12 +2911,6 @@ msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr ""
@@ -2457,6 +2950,9 @@ 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 "Your comment will not be visible to the public."
msgstr ""
@@ -2469,6 +2965,12 @@ msgstr ""
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2494,6 +2996,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po
index c48909540b1..5b65c42097e 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-20 11:16-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:41-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Polish\n"
"Language: pl_PL\n"
@@ -61,6 +61,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
@@ -121,9 +124,6 @@ msgstr ""
msgid "Add License"
msgstr ""
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-
msgid "Add new directory"
msgstr ""
@@ -136,6 +136,15 @@ msgstr ""
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -145,6 +154,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr ""
@@ -172,6 +187,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -205,6 +226,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -271,6 +295,12 @@ msgstr ""
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
@@ -418,6 +448,12 @@ msgstr ""
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr ""
@@ -493,7 +529,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -517,21 +586,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -541,27 +643,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -574,7 +724,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -589,15 +745,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -613,9 +787,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -631,12 +811,6 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "Commit Message"
msgstr ""
@@ -718,6 +892,15 @@ msgstr ""
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -745,6 +928,9 @@ msgstr ""
msgid "Create empty bare repository"
msgstr ""
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -772,6 +958,9 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -817,6 +1006,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr ""
@@ -892,6 +1087,72 @@ msgstr ""
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -931,6 +1192,12 @@ msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -979,6 +1246,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1042,9 +1324,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1075,6 +1354,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -1135,9 +1417,6 @@ msgstr ""
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr ""
@@ -1150,6 +1429,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -1225,9 +1522,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr ""
@@ -1273,9 +1579,15 @@ msgstr ""
msgid "New branch"
msgstr ""
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr ""
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr ""
@@ -1312,6 +1624,9 @@ msgstr ""
msgid "No schedules"
msgstr ""
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1378,18 +1693,33 @@ msgstr ""
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr ""
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr ""
@@ -1522,6 +1852,9 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1627,9 +1960,15 @@ msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1666,6 +2005,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1762,6 +2134,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr ""
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
@@ -1783,6 +2158,12 @@ msgstr ""
msgid "Select target branch"
msgstr ""
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1816,13 +2197,28 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1930,9 +2326,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr ""
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1951,6 +2353,9 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1972,6 +2377,75 @@ msgstr[2] ""
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 ""
@@ -2053,6 +2527,9 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2074,6 +2551,9 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -2224,15 +2704,27 @@ msgstr[2] ""
msgid "Time|s"
msgstr ""
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr ""
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr ""
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2269,6 +2761,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -2302,6 +2797,9 @@ 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 "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 ""
@@ -2431,12 +2929,6 @@ msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr ""
@@ -2476,6 +2968,9 @@ 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 "Your comment will not be visible to the public."
msgstr ""
@@ -2488,6 +2983,12 @@ msgstr ""
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2515,6 +3016,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 78e0967c3bc..9fe1cc3c11a 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-18 12:51-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:41-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -56,6 +56,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] "%{storage_name}: falha na tentativa de acesso ao storage no host:"
msgstr[1] "%{storage_name}: %{failed_attempts} falhas de acesso ao storage:"
+msgid "%{text} is available"
+msgstr "%{text} está disponível"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(veja o %{link} para informações de como instalar)."
@@ -110,14 +113,11 @@ msgid "Add Contribution guide"
msgstr "Adicionar Guia de contribuição"
msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr "Adicione o grupo de Webhooks e GitLab Enterprise Edition."
+msgstr "Adicione o Webhooks de Grupos e GitLab Enterprise Edition."
msgid "Add License"
msgstr "Adicionar Licença"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Adicionar chave SSH ao seu perfil para fazer pull ou push via SSH."
-
msgid "Add new directory"
msgstr "Adicionar novo diretório"
@@ -130,6 +130,15 @@ msgstr "Configurações avançadas"
msgid "All"
msgstr "Todos"
+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 fetching sidebar data"
+msgstr "Erro ao recuperar informações da barra lateral"
+
msgid "An error occurred. Please try again."
msgstr "Ocorreu um erro. Tente novamente."
@@ -139,6 +148,12 @@ msgstr "Aparência"
msgid "Applications"
msgstr "Aplicações"
+msgid "Apr"
+msgstr "Abr"
+
+msgid "April"
+msgstr "Abril"
+
msgid "Archived project! Repository is read-only"
msgstr "Projeto arquivado! O repositório é somente leitura"
@@ -166,6 +181,12 @@ msgstr "Artefatos"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Para anexar arquivo, arraste e solte ou %{upload_link}"
+msgid "Aug"
+msgstr "Ago"
+
+msgid "August"
+msgstr "Agosto"
+
msgid "Authentication Log"
msgstr "Log de autenticação"
@@ -199,6 +220,9 @@ msgstr "Saiba mais em %{link_to_documentation}"
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "Você pode ativar %{link_to_settings} para esse projeto."
+msgid "Available"
+msgstr "Disponível"
+
msgid "Billing"
msgstr "Cobrança"
@@ -218,28 +242,28 @@ msgid "BillingPlans|Downgrade"
msgstr "Downgrade"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "Aprenda mais sobre cada plano lendo nossa %{faq_link}."
+msgstr "Saiba mais sobre cada plano lendo nossa %{faq_link}."
msgid "BillingPlans|Manage plan"
msgstr "Gerenciar plano"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "Por favor contacte o %{customer_support_link} para resolver seu caso."
+msgstr "Por favor, entre em contato com %{customer_support_link} para resolver seu caso."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr "Veja todas as funcionalidades do seu plano %{plan_name}"
+msgstr "Veja todas as funcionalidades de %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
msgstr "Esse grupo utiliza o plano associado ao seu grupo pai."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Para gerenciar o plano para esse grupo, visite a sessão de cobrança de %{parent_billing_page_link}."
+msgstr "Para gerenciar o plano desse grupo, visite a sessão de cobrança de %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr "Atualizar"
+msgstr "Upgrade"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "Vocês está utilizando o plano %{plan_link}."
+msgstr "Você está atualmente no plano %{plan_link}."
msgid "BillingPlans|frequently asked questions"
msgstr "perguntas frequentes"
@@ -264,6 +288,12 @@ msgstr "O branch <strong>%{branch_name}</strong> foi criado. Para configurar o d
msgid "Branch has changed"
msgstr "Branch foi alterado"
+msgid "Branch is already taken"
+msgstr "Branch já utilizada"
+
+msgid "Branch name"
+msgstr "Nome da branch"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Procurar por branches"
@@ -325,7 +355,7 @@ msgid "Branches|Sort by"
msgstr "Ordernar por"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr "A branch não pode ser atualizada automaticamente porque diverge do seu upstream."
+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"
@@ -340,13 +370,13 @@ 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."
+msgstr ""
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"
+msgstr ""
msgid "Branches|merged"
msgstr "merge realizado"
@@ -388,7 +418,7 @@ msgid "Cancel edit"
msgstr "Cancelar edição"
msgid "Change Weight"
-msgstr "Mudar peso"
+msgstr "Alterar peso"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Pick para um branch"
@@ -411,6 +441,12 @@ msgstr "Gráficos"
msgid "Chat"
msgstr "Bate-papo"
+msgid "Checking %{text} availability…"
+msgstr "Verificando disponibilidade de %{text}…"
+
+msgid "Checking branch availability..."
+msgstr "Verificando disponibilidade de branch..."
+
msgid "Cherry-pick this commit"
msgstr "Cherry-pick esse commit"
@@ -418,7 +454,7 @@ msgid "Cherry-pick this merge request"
msgstr "Cherry-pick esse merge request"
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
-msgstr "Escolha quais os grupos que você deseja replicar para este nó secundário. Deixe em branco para replicar todos."
+msgstr "Escolha quais grupos você deseja replicar para este nó secundário. Deixe em branco para replicar tudo."
msgid "CiStatusLabel|canceled"
msgstr "cancelado"
@@ -486,8 +522,41 @@ msgstr "Fechar"
msgid "Cluster"
msgstr "Cluster"
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr "Um %{link_to_container_project} deve ter sido criado com essa conta"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr "%{appList} foi instalado no seu cluster"
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr "%{boldNotice} isso irá adicionar recursos extras como balanceamento de carga, que acarretará em custos adicionais. Veja %{pricingLink}"
+
+msgid "ClusterIntegration|API URL"
+msgstr "API URL"
+
+msgid "ClusterIntegration|Active"
+msgstr "Ativo"
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr "Adicionar um cluster existente"
+
+msgid "ClusterIntegration|Add cluster"
+msgstr "Adicionar cluster"
+
+msgid "ClusterIntegration|All"
+msgstr "Tudo"
+
+msgid "ClusterIntegration|Applications"
+msgstr "Aplicações"
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr "Certificado CA"
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr "Pacote de autoridade certificadora (Formato PEM)"
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr "Escolher como configurar a integração de cluster"
+
+msgid "ClusterIntegration|Cluster"
+msgstr "Cluster"
msgid "ClusterIntegration|Cluster details"
msgstr "Detalhes do cluster"
@@ -505,56 +574,137 @@ msgid "ClusterIntegration|Cluster integration is enabled for this project. Disab
msgstr "Integração do cluster está ativada para esse projeto. Desabilitar a integração não afetará seu cluster, mas desligará temporariamente a conexão do Gitlab com ele."
msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
-msgstr "O cluster está sendo criado no Google Kubernetes Engine..."
+msgstr "O Cluster está sendo criado no Google Kubernetes Engine..."
msgid "ClusterIntegration|Cluster name"
msgstr "Nome do cluster"
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
-msgstr "O cluster foi criado com sucesso no Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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 "Clusters permitem que você utilize review apps, faça deploy de suas aplicações, rode pipelines, e muito mais de um jeito simples. %{link_to_help_page}"
+
+msgid "ClusterIntegration|Copy API URL"
+msgstr "Copiar URL da API"
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr "Copiar certificado CA"
+
+msgid "ClusterIntegration|Copy Token"
+msgstr "Copiar token"
msgid "ClusterIntegration|Copy cluster name"
msgstr "Copiar nome do cluster"
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr "Criar um novo cluster do Google Engine pelo GitLab"
+
msgid "ClusterIntegration|Create cluster"
msgstr "Criar cluster"
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
-msgstr "Criar novo cluster no Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
+msgstr "Criar no GKE"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Ativar integração com o cluster"
+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 cluster"
+msgstr "Insira os detalhes para seu cluster"
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr "Padrão de ambiente"
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr "Preços do GKE"
+
+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 Kubernetes Engine"
-msgstr "Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
-msgstr "Projeto no Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Helm Tiller"
+msgstr "Helm Tiller"
+
+msgid "ClusterIntegration|Inactive"
+msgstr "Inativo"
+
+msgid "ClusterIntegration|Ingress"
+msgstr "Ingressar"
+
+msgid "ClusterIntegration|Install"
+msgstr "Instalar"
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr "Instalar aplicações no seu cluster. Leia mais em %{helpLink}"
+
+msgid "ClusterIntegration|Installed"
+msgstr "Instalado"
+
+msgid "ClusterIntegration|Installing"
+msgstr "Instalando"
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr "Integrar cluster de automação"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "Leia mais sobre %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr "Ler mais sobre clusters"
+
msgid "ClusterIntegration|Machine type"
msgstr "Tipo de máquina"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "Confira se sua conta %{link_to_requirements} para criar clusters"
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
-msgstr "Gerenciar integração de cluster com o projeto no GitLab"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
+msgstr "Gerenciar cluster de integração no projeto do GitLab"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "Gerencie seu cluster visitando %{link_gke}"
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr "Múltiplos clusters estão disponíveis no GitLab Enterprise Premium e Ultimate"
+
+msgid "ClusterIntegration|Note:"
+msgstr "Nota:"
+
msgid "ClusterIntegration|Number of nodes"
msgstr "Número de nós"
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr "Por favor, insira informações de acesso para seu cluster. Se precisar de ajuda, você pode ler %{link_to_help_page} em cluster"
+
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|Problem setting up the cluster"
+msgstr "Problema ao configurar o cluster"
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr "Problema ao configurar a lista de cluster"
+
+msgid "ClusterIntegration|Project ID"
+msgstr "ID do projeto"
+
+msgid "ClusterIntegration|Project namespace"
+msgstr "Namespace do projeto"
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "Namespace do projeto (opcional, único)"
@@ -567,8 +717,14 @@ msgstr "Remover integração com cluster"
msgid "ClusterIntegration|Remove integration"
msgstr "Remover integração"
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr "Remover integração com o cluster irá apagar a configuração de cluster que você adicionou à esse projeto. Não excluirá seu projeto."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr "Solicitação para início de instalação falhou"
+
+msgid "ClusterIntegration|Save changes"
+msgstr "Salvar alterações"
msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "Ver e editar os detalhes para seu cluster"
@@ -582,33 +738,57 @@ msgstr "Ver seus projetos"
msgid "ClusterIntegration|See zones"
msgstr "Ver zonas"
+msgid "ClusterIntegration|Service token"
+msgstr "Token de serviço"
+
+msgid "ClusterIntegration|Show"
+msgstr "Mostrar"
+
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "Alguma coisa deu errado do nosso lado."
msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
-msgstr "Algo deu errado ao criar seu cluster no Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr "Algo deu errado ao instalar %{title}"
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr "Não há clusters para mostrar"
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr "Essa conta precisa de permissão para criar um cluster no %{link_to_container_project} especificado."
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Alternar cluster"
+msgid "ClusterIntegration|Token"
+msgstr "Token"
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "Com um cluster associado à esse projeto, você pode usar revisão de apps, fazer deploy de suas aplicações, rodar suas pipelines e muito mais de um jeito simples."
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
-msgstr "Sua conta precisa ter %{link_to_kubernetes_engine}"
+msgstr ""
msgid "ClusterIntegration|Zone"
msgstr "Zona"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
-msgstr "Acesso ao Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|cluster"
msgstr "cluster"
+msgid "ClusterIntegration|documentation"
+msgstr "documentação"
+
msgid "ClusterIntegration|help page"
msgstr "ajuda"
+msgid "ClusterIntegration|installing applications"
+msgstr "Instalando aplicações"
+
msgid "ClusterIntegration|meets the requirements"
msgstr "atende aos requisitos"
@@ -623,11 +803,6 @@ msgid_plural "Commits"
msgstr[0] "Commit"
msgstr[1] "Commits"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] "Commit %d arquivo"
-msgstr[1] "Commit %d arquivos"
-
msgid "Commit Message"
msgstr "Mensagem de Commit"
@@ -709,14 +884,23 @@ msgstr "Guia de contribuição"
msgid "Contributors"
msgstr "Contribuidores"
+msgid "ContributorsPage|Building repository graph."
+msgstr "Gerando gráfico do repositório."
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+msgstr "Commits à %{branch_name}, excluindo commits de merge. Limitado à 6000 commits."
+
+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 anexos para o nó secundário"
+msgstr ""
msgid "Control the maximum concurrency of repository backfill for this secondary node"
-msgstr "Controle a concorrência máxima de preenchimento de repositórios para o nó secundário"
+msgstr ""
msgid "Copy SSH public key to clipboard"
-msgstr "Copiar chave SSH pública para área de transferência"
+msgstr ""
msgid "Copy URL to clipboard"
msgstr "Copiar URL para área de transferência"
@@ -736,6 +920,9 @@ msgstr "Criar diretório"
msgid "Create empty bare repository"
msgstr "Criar repositório bruto vazio"
+msgid "Create epic"
+msgstr "Criar épico"
+
msgid "Create file"
msgstr "Criar arquivo"
@@ -763,6 +950,9 @@ msgstr "Tag"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "criar um token de acesso pessoal"
+msgid "Creating epic"
+msgstr "Criando épico"
+
msgid "Cron Timezone"
msgstr "Fuso horário do cron"
@@ -808,6 +998,12 @@ msgstr "Todos"
msgid "DashboardProjects|Personal"
msgstr "Pessoal"
+msgid "Dec"
+msgstr "Dez"
+
+msgid "December"
+msgstr "Dezembro"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Defina um padrão personalizado utilizando a sintaxe do cron"
@@ -826,7 +1022,7 @@ msgid "Description"
msgstr "Descrição"
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
-msgstr "Modelos de descrição permitem que você defina modelos de contextos específicos para issue e descrição de merge requests para seu projeto."
+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"
@@ -882,6 +1078,72 @@ msgstr "Alterar Agendamento do Pipeline %{id}"
msgid "Emails"
msgstr "Emails"
+msgid "Environments|An error occurred while fetching the environments."
+msgstr "Um erro ocorreu ao recuperar ambientes."
+
+msgid "Environments|An error occurred while making the request."
+msgstr "Um erro ocorreu ao fazer a requisição."
+
+msgid "Environments|Commit"
+msgstr "Commit"
+
+msgid "Environments|Deployment"
+msgstr "Deploy"
+
+msgid "Environments|Environment"
+msgstr "Ambiente"
+
+msgid "Environments|Environments"
+msgstr "Ambientes"
+
+msgid "Environments|Environments are places where code gets deployed, such as staging or production."
+msgstr "Ambientes são lugares onde códigos são implantados (deploy), como homologação ou produção."
+
+msgid "Environments|Job"
+msgstr "Job"
+
+msgid "Environments|New environment"
+msgstr "Novo ambiente"
+
+msgid "Environments|No deployments yet"
+msgstr "Nenhum deploy"
+
+msgid "Environments|Open"
+msgstr "Abrir"
+
+msgid "Environments|Re-deploy"
+msgstr "Refazer"
+
+msgid "Environments|Read more about environments"
+msgstr "Ler mais sobre ambiente"
+
+msgid "Environments|Rollback"
+msgstr "Rollback"
+
+msgid "Environments|Show all"
+msgstr "Mostrar tudo"
+
+msgid "Environments|Updated"
+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 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 creating epic"
+msgstr "Erro ao criar épico"
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr "Erro ao alterar configuração de notificação de assinatura"
+
msgid "EventFilterBy|Filter by all"
msgstr "EventFilterBy|Filtrar por tudo"
@@ -921,6 +1183,12 @@ msgstr "Erro ao alterar o proprietário"
msgid "Failed to remove the pipeline schedule"
msgstr "Erro ao excluir o agendamento do pipeline"
+msgid "Feb"
+msgstr "Fev"
+
+msgid "February"
+msgstr "Fevereiro"
+
msgid "File name"
msgstr "Nome do arquivo"
@@ -968,17 +1236,32 @@ msgstr "Chaves GPG"
msgid "Geo Nodes"
msgstr "Nós de geo"
+msgid "GeoNodeSyncStatus|Failed"
+msgstr "Falhou"
+
+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 "GeoNodeSyncStatus|Out of sync"
+msgstr "Sem sincronia"
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr "Sincronizado"
+
msgid "Geo|File sync capacity"
-msgstr "Capacidade de sincronização de arquivo"
+msgstr "Capacidade de sincronização de arquivos"
msgid "Geo|Groups to replicate"
-msgstr "Grupos para replicar"
+msgstr "Grupos para replicação"
msgid "Geo|Repository sync capacity"
-msgstr "Capacidade de sincronização de repositório"
+msgstr "Capacidade de sincronização do repositório"
msgid "Geo|Select groups to replicate."
-msgstr "Selecione grupos para replicar."
+msgstr ""
msgid "Git storage health information has been reset"
msgstr "Informações sobre o status de saúde do storage Git foram reiniciadas"
@@ -1031,9 +1314,6 @@ msgstr "Nenhum grupo encontrado"
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "Você pode gerenciar permissões de membros e acesso do seu grupo para cada projeto no grupo."
-msgid "GroupsTreeRole|as"
-msgstr "como"
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr "Você tem certeza que deseja sair do grupo \"${this.group.fullName}\"?"
@@ -1064,6 +1344,9 @@ 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 ""
+
msgid "Health Check"
msgstr "Status de Saúde"
@@ -1092,21 +1375,21 @@ msgid "Import repository"
msgstr "Importar repositório"
msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr "Melhorar issue boards com o GitLab Enterprise Edition."
+msgstr ""
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr "Melhore gestão das issues com os pesos no GitLab Enterprise Edition."
+msgstr ""
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."
+msgstr ""
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"
+msgstr[0] ""
+msgstr[1] ""
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."
@@ -1121,10 +1404,7 @@ msgid "Introducing Cycle Analytics"
msgstr "Apresentando a Análise de Ciclo"
msgid "Issue board focus mode"
-msgstr "Modo de foco no issue board"
-
-msgid "Issue boards with milestones"
-msgstr "Issue board com milestone"
+msgstr ""
msgid "Issue events"
msgstr "Eventos de issue"
@@ -1133,11 +1413,29 @@ msgid "IssueBoards|Board"
msgstr "Board"
msgid "IssueBoards|Boards"
-msgstr "Boards"
+msgstr ""
msgid "Issues"
msgstr "Issues"
+msgid "Jan"
+msgstr "Jan"
+
+msgid "January"
+msgstr "Janeiro"
+
+msgid "Jul"
+msgstr "Jul"
+
+msgid "July"
+msgstr "Julho"
+
+msgid "Jun"
+msgstr "Jun"
+
+msgid "June"
+msgstr "Junho"
+
msgid "LFSStatus|Disabled"
msgstr "Desabilitado"
@@ -1192,7 +1490,7 @@ msgid "Leave project"
msgstr "Sair do projeto"
msgid "License"
-msgstr "Licença"
+msgstr ""
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
@@ -1206,14 +1504,23 @@ msgid "Locked"
msgstr "Bloqueado"
msgid "Locked Files"
-msgstr "Arquivos bloqueados"
+msgstr ""
msgid "Login"
msgstr "Entrar"
+msgid "Mar"
+msgstr "Mar"
+
+msgid "March"
+msgstr "Março"
+
msgid "Maximum git storage failures"
msgstr "Máximo de falhas do git storage"
+msgid "May"
+msgstr "Mai"
+
msgid "Median"
msgstr "Mediana"
@@ -1242,7 +1549,7 @@ msgid "More information is available|here"
msgstr "Mais informações estão disponíveis|aqui"
msgid "Multiple issue boards"
-msgstr "Múltiplos issue boards"
+msgstr ""
msgid "New Cluster"
msgstr "Novo cluster"
@@ -1258,9 +1565,15 @@ msgstr "Novo Agendamento de Pipeline"
msgid "New branch"
msgstr "Novo branch"
+msgid "New branch unavailable"
+msgstr "Novo branch indisponível"
+
msgid "New directory"
msgstr "Novo diretório"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Novo arquivo"
@@ -1297,6 +1610,9 @@ msgstr "Nenhum repositório"
msgid "No schedules"
msgstr "Nenhum agendamento"
+msgid "No time spent"
+msgstr "Nenhum tempo gasto"
+
msgid "None"
msgstr "Nenhum"
@@ -1363,18 +1679,33 @@ msgstr "Observar"
msgid "Notifications"
msgstr "Notificações"
+msgid "Nov"
+msgstr "Nov"
+
+msgid "November"
+msgstr "Novembro"
+
msgid "Number of access attempts"
msgstr "Número de tentativas de acesso"
msgid "Number of failures before backing off"
msgstr "Número de falhas antes de reverter"
+msgid "Oct"
+msgstr "Out"
+
+msgid "October"
+msgstr "Outubro"
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
msgid "Only project members can comment."
msgstr "Somente membros do projeto podem comentar."
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Aberto"
@@ -1421,7 +1752,7 @@ msgid "Pipeline Schedules"
msgstr "Agendamentos da Pipeline"
msgid "Pipeline quota"
-msgstr "Cota de pipeline"
+msgstr ""
msgid "PipelineCharts|Failed:"
msgstr "Falhou:"
@@ -1507,6 +1838,9 @@ msgstr "com etapa"
msgid "Pipeline|with stages"
msgstr "com etapas"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr "Preferências"
@@ -1610,22 +1944,28 @@ msgid "ProjectNetworkGraph|Graph"
msgstr "Ãrvore"
msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr "Fale com um administrador para mudar essa configuração."
+msgstr ""
+
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr "Rodar pipeline na branch default imediatamente"
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
-msgstr "Esse repositório só aceita push de commits assinados."
+msgstr ""
+
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr "Problema ao definir configurações de CI/CD Javascript"
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr "Essa configuração está aplicada à nivel de servidor e pode ser sobrescrita por um adminstrador."
+msgstr ""
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr "Essa configuração está aplicada à nivel de servidor mas pode ser sobrescrita para esse projeto."
+msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr "Essa configuração será aplicada à todos os projetos, a não ser que sejam sobrescritos pelo administrador."
+msgstr ""
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr "Nesse repositório, usuários só podem fazer push de commits verificados pelos seus e-mails."
+msgstr ""
msgid "Projects"
msgstr "Projetos"
@@ -1651,6 +1991,39 @@ 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 "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|Finding and configuring metrics..."
+msgstr "Encontrando e configurando métricas..."
+
+msgid "PrometheusService|Metrics"
+msgstr "Métricas"
+
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr "Métricas são automaticamente configuradas e monitoradas baseadas na biblioteca de métricas de exportadores populares."
+
+msgid "PrometheusService|Missing environment variable"
+msgstr "Variável de ambiente ausente"
+
+msgid "PrometheusService|Monitored"
+msgstr "Monitorado"
+
+msgid "PrometheusService|More information"
+msgstr "Mais informações"
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr "Nenhuma métrica está sendo monitorada. Para inicar o monitoramento, faça deploy em um ambiente."
+
+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 monitoring"
+msgstr "Monitoramento com Prometheus"
+
+msgid "PrometheusService|View environments"
+msgstr "Ver ambientes"
+
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."
@@ -1658,13 +2031,13 @@ 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"
+msgstr ""
msgid "Push events"
msgstr "Eventos de push"
msgid "PushRule|Committer restriction"
-msgstr "Restrição de commit"
+msgstr ""
msgid "Read more"
msgstr "Leia mais"
@@ -1679,7 +2052,7 @@ msgid "RefSwitcher|Tags"
msgstr "Tags"
msgid "Registry"
-msgstr "Registry"
+msgstr "Registro"
msgid "Related Commits"
msgstr "Commits Relacionados"
@@ -1747,6 +2120,9 @@ msgstr "Agendamentos"
msgid "Scheduling Pipelines"
msgstr "Agendando pipelines"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Procurar branch e tags"
@@ -1768,6 +2144,12 @@ msgstr "Selecionar fuso horário"
msgid "Select target branch"
msgstr "Selecionar branch de destino"
+msgid "Sep"
+msgstr "Set"
+
+msgid "September"
+msgstr "Setembro"
+
msgid "Service Templates"
msgstr "Modelos de serviço"
@@ -1800,14 +2182,29 @@ msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr "Snippets"
msgid "Something went wrong on our end."
msgstr "Algo deu errado do nosso lado."
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "Algo deu errado ao tentar mudar o estado de bloqueio de ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgstr "Algo deu errado ao tentar mudar o estado de ${this.issuableDisplayName}"
msgid "Something went wrong while fetching the projects."
msgstr "Algo deu errado ao recuperar os projetos."
@@ -1858,7 +2255,7 @@ msgid "SortOptions|Least popular"
msgstr "Menos populares"
msgid "SortOptions|Less weight"
-msgstr "Menor peso"
+msgstr ""
msgid "SortOptions|Milestone"
msgstr "Milestone"
@@ -1870,7 +2267,7 @@ msgid "SortOptions|Milestone due soon"
msgstr "Milestone de fim mais próximo"
msgid "SortOptions|More weight"
-msgstr "Mais peso"
+msgstr ""
msgid "SortOptions|Most popular"
msgstr "Mais populares"
@@ -1912,11 +2309,17 @@ msgid "SortOptions|Start soon"
msgstr "Iniciar mais próximo"
msgid "SortOptions|Weight"
-msgstr "Peso"
+msgstr ""
+
+msgid "Source"
+msgstr "Origem"
msgid "Source code"
msgstr "Código-fonte"
+msgid "Source is not available"
+msgstr "Origem não está disponível"
+
msgid "Spam Logs"
msgstr "Logs de spam"
@@ -1935,6 +2338,9 @@ msgstr "Iniciar um %{new_merge_request} a partir dessas alterações"
msgid "Start the Runner!"
msgstr "Inicie o Runner!"
+msgid "Stopped"
+msgstr "Parado"
+
msgid "Subgroups"
msgstr "Subgrupos"
@@ -1955,6 +2361,75 @@ msgstr[1] "Tags"
msgid "Tags"
msgstr "Tags"
+msgid "TagsPage|Browse commits"
+msgstr "Navegar nos commits"
+
+msgid "TagsPage|Browse files"
+msgstr "Navegar nos arquivos"
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr "Não foi possível encontrar o commit HEAD para esse tag"
+
+msgid "TagsPage|Cancel"
+msgstr "Cancelar"
+
+msgid "TagsPage|Create tag"
+msgstr "Criar tag"
+
+msgid "TagsPage|Delete tag"
+msgstr "Apagar tag"
+
+msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
+msgstr "Apagar a tag %{tag_name} é uma ação destrutiva e irreversível. Tem certeza?"
+
+msgid "TagsPage|Edit release notes"
+msgstr "Editar o release notes"
+
+msgid "TagsPage|Existing branch name, tag, or commit SHA"
+msgstr "Nome de branch, tag ou SHA do commit existente"
+
+msgid "TagsPage|Filter by tag name"
+msgstr "Filtrar tag por nome"
+
+msgid "TagsPage|New Tag"
+msgstr "Novo tag"
+
+msgid "TagsPage|New tag"
+msgstr "Novo tag"
+
+msgid "TagsPage|Optionally, add a message to the tag."
+msgstr "Opcionalmente, adicionar a mensagem à essa tag."
+
+msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
+msgstr "Opcionalmente, adicionar release notes à tag. Eles serão armazenados no banco de dados do GitLab e mostrado na página de tags."
+
+msgid "TagsPage|Release notes"
+msgstr "Release notes"
+
+msgid "TagsPage|Repository has no tags yet."
+msgstr "Repositório ainda não tem tags."
+
+msgid "TagsPage|Sort by"
+msgstr "Ordenar por"
+
+msgid "TagsPage|Tags"
+msgstr "Tags"
+
+msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
+msgstr "Tags dão capacidade de marcar pontos específicos na história como sendo importantes"
+
+msgid "TagsPage|This tag has no release notes."
+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|protected"
+msgstr "protegido"
+
msgid "Target Branch"
msgstr "Branch de destino"
@@ -1962,10 +2437,10 @@ msgid "Team"
msgstr "Equipe"
msgid "Thanks! Don't show me this again"
-msgstr "Obrigado! Não mostrar novamente"
+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 "A pesquisa global avançado no GitLab é um serviço poderoso de pesquisa que poupa seu tempo. Ao invés de criar código duplicado e perder seu tempo, você pode agora pesquisar por código de outros times que podem ajudar no seu próprio projeto."
+msgstr ""
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "O limite do recuso do circuitbreaker deve ser inferior ao limite de contagem de falhas"
@@ -2036,6 +2511,9 @@ msgstr "O valor situado no ponto médio de uma série de valores observados. Ex.
msgid "There are problems accessing Git storage: "
msgstr "Há problemas para acessar o storage Git: "
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "Esse branch mudou desde quando você começou sua edição. Você quer criar um novo branch?"
@@ -2057,6 +2535,9 @@ 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 "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Tempo até que uma issue seja agendada"
@@ -2205,14 +2686,26 @@ msgstr[1] "mins"
msgid "Time|s"
msgstr "s"
+msgid "Title"
+msgstr "Título"
+
msgid "Total Time"
msgstr "Tempo Total"
+msgid "Total issue time spent"
+msgstr "Tempo total gasto"
+
msgid "Total test time for all commits/merges"
msgstr "Tempo de teste total para todos os commits/merges"
msgid "Track activity with Contribution Analytics."
-msgstr "Acompanhar atividade com o Contribution Analytics."
+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 "Turn on Service Desk"
+msgstr "Ativar Service Desk"
msgid "Unlock"
msgstr "Desbloquear"
@@ -2227,13 +2720,13 @@ msgid "Unsubscribe"
msgstr "Desassinar"
msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "Atualize seu plano para ativar a pesquisa global avançada."
+msgstr "Atualize seu plano para ativar a Pesquisa Global Avançada."
msgid "Upgrade your plan to activate Contribution Analytics."
msgstr "Atualize seu plano para ativar o Contribution Analytics."
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "Atualize seu plano para ativar Webhooks de grupo."
+msgstr "Atualize seu plano para ativar Webhooks do grupo."
msgid "Upgrade your plan to activate Issue weight."
msgstr "Atualize seu plano para ativar peso nas issues."
@@ -2250,6 +2743,9 @@ msgstr "Enviar arquivo"
msgid "UploadLink|click to upload"
msgstr "clique para fazer upload"
+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 the following registration token during setup:"
msgstr "Use o seguinte token de registro durante a configuração:"
@@ -2283,8 +2779,11 @@ msgstr "Precisa visualizar os dados? Solicite acesso ao administrador."
msgid "We don't have enough data to show this stage."
msgstr "Esta etapa não possui dados suficientes para exibição."
+msgid "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 "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
-msgstr "Webhooks permitem que você acione uma URL se, por exemplo, um novo código for feito push ou uma nova issue criada. Você pode configurar os webhooks para escutar eventos específicos como push, issue ou merge request. Webhooks de grupo aplicarão para todos os projetos no grupo, permitindo você padronizar o funcionamento em todo o grupo."
+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."
msgid "Weight"
msgstr "Peso"
@@ -2395,7 +2894,7 @@ msgid "Wiki|Wiki Pages"
msgstr "Páginas Wiki"
msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
-msgstr "Com o contribution analytics você pode ter uma visão geral da atividade em issue, merge request e evento de push para sua organização e seus membros."
+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"
@@ -2412,12 +2911,6 @@ msgstr "Você está prestes a remover a relação de fork do projeto original %{
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Você irá transferir %{project_name_with_namespace} para outro proprietário. Tem certeza ABSOLUTA?"
-msgid "You are on a read-only GitLab instance."
-msgstr "Você está em uma instância somente-leitura do GitLab."
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr "Você está em uma instância somente-leitura do GitLab. Se você quiser fazer qualquer alteração visite %{link_to_primary_node}."
-
msgid "You can only add files when you are on a branch"
msgstr "Você somente pode adicionar arquivos quando estiver em um branch"
@@ -2457,6 +2950,9 @@ msgstr "Você não poderá fazer pull ou push via %{protocol} até que %{set_pas
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Você não conseguirá fazer pull ou push no projeto via SSH até que adicione %{add_ssh_key_link} ao seu perfil"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr "Você não poderá fazer push ou pull do código via SSH enquanto não adicionar sua chave SSH no seu perfil"
+
msgid "Your comment will not be visible to the public."
msgstr "Seu comentário não estará visível ao público."
@@ -2469,6 +2965,12 @@ msgstr "Seu nome"
msgid "Your projects"
msgstr "Seus projetos"
+msgid "branch name"
+msgstr "nome da branch"
+
+msgid "by"
+msgstr "por"
+
msgid "commit"
msgstr "commit"
@@ -2494,6 +2996,9 @@ msgstr "senha"
msgid "personal access token"
msgstr "token de acesso pessoal"
+msgid "source"
+msgstr "origem"
+
msgid "to help your contributors communicate effectively!"
msgstr "para ajudar seus contribuintes à se comunicar de maneira eficaz!"
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index b25a5d1e75b..898d55e7d4e 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-17 07:55-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:40-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -61,6 +61,9 @@ msgstr[0] "%{storage_name}: Ð½ÐµÑƒÐ´Ð°Ñ‡Ð½Ð°Ñ Ð¿Ð¾Ð¿Ñ‹Ñ‚ÐºÐ° доÑтупа к
msgstr[1] "%{storage_name}: %{failed_attempts} - неудачные попытки доÑтупа к хранилищу:"
msgstr[2] "%{storage_name}: %{failed_attempts} - неудачные попытки доÑтупа к хранилищу:"
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдите по ÑÑылке %{link} Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ð¸ об уÑтановке)."
@@ -107,7 +110,7 @@ msgid "Activity"
msgstr "ÐктивноÑÑ‚ÑŒ"
msgid "Add"
-msgstr "Добавить"
+msgstr ""
msgid "Add Changelog"
msgstr "Добавить Журнал Изменений"
@@ -116,14 +119,11 @@ msgid "Add Contribution guide"
msgstr "Добавить РуководÑтво учаÑтника"
msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr "Добавить групповые веб-обработчики и GitLab Enterprise Edition."
+msgstr ""
msgid "Add License"
msgstr "Добавить Лицензию"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Добавьте ключ SSH в Ñвой профиль, чтобы отправлÑÑ‚ÑŒ или получать код через SSH."
-
msgid "Add new directory"
msgstr "Добавить новый каталог"
@@ -136,6 +136,15 @@ msgstr "РаÑширенные наÑтройки"
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr "Произошла ошибка. ПожалуйÑта, попробуйте Ñнова."
@@ -145,6 +154,12 @@ msgstr "Оформление"
msgid "Applications"
msgstr "ПриложениÑ"
+msgid "Apr"
+msgstr "Ðпр."
+
+msgid "April"
+msgstr "Ðпрель"
+
msgid "Archived project! Repository is read-only"
msgstr "Ðрхивный проект! Репозиторий доÑтупен только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ"
@@ -172,6 +187,12 @@ msgstr "Ðртефакты"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Приложить файл через drag &amp; drop или %{upload_link}"
+msgid "Aug"
+msgstr "Ðвг."
+
+msgid "August"
+msgstr "ÐвгуÑÑ‚"
+
msgid "Authentication Log"
msgstr "Журнал аутентификации"
@@ -191,7 +212,7 @@ msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr "Auto DevOps (бета)"
msgid "AutoDevOps|Auto DevOps documentation"
-msgstr ""
+msgstr "Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Auto DevOps"
msgid "AutoDevOps|Enable in settings"
msgstr "Включить в наÑтройках"
@@ -205,14 +226,17 @@ msgstr "Подробнее по ÑÑылке %{link_to_documentation}"
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "Ð’Ñ‹ можете активировать %{link_to_settings} Ð´Ð»Ñ Ñтого проекта."
+msgid "Available"
+msgstr ""
+
msgid "Billing"
-msgstr "Тариф"
+msgstr ""
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
msgstr "%{group_name} иÑпользует тарифный план %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "ÐвтоматичеÑкое повышение или понижение недоÑтупно Ð´Ð»Ñ Ð½ÐµÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… тарифных планов в наÑтоÑщее времÑ."
+msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкое повышение или понижение недоÑтупно Ð´Ð»Ñ Ð½ÐµÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… тарифных планов."
msgid "BillingPlans|Current plan"
msgstr "Текущий тарифный план"
@@ -221,10 +245,10 @@ msgid "BillingPlans|Customer Support"
msgstr "Поддержка Клиентов"
msgid "BillingPlans|Downgrade"
-msgstr "Понижение"
+msgstr "Понизить"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "Узнайте больше о каждом тарифном плане прочитав наш %{faq_link}."
+msgstr "Узнайте больше о каждом тарифном плане, прочитав нашу Ñтраницу %{faq_link}."
msgid "BillingPlans|Manage plan"
msgstr "Управление тарифным планом"
@@ -236,19 +260,19 @@ msgid "BillingPlans|See all %{plan_name} features"
msgstr "ПоÑмотреть возможноÑти %{plan_name} полноÑтью"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Эта группа иÑпользует тарифный план ÑвÑзанный Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑкой группой."
+msgstr "Эта группа иÑпользует тарифный план, ÑвÑзанный Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑкой группой."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr "Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð°Ñ€Ð¸Ñ„Ð½Ñ‹Ð¼ планом Ñтой группы поÑетите раздел тарификации %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr "Повышение"
+msgstr "ПовыÑить"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð²Ñ‹ иÑпользуете тарифный план %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr "ЧаÑто задаваемые вопроÑÑ‹"
+msgstr "чаÑто задаваемых вопроÑов"
msgid "BillingPlans|monthly"
msgstr "ежемеÑÑчно"
@@ -271,6 +295,12 @@ msgstr "Ветка <strong>%{branch_name}</strong> Ñоздана. Ð”Ð»Ñ Ð½Ð°Ñ
msgid "Branch has changed"
msgstr "Ветка была изменена"
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ПоиÑк веток"
@@ -332,7 +362,7 @@ msgid "Branches|Sort by"
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 "Ветка \"по умолчанию\" не может быть удалена"
@@ -347,13 +377,13 @@ 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 "Чтобы отменить локальные Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸ перезапиÑать ветку верÑией из родительÑкого репозиториÑ, удалите её здеÑÑŒ и выберите \"Обновить ÑейчаÑ\" выше."
+msgstr ""
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ безвозвратно удалить защищённую ветку %{branch_name}."
msgid "Branches|diverged from upstream"
-msgstr "раÑходÑÑ‚ÑÑ Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑким репозиторием"
+msgstr ""
msgid "Branches|merged"
msgstr "влита"
@@ -395,7 +425,7 @@ msgid "Cancel edit"
msgstr "Отменить редактирование"
msgid "Change Weight"
-msgstr "Изменить ВеÑ"
+msgstr ""
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Выбрать в ветке"
@@ -418,6 +448,12 @@ msgstr "Диаграммы"
msgid "Chat"
msgstr "Чат"
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Подобрать в Ñтом коммите"
@@ -425,7 +461,7 @@ msgid "Cherry-pick this merge request"
msgstr "Подобрать Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
-msgstr "Выберите группы, которые хотите Ñкопировать на вторичный узел. ОÑтавьте пуÑтым Ð´Ð»Ñ ÐºÐ¾Ð¿Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ðµ вÑего."
+msgstr ""
msgid "CiStatusLabel|canceled"
msgstr "отменено"
@@ -488,13 +524,46 @@ msgid "Clone repository"
msgstr "Клонировать репозиторий"
msgid "Close"
-msgstr "Закрыть"
+msgstr ""
msgid "Cluster"
msgstr "КлаÑтер"
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr "%{link_to_container_project} должен быть Ñоздан под Ñтой учетной запиÑью"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
+msgstr ""
msgid "ClusterIntegration|Cluster details"
msgstr "Параметры клаÑтера"
@@ -512,56 +581,137 @@ msgid "ClusterIntegration|Cluster integration is enabled for this project. Disab
msgstr "Ð”Ð»Ñ Ñтого проекта включена Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров. Отключение интеграции не повлиÑет на клаÑтер, но Ñоединение Ñ GitLab будет временно отключено."
msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
-msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер в Google Kubernetes Engine..."
+msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr "Ðазвание клаÑтера"
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
-msgstr "КлаÑтер был уÑпешно Ñоздан в Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
+msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr "Копировать название клаÑтера"
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr "Создать клаÑтер"
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
-msgstr "Создать новый клаÑтер в Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
+msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Включить интеграцию Ñ ÐºÐ»Ð°Ñтерами"
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Идентификатор проекта в Google Cloud Platform"
msgid "ClusterIntegration|Google Kubernetes Engine"
-msgstr "Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
-msgstr "Проект Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "Узнайте больше на %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr "Тип машины"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "УбедитеÑÑŒ, что ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ %{link_to_requirements} Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтеров"
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
-msgstr "Управление интеграцией клаÑтера на вашем проекте Gitlab"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
+msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "УправлÑйте клаÑтером, Ð¿ÐµÑ€ÐµÐ¹Ð´Ñ Ð¿Ð¾ ÑÑылке %{link_gke}"
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr "Примечание:"
+
msgid "ClusterIntegration|Number of nodes"
msgstr "КоличеÑтво узлов"
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "ПожалуйÑта, убедитеÑÑŒ, что ваш аккаунт Google отвечает Ñледующим требованиÑм:"
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "ПроÑтранÑтво имен проекта (необÑзательное, уникальное)"
@@ -574,8 +724,14 @@ msgstr "Удалить интеграцию Ñ ÐºÐ»Ð°Ñтером"
msgid "ClusterIntegration|Remove integration"
msgstr "Удалить интеграцию"
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr "При удалении интеграции Ñ ÐºÐ»Ð°Ñтером будет удалена ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтера, которую вы добавили в Ñтот проект. Данное дейÑтвие не удалит Ñам проект."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
+msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "ПроÑмотреть и отредактировать параметры Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ клаÑтера"
@@ -589,33 +745,57 @@ msgstr "См. ваши проекты"
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
-msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтера в Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Переключить КлаÑтер"
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "ЕÑли привÑзать клаÑтер к Ñтому проекту, вы Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью Ñможете иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ²ÑŒÑŽ, развертывать ваши приложениÑ, запуÑкать Ñборочные линии и многое другое."
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
-msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_kubernetes_engine}"
+msgstr ""
msgid "ClusterIntegration|Zone"
msgstr "Зона"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
-msgstr "доÑтуп к Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|cluster"
msgstr "клаÑтер"
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr "Ñтраница Ñправки"
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr "отвечает требованиÑм"
@@ -631,12 +811,6 @@ msgstr[0] "Коммит"
msgstr[1] "Коммиты"
msgstr[2] "Коммиты"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] "ЗафикÑировать %d файл"
-msgstr[1] "ЗафикÑировать %d файла"
-msgstr[2] "ЗафикÑировать %d файлов"
-
msgid "Commit Message"
msgstr "ОпиÑание Коммита"
@@ -718,14 +892,23 @@ msgstr "РуководÑтво учаÑтника"
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
-msgstr "Контролировать макÑимальное количеÑтво потоков фоновой загрузки LFS/вложений Ð´Ð»Ñ Ñтого вторичного узла"
+msgstr ""
msgid "Control the maximum concurrency of repository backfill for this secondary node"
-msgstr "Контролировать макÑимальное количеÑтво потоков фоновой загрузки хранилища Ð´Ð»Ñ Ñтого вторичного узла"
+msgstr ""
msgid "Copy SSH public key to clipboard"
-msgstr "Скопировать публичный ключ SSH в буфер обмена"
+msgstr ""
msgid "Copy URL to clipboard"
msgstr "Копировать URL в буфер обмена"
@@ -745,6 +928,9 @@ msgstr "Создать каталог"
msgid "Create empty bare repository"
msgstr "Создать пуÑтой репозиторий"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr "Создать файл"
@@ -772,6 +958,9 @@ msgstr "Тег"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Ñоздать перÑональный токен доÑтупа"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Ð’Ñ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ð·Ð¾Ð½Ð° Cron"
@@ -817,6 +1006,12 @@ msgstr "Ð’Ñе"
msgid "DashboardProjects|Personal"
msgstr "Личные"
+msgid "Dec"
+msgstr "Дек."
+
+msgid "December"
+msgstr "Декабрь"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Определить наÑтраиваемый шаблон Ñ ÑинтакÑиÑом cron"
@@ -836,7 +1031,7 @@ msgid "Description"
msgstr "ОпиÑание"
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
-msgstr "Шаблоны опиÑаний позволÑÑŽÑ‚ вам определить Ñпецифичные шаблоны Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð¾Ð±Ñуждений и запроÑов на ÑлиÑние в вашем проекте."
+msgstr ""
msgid "Details"
msgstr "ÐŸÐ¾Ð´Ñ€Ð¾Ð±Ð½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
@@ -851,7 +1046,7 @@ msgid "Dismiss Cycle Analytics introduction box"
msgstr "Отключить блок Ð²Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð² Ðналитику Цикла"
msgid "Dismiss Merge Request promotion"
-msgstr "Отключить анонÑÑ‹ Ð´Ð»Ñ Ð—Ð°Ð¿Ñ€Ð¾Ñов на СлиÑние"
+msgstr ""
msgid "Don't show again"
msgstr "Ðе показывать Ñнова"
@@ -892,6 +1087,72 @@ msgstr "Изменить раÑпиÑание Ñборочной линии %{id
msgid "Emails"
msgstr "Email-адреÑа"
+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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "Фильтр по вÑему"
@@ -931,6 +1192,12 @@ msgstr "Ðе удалоÑÑŒ изменить владельца"
msgid "Failed to remove the pipeline schedule"
msgstr "Ðе удалоÑÑŒ удалить раÑпиÑание Ñборочной линии"
+msgid "Feb"
+msgstr "Фев."
+
+msgid "February"
+msgstr "Февраль"
+
msgid "File name"
msgstr "Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°"
@@ -977,19 +1244,34 @@ msgid "GPG Keys"
msgstr "GPG Ключи"
msgid "Geo Nodes"
-msgstr "ГеографичеÑкие Узлы"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
msgid "Geo|File sync capacity"
-msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации файлов"
+msgstr ""
msgid "Geo|Groups to replicate"
-msgstr "Группы Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸"
+msgstr ""
msgid "Geo|Repository sync capacity"
-msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации репозиториÑ"
+msgstr ""
msgid "Geo|Select groups to replicate."
-msgstr "Выберите группы Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸."
+msgstr ""
msgid "Git storage health information has been reset"
msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑтабильноÑти Git хранилища была Ñброшена"
@@ -1042,9 +1324,6 @@ msgstr "Группы не найдены"
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "Ð’Ñ‹ можете управлÑÑ‚ÑŒ правами и доÑтупом учаÑтников вашей группы к каждому проекту в группе."
-msgid "GroupsTreeRole|as"
-msgstr "как"
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr "Вы уверены, что вы хотите покинуть группу \"${this.group.fullName}\"?"
@@ -1075,6 +1354,9 @@ msgstr "К Ñожалению, по вашему запроÑу групп не
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "К Ñожалению, по вашему запроÑу групп или проектов не найдено"
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr "Проверка работоÑпоÑобноÑти"
@@ -1103,22 +1385,22 @@ msgid "Import repository"
msgstr "Импорт репозиториÑ"
msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr "Улучшить доÑки обÑуждений Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ верÑии GitLab Enterprise Edition."
+msgstr ""
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr "Улучшить управление обÑуждениÑми Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñтью Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ð²ÐµÑа обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸ помощи GitLab Enterprise Edition."
+msgstr ""
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
-msgstr "Улучшить поиÑк при помощи РаÑширенного Глобального ПоиÑка в верÑии GitLab Enterprise Edition."
+msgstr ""
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[0] ""
+msgstr[1] ""
+msgstr[2] ""
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "Внутренний - Группу и включённые в неё проекты может видеть любой зарегиÑтрированный пользователь."
@@ -1133,10 +1415,7 @@ msgid "Introducing Cycle Analytics"
msgstr "Внедрение Цикла Ðналитик"
msgid "Issue board focus mode"
-msgstr "Режим фокуÑировки над доÑкой обÑуждений"
-
-msgid "Issue boards with milestones"
-msgstr "ДоÑки обÑуждений Ñ Ð²ÐµÑ…Ð°Ð¼Ð¸"
+msgstr ""
msgid "Issue events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ð±Ñуждений"
@@ -1145,11 +1424,29 @@ msgid "IssueBoards|Board"
msgstr "ДоÑка"
msgid "IssueBoards|Boards"
-msgstr "ДоÑки"
+msgstr ""
msgid "Issues"
msgstr "ОбÑуждениÑ"
+msgid "Jan"
+msgstr "Янв."
+
+msgid "January"
+msgstr "Январь"
+
+msgid "Jul"
+msgstr "Июл."
+
+msgid "July"
+msgstr "Июль"
+
+msgid "Jun"
+msgstr "Июн."
+
+msgid "June"
+msgstr "Июнь"
+
msgid "LFSStatus|Disabled"
msgstr "Отключено"
@@ -1205,7 +1502,7 @@ msgid "Leave project"
msgstr "Покинуть проект"
msgid "License"
-msgstr "ЛицензиÑ"
+msgstr ""
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
@@ -1220,14 +1517,23 @@ msgid "Locked"
msgstr "Заблокировано"
msgid "Locked Files"
-msgstr "Заблокированные Файлы"
+msgstr ""
msgid "Login"
msgstr "Войти"
+msgid "Mar"
+msgstr "Мар."
+
+msgid "March"
+msgstr "Март"
+
msgid "Maximum git storage failures"
msgstr "МакÑимальное количеÑтво Ñбоев хранилища git"
+msgid "May"
+msgstr "Май"
+
msgid "Median"
msgstr "Среднее"
@@ -1256,7 +1562,7 @@ msgid "More information is available|here"
msgstr "Больше информации доÑтупно|тут"
msgid "Multiple issue boards"
-msgstr "Сводные доÑки задач"
+msgstr ""
msgid "New Cluster"
msgstr "Ðовый КлаÑтер"
@@ -1273,9 +1579,15 @@ msgstr "Ðовое РаÑпиÑание Сборочной Линии"
msgid "New branch"
msgstr "ÐÐ¾Ð²Ð°Ñ Ð²ÐµÑ‚ÐºÐ°"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "Ðовый каталог"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "Ðовый файл"
@@ -1312,6 +1624,9 @@ msgstr "Ðет репозиториÑ"
msgid "No schedules"
msgstr "Ðет раÑпиÑаний"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr "ПуÑто"
@@ -1378,11 +1693,23 @@ msgstr "ОтÑлеживать"
msgid "Notifications"
msgstr "УведомлениÑ"
+msgid "Nov"
+msgstr "ÐоÑб."
+
+msgid "November"
+msgstr "ÐоÑбрь"
+
msgid "Number of access attempts"
msgstr "КоличеÑтво попыток доÑтупа"
msgid "Number of failures before backing off"
-msgstr ""
+msgstr "КоличеÑтво ошибок перед откатом"
+
+msgid "Oct"
+msgstr "Окт."
+
+msgid "October"
+msgstr "ОктÑбрь"
msgid "OfSearchInADropdown|Filter"
msgstr "Фильтр"
@@ -1390,6 +1717,9 @@ msgstr "Фильтр"
msgid "Only project members can comment."
msgstr "Только учаÑтники проекта могут оÑтавлÑÑ‚ÑŒ комментарии."
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Открыто"
@@ -1436,7 +1766,7 @@ msgid "Pipeline Schedules"
msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ñ‹Ñ… Линий"
msgid "Pipeline quota"
-msgstr "Квота Ñборочной линии"
+msgstr ""
msgid "PipelineCharts|Failed:"
msgstr "Ðеудача:"
@@ -1522,6 +1852,9 @@ msgstr "Ñо Ñтадией"
msgid "Pipeline|with stages"
msgstr "Ñо ÑтадиÑми"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr "ПредпочтениÑ"
@@ -1625,19 +1958,25 @@ msgid "ProjectNetworkGraph|Graph"
msgstr "Граф"
msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr "ОбратитеÑÑŒ к админиÑтратору Ð´Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñтой наÑтройки."
+msgstr ""
+
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
-msgstr "Только подпиÑанные коммиты могут быть помещены в Ñтот репозиторий."
+msgstr ""
+
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr "Эта наÑтройка применÑетÑÑ Ð½Ð° уровне Ñервера и может быть переопределена админиÑтратором."
+msgstr ""
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr "Эта наÑтройка применÑетÑÑ Ð½Ð° уровне Ñервера, но была переопределена Ð´Ð»Ñ Ñтого проекта."
+msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr "Эта наÑтройка будет применена Ð´Ð»Ñ Ð²Ñех проектов, еÑли иное поведение не переопределено админиÑтратором."
+msgstr ""
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
@@ -1666,6 +2005,39 @@ msgstr "К Ñожалению, по вашему запроÑу проекты Ð
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Эта функциональноÑÑ‚ÑŒ требует поддержки localStorage в вашем браузере"
+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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "Публичный - Группу и включённые в неё проекты могут видеть вÑе, без какой-либо проверки подлинноÑти."
@@ -1673,7 +2045,7 @@ msgid "Public - The project can be accessed without any authentication."
msgstr "Публичный - ДоÑтуп к проекту возможен без какой-либо проверки подлинноÑти."
msgid "Push Rules"
-msgstr "Правила Отправки"
+msgstr ""
msgid "Push events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸"
@@ -1694,7 +2066,7 @@ msgid "RefSwitcher|Tags"
msgstr "Теги"
msgid "Registry"
-msgstr "РееÑÑ‚Ñ€"
+msgstr ""
msgid "Related Commits"
msgstr "СвÑзанные коммиты"
@@ -1762,6 +2134,9 @@ msgstr "РаÑпиÑаниÑ"
msgid "Scheduling Pipelines"
msgstr "Планирование Сборочных Линий"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "Ðайти ветки и теги"
@@ -1783,6 +2158,12 @@ msgstr "Выбор временной зоны"
msgid "Select target branch"
msgstr "Выбор целевой ветки"
+msgid "Sep"
+msgstr "Сент."
+
+msgid "September"
+msgstr "СентÑбрь"
+
msgid "Service Templates"
msgstr "Шаблоны Служб"
@@ -1816,14 +2197,29 @@ msgstr[0] "Показано %d Ñобытие"
msgstr[1] "Показано %d Ñобытий"
msgstr[2] "Показано %d Ñобытий"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr "Сниппеты"
msgid "Something went wrong on our end."
msgstr "У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "Что-то пошло не так при попытке Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²Ð°Ð½Ð½Ð¾Ð³Ð¾ ÑоÑтоÑÐ½Ð¸Ñ Ñтого ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgstr ""
msgid "Something went wrong while fetching the projects."
msgstr "Что-то пошло не так при получении проектов."
@@ -1874,7 +2270,7 @@ msgid "SortOptions|Least popular"
msgstr "Ðаименее популÑрный"
msgid "SortOptions|Less weight"
-msgstr "Меньший веÑ"
+msgstr ""
msgid "SortOptions|Milestone"
msgstr "Веха"
@@ -1886,7 +2282,7 @@ msgid "SortOptions|Milestone due soon"
msgstr "Веха, наÑÑ‚ÑƒÐ¿Ð°ÑŽÑ‰Ð°Ñ Ñ€Ð°Ð½ÑŒÑˆÐµ"
msgid "SortOptions|More weight"
-msgstr "Больший веÑ"
+msgstr ""
msgid "SortOptions|Most popular"
msgstr "Ðаиболее популÑрный"
@@ -1928,11 +2324,17 @@ msgid "SortOptions|Start soon"
msgstr "Ðачатые недавно"
msgid "SortOptions|Weight"
-msgstr "ВеÑ"
+msgstr ""
+
+msgid "Source"
+msgstr "ИÑточник"
msgid "Source code"
msgstr "ИÑходный код"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr "Спам Логи"
@@ -1951,6 +2353,9 @@ msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
msgid "Start the Runner!"
msgstr "ЗапуÑтить GitLab Runner!"
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr "Подгруппы"
@@ -1972,6 +2377,75 @@ msgstr[2] "Теги"
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 "Ветка"
@@ -1979,10 +2453,10 @@ msgid "Team"
msgstr "Команда"
msgid "Thanks! Don't show me this again"
-msgstr "СпаÑибо! Больше не показывайте мне Ñто Ñообщение"
+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 - Ñто Ñерьезный инÑтрумент который Ñокращает ваше времÑ. ВмеÑто ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð´ÑƒÐ±Ð»Ð¸Ñ€ÑƒÑŽÑ‰ÐµÐ³Ð¾ кода и траты времени, вы можете иÑкать код внутри других команд, который поможет вам в вашем проекте."
+msgstr ""
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "Порог ÑÑ€Ð°Ð±Ð°Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð¡ircuitBreaker должен быть меньше, чем порог ÑÑ€Ð°Ð±Ð°Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ ÑбоÑ"
@@ -2053,6 +2527,9 @@ msgstr "Среднее значение в Ñ€Ñду. Пример: между 3,
msgid "There are problems accessing Git storage: "
msgstr "Проблемы Ñ Ð´Ð¾Ñтупом к Git хранилищу: "
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "Эта ветка была изменена, пока вы её редактировали. Ð’Ñ‹ хотите Ñоздать новую ветку?"
@@ -2074,6 +2551,9 @@ msgstr "Это означает, что вы не можете отправитÑ
msgid "This merge request is locked."
msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние заблокирован."
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала Ð¿Ð¾Ð¿Ð°Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð² планировщик"
@@ -2224,14 +2704,26 @@ msgstr[2] "мин"
msgid "Time|s"
msgstr "Ñ"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "Общее времÑ"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "Общее Ð²Ñ€ÐµÐ¼Ñ Ñ‚ÐµÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„Ð¸ÐºÑаций/ÑлиÑний"
msgid "Track activity with Contribution Analytics."
-msgstr "ОтÑлеживать активноÑÑ‚ÑŒ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Ðналитики УчаÑтников."
+msgstr ""
+
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
msgid "Unlock"
msgstr "Разблокировать"
@@ -2246,19 +2738,19 @@ msgid "Unsubscribe"
msgstr "ОтпиÑатьÑÑ"
msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Улучшенный Глобальный ПоиÑк."
+msgstr ""
msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Ðналитики УчаÑтников."
+msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Групповые Веб-Обработчики."
+msgstr ""
msgid "Upgrade your plan to activate Issue weight."
-msgstr "Обновите ваш тарифный план Ð´Ð»Ñ Ð¿Ð¾ÑÐ²Ð»ÐµÐ½Ð¸Ñ Ð²ÐµÑа у обÑуждений."
+msgstr ""
msgid "Upgrade your plan to improve Issue boards."
-msgstr "Обновите ваш тарифный план, чтобы улучшить доÑки обÑуждений."
+msgstr ""
msgid "Upload New File"
msgstr "Загрузить новый файл"
@@ -2269,6 +2761,9 @@ msgstr "Загрузить файл"
msgid "UploadLink|click to upload"
msgstr "кликните Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr "ИÑпользуйте Ñледующий токен региÑтрации в процеÑÑе уÑтановки:"
@@ -2302,11 +2797,14 @@ 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 "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 "ВеÑ"
+msgstr ""
msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
msgstr "Когда доÑтуп к хранилищу получить не удалоÑÑŒ, GitLab приоÑтановит доÑтуп к хранилищу на времÑ, указанное здеÑÑŒ. Это позволит файловой ÑиÑтеме воÑÑтановитьÑÑ. Репозитории на Ñбойных \"шардах\" будут временно недоÑтупны"
@@ -2414,7 +2912,7 @@ 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 ""
msgid "Withdraw Access Request"
msgstr "Отменить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа"
@@ -2431,17 +2929,11 @@ msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить ÑвÑзь ответвлен
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ передать проект %{project_name_with_namespace} другому владельцу. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
-msgid "You are on a read-only GitLab instance."
-msgstr "Ð’Ñ‹ находитеÑÑŒ на ÑкземплÑре \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab."
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr "Ð’Ñ‹ находитеÑÑŒ на ÑкземплÑре \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab. ЕÑли вы хотите произвеÑти любые изменениÑ, вы должны перейти на \"оÑновной\" ÑкземплÑÑ€ по ÑÑылке %{link_to_primary_node}."
-
msgid "You can only add files when you are on a branch"
msgstr "Ð’Ñ‹ можете добавлÑÑ‚ÑŒ только файлы, когда находитеÑÑŒ в ветке"
msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
-msgstr "Ð’Ñ‹ не можете запиÑывать на подчиненные ÑкземплÑры \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab Geo. ИÑпользуйте вмеÑто Ñтого %{link_to_primary_node}."
+msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr "Ð’Ñ‹ не можете запиÑывать на Ñтот ÑкземплÑÑ€ \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab."
@@ -2476,6 +2968,9 @@ msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код проекта через SSH пока %{add_ssh_key_link} в ваш профиль."
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr "Ваш комментарий не будет виден вÑем."
@@ -2488,8 +2983,14 @@ msgstr "Ваше имÑ"
msgid "Your projects"
msgstr "Ваши проекты"
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
-msgstr "коммит"
+msgstr ""
msgid "day"
msgid_plural "days"
@@ -2515,8 +3016,11 @@ msgstr "пароль"
msgid "personal access token"
msgstr "токен Ð´Ð»Ñ Ð¿ÐµÑ€Ñонального доÑтупа"
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
-msgstr "чтобы помочь вашим учаÑтникам взаимодейÑтвовать Ñффективнее!"
+msgstr ""
msgid "username"
msgstr "Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 53054bdaa27..fc62776a7a4 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-21 16:43-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 06:39-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -61,6 +61,9 @@ msgstr[0] "%{storage_name}: Ñпроба невдалого доÑтупу до
msgstr[1] "%{storage_name}: %{failed_attempts} невдалі Ñпроби доÑтупу до Ñховища:"
msgstr[2] "%{storage_name}: %{failed_attempts} невдалих Ñпроб доÑтупу до Ñховища:"
+msgid "%{text} is available"
+msgstr "%{text} доÑтупний"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдіть за поÑиланнÑм %{link} Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— ÑтоÑовно вÑтановленнÑ)."
@@ -83,7 +86,7 @@ msgid "2FA enabled"
msgstr "Двоетапна Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ ÑƒÐ²Ñ–Ð¼ÐºÐ½ÐµÐ½Ð°"
msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Це набір графічних елементів Ð´Ð»Ñ Ð±ÐµÐ·Ð¿ÐµÑ€ÐµÑ€Ð²Ð½Ð¾Ñ— інтеграції"
+msgstr "Ðабір графіків відноÑно безперервної інтеграції"
msgid "About auto deploy"
msgstr "Про авто розгортаннÑ"
@@ -95,7 +98,7 @@ 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 "Account"
msgstr "Обліковий запиÑ"
@@ -113,17 +116,14 @@ msgid "Add Changelog"
msgstr "Додати ÑпиÑок змін (Changelog)"
msgid "Add Contribution guide"
-msgstr "Додати керівництво Ð´Ð»Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¸Ð±â€™ÑŽÑ‚Ð¾Ñ€Ñ–Ð²"
+msgstr ""
msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr "Додайте групу Webhooks та GitLab Enterprise Edition."
+msgstr ""
msgid "Add License"
msgstr "Додати ліцензію"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "Додайте SSH ключ в Ñвій профіль, щоб мати можливіÑÑ‚ÑŒ завантажити чи надіÑлати зміни через SSH."
-
msgid "Add new directory"
msgstr "Додати новий каталог"
@@ -136,6 +136,15 @@ msgstr "Додаткові параметри"
msgid "All"
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 fetching sidebar data"
+msgstr "Виникла помилка під Ñ‡Ð°Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… Ð´Ð»Ñ Ð±Ñ–Ñ‡Ð½Ð¾Ñ— панелі"
+
msgid "An error occurred. Please try again."
msgstr "СталаÑÑŒ помилка. Спробуйте ще раз."
@@ -145,11 +154,17 @@ msgstr "Зовнішній виглÑд"
msgid "Applications"
msgstr "Додатки"
+msgid "Apr"
+msgstr "квіт."
+
+msgid "April"
+msgstr "квітень"
+
msgid "Archived project! Repository is read-only"
msgstr "Заархівований проект! Репозиторій доÑтупний лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ"
msgid "Are you sure you want to delete this pipeline schedule?"
-msgstr "Ви впевнені, що хочете видалити цей розклад Ð´Ð»Ñ ÐšÐ¾Ð½Ð²ÐµÑ”Ñ€Ð°?"
+msgstr ""
msgid "Are you sure you want to discard your changes?"
msgstr "Ви впевнені, що бажаєте ÑкаÑувати ваші зміни?"
@@ -161,7 +176,7 @@ msgid "Are you sure you want to reset registration token?"
msgstr "Ви впевнені, що бажаєте Ñкинути реєÑтраційний токен?"
msgid "Are you sure you want to reset the health check token?"
-msgstr "Ви впевнені, що Ви хочете Ñкинути цей ключ перевірки працездатноÑÑ‚Ñ–?"
+msgstr "Ви впевнені, що хочете Ñкинути цей ключ перевірки працездатноÑÑ‚Ñ–?"
msgid "Are you sure?"
msgstr "Ви впевнені?"
@@ -172,6 +187,12 @@ msgstr "Ðртефакти"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикріпити файл за допомогою перетÑÐ³ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{upload_link}"
+msgid "Aug"
+msgstr "Ñерп."
+
+msgid "August"
+msgstr "Ñерпень"
+
msgid "Authentication Log"
msgstr "Журнал автентифікації"
@@ -205,6 +226,9 @@ msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в %{link_to_documentation}"
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "Ви можете активувати %{link_to_settings} Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
+msgid "Available"
+msgstr "ДоÑтупний"
+
msgid "Billing"
msgstr "Білінг"
@@ -236,10 +260,10 @@ msgid "BillingPlans|See all %{plan_name} features"
msgstr "ПодивітьÑÑ Ð²ÑÑ– можливоÑÑ‚Ñ– %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Ð¦Ñ Ð³Ñ€ÑƒÐ¿Ð° викориÑтовує план, пов'Ñзаний з батьківÑькою групою."
+msgstr "Ð¦Ñ Ð³Ñ€ÑƒÐ¿Ð° викориÑтовує план, пов'Ñзаний із батьківÑькою групою."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð¿Ð»Ð°Ð½Ð¾Ð¼ цієї групи відвідайте Ñекцію оплати %{parent_billing_page_link}."
+msgstr "Щоб керувати планом цієї групи, відвідайте розділ білінгу на %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
msgstr "Підвищити"
@@ -257,7 +281,7 @@ msgid "BillingPlans|paid annually at %{price_per_year}"
msgstr "ОплачуєтьÑÑ Ñ‰Ð¾Ñ€Ñ–Ñ‡Ð½Ð¾ %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr "За кориÑтувача"
+msgstr ""
msgid "Branch"
msgid_plural "Branches"
@@ -271,11 +295,17 @@ msgstr "Гілка <strong>%{branch_name}</strong> Ñтворена. Ð”Ð»Ñ Ð½Ð°
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 "Переключити гілку"
+msgstr ""
msgid "Branches"
msgstr "Гілки"
@@ -401,13 +431,13 @@ msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Вибрати в гілці"
msgid "ChangeTypeActionLabel|Revert in branch"
-msgstr "СкаÑувати у гілці"
+msgstr "Ðнулювати у гілці"
msgid "ChangeTypeAction|Cherry-pick"
-msgstr "Cherry-pick"
+msgstr ""
msgid "ChangeTypeAction|Revert"
-msgstr "СкаÑувати"
+msgstr "Ðнулювати коміт"
msgid "Changelog"
msgstr "СпиÑок змін (Changelog)"
@@ -418,14 +448,20 @@ msgstr "Графіки"
msgid "Chat"
msgstr "Чат"
+msgid "Checking %{text} availability…"
+msgstr "Перевірка доÑтупноÑÑ‚Ñ– %{text}…"
+
+msgid "Checking branch availability..."
+msgstr "Перевірка доÑтупноÑÑ‚Ñ– гілки..."
+
msgid "Cherry-pick this commit"
-msgstr "Cherry-pick в цьому коміті"
+msgstr ""
msgid "Cherry-pick this merge request"
-msgstr "Cherry-pick в цьому запиті на злиттÑ"
+msgstr ""
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
-msgstr "Виберіть, Ñкі групи ви хочете реплікувати на цю вторинну ноду. Залиште порожнім, щоб реплікувати вÑе."
+msgstr ""
msgid "CiStatusLabel|canceled"
msgstr "ÑкаÑовано"
@@ -493,8 +529,41 @@ msgstr "Закрити"
msgid "Cluster"
msgstr "КлаÑтер"
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr "%{link_to_container_project} напевно було Ñтворено під цим обліковим запиÑом"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr "%{appList} уÑпішно вÑтановлені на вашому клаÑтері"
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr "%{boldNotice} Це додаÑÑ‚ÑŒ реÑурÑи (наприклад баланÑер навантаженнÑ), що Ñпричинить додаткові витрати. ПереглÑньте %{pricingLink}"
+
+msgid "ClusterIntegration|API URL"
+msgstr "API URL"
+
+msgid "ClusterIntegration|Active"
+msgstr "Ðктивний"
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr "Додати Ñ–Ñнуючий клаÑтер"
+
+msgid "ClusterIntegration|Add cluster"
+msgstr "Додати клаÑтер"
+
+msgid "ClusterIntegration|All"
+msgstr "Ð’ÑÑ–"
+
+msgid "ClusterIntegration|Applications"
+msgstr "Додатки"
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr "Сертифікат центру Ñертифікації"
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr "Ðабір Ñертифікатів (формат PEM)"
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr "Виберіть ÑпоÑіб Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— з клаÑтером"
+
+msgid "ClusterIntegration|Cluster"
+msgstr "КлаÑтер"
msgid "ClusterIntegration|Cluster details"
msgstr "Параметри клаÑтера"
@@ -509,7 +578,7 @@ msgid "ClusterIntegration|Cluster integration is enabled for this project."
msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером увімкнена Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту увімкнена Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером. Ð’Ð¸ÐºÐ½ÐµÐ½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— не вплине на клаÑтер, але з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ GitLab з ним буде тимчаÑово розірване."
+msgstr ""
msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Kubernetes Engine..."
@@ -517,21 +586,54 @@ msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Kubernetes Engine..."
msgid "ClusterIntegration|Cluster name"
msgstr "Ім'Ñ ÐºÐ»Ð°Ñтера"
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
-msgstr "КлаÑтер був уÑпішно Ñтворений в Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr "КлаÑтер був уÑпішно Ñтворено в Google Kubernetes Engine. Оновіть Ñторінку, щоб переглÑнути додатову інформацію"
+
+msgid "ClusterIntegration|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 "КлаÑтери дозволÑÑŽÑ‚ÑŒ вам викориÑтовувати Review Apps, розгортати ваші програми, запуÑкати ваші конвеєри Ñ– багато іншого проÑтим ÑпоÑобом. %{link_to_help_page}"
+
+msgid "ClusterIntegration|Copy API URL"
+msgstr "Скопіювати URL API"
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr "Скопіювати Ñертифікат центру Ñертифікації"
+
+msgid "ClusterIntegration|Copy Token"
+msgstr "Скопіювати Токен"
msgid "ClusterIntegration|Copy cluster name"
msgstr "Копіювати назву клаÑтера"
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr "Створити новий клаÑтер у Google Engine прÑмо з GitLab"
+
msgid "ClusterIntegration|Create cluster"
msgstr "Створити клаÑтер"
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
-msgstr "Створити новий клаÑтер в Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr "Створити клаÑтер в Google Kubernetes Engine"
+
+msgid "ClusterIntegration|Create on GKE"
+msgstr "Створити в GKE"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Увімкнути інтеграцію із клаÑтерами"
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr "Вкажіть параметри Ñ–Ñнуючого клаÑтера Kubernetes"
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr "Введіть докладний Ð¾Ð¿Ð¸Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ клаÑтера"
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr "Шаблон Ñередовища"
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr "ВартіÑÑ‚ÑŒ GKE"
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr "GitLab Runner"
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Ідентифікатор проекту в Google Cloud Platform"
@@ -541,29 +643,77 @@ msgstr "Google Kubernetes Engine"
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr "Проект Google Kubernetes Engine"
+msgid "ClusterIntegration|Helm Tiller"
+msgstr "Helm Tiller"
+
+msgid "ClusterIntegration|Inactive"
+msgstr "Ðеактивні"
+
+msgid "ClusterIntegration|Ingress"
+msgstr "Ingress"
+
+msgid "ClusterIntegration|Install"
+msgstr "Ð’Ñтановити"
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr "Ð’Ñтановіть додатки у ваш клаÑтер. Докладніше про %{helpLink}"
+
+msgid "ClusterIntegration|Installed"
+msgstr "Ð’Ñтановлений"
+
+msgid "ClusterIntegration|Installing"
+msgstr "Ð’ÑтановленнÑ"
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ ÐºÐ»Ð°Ñтерної автоматизації"
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про клаÑтери"
+
msgid "ClusterIntegration|Machine type"
msgstr "Тип машини"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "ПереконайтеÑÑ, що ваш обліковий Ð·Ð°Ð¿Ð¸Ñ %{link_to_requirements} Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтерів"
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
-msgstr "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ”ÑŽ із клаÑтером у вашому Gitlab-проекті."
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
+msgstr "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ”ÑŽ із клаÑтером у вашому Gitlab-проекті"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "Ð”Ð»Ñ ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñвоїм клаÑтером перейдіть на %{link_gke}"
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr "Кілька клаÑтерів доÑтупні в GitLab Enterprise Edition Premium Ñ– Ultimate"
+
+msgid "ClusterIntegration|Note:"
+msgstr "Примітка:"
+
msgid "ClusterIntegration|Number of nodes"
msgstr "КількіÑÑ‚ÑŒ вузлів"
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr "Введіть інформацію про доÑтуп до Ñвого клаÑтера. Якщо вам потрібна допомога, ви можете прочитати наші %{link_to_help_page} по клаÑтерам"
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
-msgstr "Будь-лаÑка впевнітьÑÑ, що ваш Google-аккаунт задовольнÑÑ” наÑтупним вимогам:"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr "Проблема Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ»Ð°Ñтера"
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr "Проблема Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑпиÑку клаÑтерів"
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
msgid "ClusterIntegration|Project namespace (optional, unique)"
-msgstr "Namespace проекту (не обов’Ñзковий, унікальний)"
+msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
msgstr "Прочитайте нашу документацію %{link_to_help_page} по інтеграції із клаÑтером."
@@ -574,8 +724,14 @@ msgstr "Видалити інтеграцію з клаÑтером"
msgid "ClusterIntegration|Remove integration"
msgstr "Видалити інтеграцію"
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr "При видаленні інтеграції з клаÑтером буде видалена ÐºÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ ÐºÐ»Ð°Ñтера, Ñку ви додали в цей проект. Дана Ð´Ñ–Ñ Ð½Ðµ видалить Ñам проект."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтерної інтеграції призведе до Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ— клаÑтера, Ñку ви додали до цього проекту. Ð¦Ñ Ð´Ñ–Ñ Ð½Ðµ буде видалÑти ваш клаÑтер у Google Kubernetes Engine."
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr "Запит про початок вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ðµ виконано"
+
+msgid "ClusterIntegration|Save changes"
+msgstr "Зберегти зміни"
msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "ПереглÑнути та редагувати параметри вашого клаÑтера"
@@ -589,15 +745,33 @@ msgstr "ПереглÑнути ваші проекти"
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в Google Kubernetes Engine"
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr "Під Ñ‡Ð°Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ %{title} ÑталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ°"
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr "Ðемає клаÑтерів Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ"
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr "Цей обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¼Ð°Ñ” мати дозволи Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в %{link_to_container_project}, зазначеному нижче"
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Переключити КлаÑтер"
+msgid "ClusterIntegration|Token"
+msgstr "Токен"
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "За допомогою підключеного до цього проекту клаÑтера, ви можете викориÑтовувати Review Apps, розгортати ваші проекти, запуÑкати конвеєри збірки та багато іншого."
@@ -613,9 +787,15 @@ msgstr "доÑтуп до Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "клаÑтер"
+msgid "ClusterIntegration|documentation"
+msgstr "документаціÑ"
+
msgid "ClusterIntegration|help page"
msgstr "Ñторінка допомоги"
+msgid "ClusterIntegration|installing applications"
+msgstr "вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÑ–Ð²"
+
msgid "ClusterIntegration|meets the requirements"
msgstr "задовольнÑÑ” вимогам"
@@ -631,12 +811,6 @@ msgstr[0] "Коміт"
msgstr[1] "Коміта"
msgstr[2] "Комітів"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] "Закомітити %d файл"
-msgstr[1] "Закомітити %d файли"
-msgstr[2] "Закомітити %d файлів"
-
msgid "Commit Message"
msgstr "Коміт-повідомелннÑ"
@@ -704,7 +878,7 @@ msgid "ContainerRegistry|Tag"
msgstr "Тег"
msgid "ContainerRegistry|Tag ID"
-msgstr "Тег ID"
+msgstr ""
msgid "ContainerRegistry|Use different image names"
msgstr "ВикориÑтовуйте різні імена образів"
@@ -713,11 +887,20 @@ msgid "ContainerRegistry|With the Docker Container Registry integrated into GitL
msgstr "За допомогою вбудованого в GitLab реєÑтру Docker контейнерів кожен проект може мати влаÑне міÑце Ð´Ð»Ñ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Docker образів."
msgid "Contribution guide"
-msgstr "Керівництво контриб’юторів"
+msgstr ""
msgid "Contributors"
msgstr "Контриб’ютори"
+msgid "ContributorsPage|Building repository graph."
+msgstr ""
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+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/вкладень Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ вторинного вузла"
@@ -737,7 +920,7 @@ msgid "Create New Directory"
msgstr "Створити новий каталог"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
-msgstr "Створити токен доÑтупу Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ аккауета, щоб відправлÑти або отримувати через %{protocol}."
+msgstr ""
msgid "Create directory"
msgstr "Створити каталог"
@@ -745,6 +928,9 @@ msgstr "Створити каталог"
msgid "Create empty bare repository"
msgstr "Створити порожній репозиторій"
+msgid "Create epic"
+msgstr "Створити епік"
+
msgid "Create file"
msgstr "Створити файл"
@@ -772,6 +958,9 @@ msgstr "Тег"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Створити токен Ð´Ð»Ñ Ð¾ÑобиÑтого доÑтупу"
+msgid "Creating epic"
+msgstr "Ð¡Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐµÐ¿Ñ–ÐºÑƒ"
+
msgid "Cron Timezone"
msgstr "ЧаÑовий поÑÑ Cron"
@@ -788,10 +977,10 @@ msgid "Cycle Analytics"
msgstr "Ðналіз циклу"
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
-msgstr "Ðналітика циклу дає оглÑд того, Ñкільки чаÑу потрібно, щоб перейти від ідеї до виробництва у вашому проекті."
+msgstr ""
msgid "CycleAnalyticsStage|Code"
-msgstr "Код"
+msgstr ""
msgid "CycleAnalyticsStage|Issue"
msgstr "Проблема"
@@ -800,13 +989,13 @@ msgid "CycleAnalyticsStage|Plan"
msgstr "ПлануваннÑ"
msgid "CycleAnalyticsStage|Production"
-msgstr "ПРОД"
+msgstr ""
msgid "CycleAnalyticsStage|Review"
msgstr "ЗатвердженнÑ"
msgid "CycleAnalyticsStage|Staging"
-msgstr "ДЕВ"
+msgstr "Staging"
msgid "CycleAnalyticsStage|Test"
msgstr "ТеÑтуваннÑ"
@@ -817,6 +1006,12 @@ msgstr "Ð’ÑÑ–"
msgid "DashboardProjects|Personal"
msgstr "ОÑобиÑÑ‚Ñ–"
+msgid "Dec"
+msgstr "груд."
+
+msgid "December"
+msgstr "грудень"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Визначте влаÑний шаблон за допомогою ÑинтакÑиÑу cron"
@@ -830,13 +1025,13 @@ msgstr[1] "РозгортаннÑ"
msgstr[2] "Розгортань"
msgid "Deploy Keys"
-msgstr "Ключи Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ"
+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 "Шаблони опиÑу дозволÑÑŽÑ‚ÑŒ визначити конкретні шаблони обговорень та запитів на Ð·Ð»Ð¸Ð²Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проекту."
+msgstr ""
msgid "Details"
msgstr "Деталі"
@@ -851,7 +1046,7 @@ msgid "Dismiss Cycle Analytics introduction box"
msgstr "Відмінити блок вÑтупу до Ðналитики Циклу"
msgid "Dismiss Merge Request promotion"
-msgstr "Ðе показувати промоушн запитів на злиттÑ"
+msgstr "Відхилити рекламу Ð´Ð»Ñ Ð—Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на злиттÑ"
msgid "Don't show again"
msgstr "Ðе показувати знову"
@@ -878,7 +1073,7 @@ msgid "DownloadCommit|Email Patches"
msgstr "Email-патчи"
msgid "DownloadCommit|Plain Diff"
-msgstr "Plain Diff"
+msgstr ""
msgid "DownloadSource|Download"
msgstr "Завантажити"
@@ -892,6 +1087,72 @@ msgstr "Редагувати Розклад Конвеєра %{id}"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr "Епік буде видалено! Ви впевнені?"
+
+msgid "Epics"
+msgstr "Епіки"
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr "Епіки дозволÑÑŽÑ‚ÑŒ керувати вашим портфелем проектів ефективніше та з меншими зуÑиллÑми"
+
+msgid "Error creating epic"
+msgstr "Помилка при Ñтворенні епіку"
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° під Ñ‡Ð°Ñ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñки на ÑповіщеннÑ"
+
msgid "EventFilterBy|Filter by all"
msgstr "Фільтрувати по вÑім"
@@ -905,7 +1166,7 @@ msgid "EventFilterBy|Filter by merge events"
msgstr "Фільтрувати по запитам на злиттÑ"
msgid "EventFilterBy|Filter by push events"
-msgstr "Фільтрувати по push-подіÑÑ…"
+msgstr ""
msgid "EventFilterBy|Filter by team"
msgstr "Фільтрувати по команді"
@@ -929,7 +1190,13 @@ msgid "Failed to change the owner"
msgstr "Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ влаÑника"
msgid "Failed to remove the pipeline schedule"
-msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ розклад Конвеєра"
+msgstr ""
+
+msgid "Feb"
+msgstr "лют."
+
+msgid "February"
+msgstr "лютий"
msgid "File name"
msgstr "Ім'Ñ Ñ„Ð°Ð¹Ð»Ñƒ"
@@ -950,7 +1217,7 @@ msgid "FirstPushedBy|First"
msgstr "Перший"
msgid "FirstPushedBy|pushed by"
-msgstr "ÐадіÑлані зміни від"
+msgstr ""
msgid "Fork"
msgid_plural "Forks"
@@ -968,10 +1235,10 @@ msgid "Format"
msgstr "Формат"
msgid "From issue creation until deploy to production"
-msgstr "З моменту ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ до Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° ПРОД"
+msgstr ""
msgid "From merge request merge until deploy to production"
-msgstr "З об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð¾ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° ПРОД"
+msgstr ""
msgid "GPG Keys"
msgstr "GPG ключі"
@@ -979,6 +1246,21 @@ msgstr "GPG ключі"
msgid "Geo Nodes"
msgstr "Гео-Вузли"
+msgid "GeoNodeSyncStatus|Failed"
+msgstr "Ðевдало"
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr "Вузол не працює або зламаний."
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr "Вузол працює повільно, перевантажений або тільки що відновивÑÑ Ð¿Ñ–ÑÐ»Ñ Ð·Ð±Ð¾ÑŽ."
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr "ÐеÑинхронізовано"
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr "Синхронізовано"
+
msgid "Geo|File sync capacity"
msgstr "ПропуÑкна здатніÑÑ‚ÑŒ Ñинхронізації файлів"
@@ -1042,9 +1324,6 @@ msgstr "Групи не знайдені"
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "Ви можете керувати правами доÑтупу членів групи мати доÑтуп до кожного проекту в ній."
-msgid "GroupsTreeRole|as"
-msgstr "Ñк"
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr "Ви впевнені, що хочете залишити групу \"${this.group.fullName}\"?"
@@ -1075,6 +1354,9 @@ msgstr "Ðа жаль жодна группа не задовольнÑÑ” пар
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "Ðа жаль жодна группа чи проект не задовольнÑÑ” параметрам вашого запиту"
+msgid "Have your users email"
+msgstr "Електронна пошта Ð´Ð»Ñ Ð·Ð²ÐµÑ€Ñ‚Ð°Ð½ÑŒ кориÑтувачів"
+
msgid "Health Check"
msgstr "Перевірки працездатноÑÑ‚Ñ–"
@@ -1100,10 +1382,10 @@ msgid "Housekeeping successfully started"
msgstr "ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÑƒÑпішно розпочато"
msgid "Import repository"
-msgstr "Імпорт репозеторіÑ"
+msgstr ""
msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr "Покращити дошки обговорень за допомогою верÑÑ–Ñ— GitLab Enterprise Edition."
+msgstr ""
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
msgstr "Покращити ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð°Ð¼Ð¸ з можливіÑÑ‚ÑŽ Ð²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð²Ð°Ð³Ð¸ проблеми за допомогою GitLab Enterprise Edition."
@@ -1117,7 +1399,7 @@ msgstr "Ð’Ñтановіть Runner, ÑуміÑний з GitLab CI"
msgid "Instance"
msgid_plural "Instances"
msgstr[0] "ІнÑтанÑ"
-msgstr[1] "ІнÑтанÑа"
+msgstr[1] "IнÑтанÑи"
msgstr[2] "ІнÑтанÑів"
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
@@ -1133,10 +1415,7 @@ msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавлÑємо аналітику циклу"
msgid "Issue board focus mode"
-msgstr "Режим фокуÑÑƒÐ²Ð°Ð½Ð½Ñ Ð½Ð°Ð´ дошкою обговорень"
-
-msgid "Issue boards with milestones"
-msgstr "Дошка обговорень із етапами"
+msgstr "Режим фокуÑÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð´Ð¾ÑˆÐºÐ¸ обговорень"
msgid "Issue events"
msgstr "Проблеми"
@@ -1150,6 +1429,24 @@ msgstr "Дошки"
msgid "Issues"
msgstr "Проблеми"
+msgid "Jan"
+msgstr "Ñіч."
+
+msgid "January"
+msgstr "Ñічень"
+
+msgid "Jul"
+msgstr "лип."
+
+msgid "July"
+msgstr "липень"
+
+msgid "Jun"
+msgstr "чер."
+
+msgid "June"
+msgstr "червень"
+
msgid "LFSStatus|Disabled"
msgstr "Вимкнено"
@@ -1184,7 +1481,7 @@ msgid "Last updated"
msgstr "ВоÑтаннє оновленно"
msgid "LastPushEvent|You pushed to"
-msgstr "Ви надіÑлали зміни до"
+msgstr ""
msgid "LastPushEvent|at"
msgstr "в"
@@ -1225,9 +1522,18 @@ msgstr "Заблоковані файли"
msgid "Login"
msgstr "Вхід"
+msgid "Mar"
+msgstr "бер."
+
+msgid "March"
+msgstr "березень"
+
msgid "Maximum git storage failures"
msgstr "МакÑимальна кількіÑÑ‚ÑŒ невдач в Ñховищі даних git"
+msgid "May"
+msgstr "травень"
+
msgid "Median"
msgstr "Медіана"
@@ -1238,7 +1544,7 @@ msgid "Merge Requests"
msgstr "Запити на злиттÑ"
msgid "Merge events"
-msgstr "Запити на злиттÑ"
+msgstr ""
msgid "Merge request"
msgstr "Запит на злиттÑ"
@@ -1256,7 +1562,7 @@ msgid "More information is available|here"
msgstr "тут"
msgid "Multiple issue boards"
-msgstr "Зведені дошки обговореннÑ"
+msgstr "Кілька дошок обговореннÑ"
msgid "New Cluster"
msgstr "Ðовий клаÑтер"
@@ -1273,9 +1579,15 @@ msgstr "Ðовий розклад Конвеєра"
msgid "New branch"
msgstr "Ðова гілка"
+msgid "New branch unavailable"
+msgstr "Ðова гілка недоÑтупна"
+
msgid "New directory"
msgstr "Ðовий каталог"
+msgid "New epic"
+msgstr "Ðовий епік"
+
msgid "New file"
msgstr "Ðовий файл"
@@ -1295,7 +1607,7 @@ msgid "New schedule"
msgstr "Ðовий Розклад"
msgid "New snippet"
-msgstr "Ðовий Ñніппет"
+msgstr ""
msgid "New subgroup"
msgstr "Ðова підгрупа"
@@ -1307,11 +1619,14 @@ msgid "No container images stored for this project. Add one by following the ins
msgstr "Ð’ цьому проекті немає жодного образа контейнера. Додайте його за інÑтрукціÑми вище."
msgid "No repository"
-msgstr "Ðемає репозеторіÑ"
+msgstr ""
msgid "No schedules"
msgstr "немає Розкладів"
+msgid "No time spent"
+msgstr "Ðемає витраченого чаÑу"
+
msgid "None"
msgstr "Жоден"
@@ -1328,13 +1643,13 @@ msgid "NotificationEvent|Close issue"
msgstr "Проблема закрита"
msgid "NotificationEvent|Close merge request"
-msgstr "Запит на об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ð°ÐºÑ€Ð¸Ñ‚Ð¸Ð¹"
+msgstr ""
msgid "NotificationEvent|Failed pipeline"
msgstr "Ðевдача в конвеєрі"
msgid "NotificationEvent|Merge merge request"
-msgstr "Об'єднати запит на злиттÑ"
+msgstr ""
msgid "NotificationEvent|New issue"
msgstr "Ðова проблема"
@@ -1349,7 +1664,7 @@ msgid "NotificationEvent|Reassign issue"
msgstr "Перепризначити проблему"
msgid "NotificationEvent|Reassign merge request"
-msgstr "Перепризначити запит на злиттÑ"
+msgstr ""
msgid "NotificationEvent|Reopen issue"
msgstr "Повторне Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñƒ"
@@ -1378,18 +1693,33 @@ msgstr "ВідÑтежувати"
msgid "Notifications"
msgstr "СповіщеннÑ"
+msgid "Nov"
+msgstr "лиÑÑ‚."
+
+msgid "November"
+msgstr "лиÑтопад"
+
msgid "Number of access attempts"
msgstr "КількіÑÑ‚ÑŒ Ñпроб доÑтупу"
msgid "Number of failures before backing off"
msgstr "КількіÑÑ‚ÑŒ помилок до призупиненнÑ"
+msgid "Oct"
+msgstr "жовт."
+
+msgid "October"
+msgstr "жовтень"
+
msgid "OfSearchInADropdown|Filter"
msgstr "Фільтр"
msgid "Only project members can comment."
msgstr "Тільки учаÑники проекту можуть залишати коментарі."
+msgid "Opened"
+msgstr "Відкрито"
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Відкрито"
@@ -1469,7 +1799,7 @@ msgid "PipelineSchedules|Input variable key"
msgstr "Введіть ім'Ñ Ð·Ð¼Ñ–Ð½Ð½Ð¾Ñ—"
msgid "PipelineSchedules|Input variable value"
-msgstr "Вхідні Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½Ð½Ð¸Ñ…"
+msgstr ""
msgid "PipelineSchedules|Next Run"
msgstr "ÐаÑтупний запуÑк"
@@ -1478,7 +1808,7 @@ msgid "PipelineSchedules|None"
msgstr "Ðемає"
msgid "PipelineSchedules|Provide a short description for this pipeline"
-msgstr "Задайте короткий Ð¾Ð¿Ð¸Ñ Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Конвеєру"
+msgstr ""
msgid "PipelineSchedules|Remove variable row"
msgstr "Видалити змінні"
@@ -1493,7 +1823,7 @@ msgid "PipelineSchedules|Variables"
msgstr "Змінні"
msgid "PipelineSheduleIntervalPattern|Custom"
-msgstr "ВлаÑні"
+msgstr ""
msgid "Pipelines"
msgstr "Конвеєри"
@@ -1522,11 +1852,14 @@ msgstr "зі Ñтадією"
msgid "Pipeline|with stages"
msgstr "зі ÑтадіÑми"
+msgid "Please solve the reCAPTCHA"
+msgstr "Будь лаÑка, пройдіть reCAPTCHA"
+
msgid "Preferences"
msgstr "ÐалаштуваннÑ"
msgid "Private - Project access must be granted explicitly to each user."
-msgstr "Приватний — доÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
+msgstr ""
msgid "Private - The group and its projects can only be viewed by members."
msgstr "Приватна — цю групу та Ñ—Ñ— проекти можуть бачити тільки Ñ—Ñ— кориÑтувачі."
@@ -1583,7 +1916,7 @@ msgid "Project '%{project_name}' was successfully updated."
msgstr "Проект '%{project_name}' уÑпішно оновлено."
msgid "Project access must be granted explicitly to each user."
-msgstr "ДоÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
+msgstr ""
msgid "Project details"
msgstr "Деталі проекту"
@@ -1607,7 +1940,7 @@ msgid "ProjectFeature|Disabled"
msgstr "Вимкнено"
msgid "ProjectFeature|Everyone with access"
-msgstr "Ð’Ñе з доÑтупом"
+msgstr ""
msgid "ProjectFeature|Only team members"
msgstr "Тільки члени команди"
@@ -1619,7 +1952,7 @@ msgid "ProjectLastActivity|Never"
msgstr "Ðіколи"
msgid "ProjectLifecycle|Stage"
-msgstr "Етап"
+msgstr "СтадіÑ"
msgid "ProjectNetworkGraph|Graph"
msgstr "ІÑторіÑ"
@@ -1627,9 +1960,15 @@ msgstr "ІÑторіÑ"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr "ЗвернітьÑÑ Ð´Ð¾ адмініÑтратора, щоб змінити це налаштуваннÑ."
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr "Зразу запуÑкати конвеєр у гілкці за замовчуваннÑм"
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr "Тільки підпиÑані коміти можуть бути надіÑлані в цей репозиторій."
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr "Проблема Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð°Ñ€Ð°Ð¼ÐµÑ‚Ñ€Ñ–Ð² CI / CD JavaScript"
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr "Цей параметр заÑтоÑовуєтьÑÑ Ð½Ð° рівні Ñервера та може бути перевизначений адмініÑтратором."
@@ -1640,7 +1979,7 @@ msgid "ProjectSettings|This setting will be applied to all projects unless overr
msgstr "Цей параметр буде заÑтоÑовано до вÑÑ–Ñ… проектів, Ñкщо адмініÑтратор не змінить його."
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr "КориÑтувачі можуть виконувати push в цей репозиторій лише тих комітів, Ñкі міÑÑ‚ÑÑ‚ÑŒ одну із підтверджених Ð°Ð´Ñ€ÐµÑ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти."
+msgstr ""
msgid "Projects"
msgstr "Проекти"
@@ -1666,6 +2005,39 @@ msgstr "Ðа жаль, по вашоу запиту проектів не зна
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Ð¦Ñ Ñ„ÑƒÐ½ÐºÑ†Ñ–Ñ Ð¿Ð¾Ñ‚Ñ€ÐµÐ±ÑƒÑ” підтримки localStorage вашим браузером"
+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 "За замовчуваннÑм, Prometheus запуÑкаєтьÑÑ Ð·Ð° адреÑою ‘http://localhost:9090’. Ðе рекомендуєтьÑÑ Ð·Ð¼Ñ–Ð½ÑŽÐ²Ð°Ñ‚Ð¸ цю адреÑу Ñ– порт, бо це може призвеÑти до конфлікту з іншими ÑервіÑами запущеними на GitLab Ñервері."
+
+msgid "PrometheusService|Finding and configuring metrics..."
+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|Monitored"
+msgstr "Моніторинг підключено"
+
+msgid "PrometheusService|More information"
+msgstr "Додаткова інформаціÑ"
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr "Жодні метрики не відÑлідковуютьÑÑ. Ð”Ð»Ñ Ð¿Ð¾Ñ‡Ð°Ñ‚ÐºÑƒ моніторингу, розгорніть Ñередовище."
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr "Базова адреÑа Prometheus API, наприклад http://prometheus.example.com/"
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr "Моніторинг Prometheus"
+
+msgid "PrometheusService|View environments"
+msgstr "ПереглÑд Ñередовищ"
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "Публічна — група та вÑÑ– публічні проекти можуть переглÑдатиÑÑ Ð±ÐµÐ· автентифікації."
@@ -1673,10 +2045,10 @@ msgid "Public - The project can be accessed without any authentication."
msgstr "Публічний — проект может переглÑдатиÑÑ Ð±ÐµÐ· автентифікації."
msgid "Push Rules"
-msgstr "Push-правила"
+msgstr ""
msgid "Push events"
-msgstr "Push-події"
+msgstr ""
msgid "PushRule|Committer restriction"
msgstr "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ‚ÐµÑ€Ð°"
@@ -1685,7 +2057,7 @@ msgid "Read more"
msgstr "Докладніше"
msgid "Readme"
-msgstr "Прочитай Мене"
+msgstr "ІнÑтрукціÑ"
msgid "RefSwitcher|Branches"
msgstr "Гілки"
@@ -1700,19 +2072,19 @@ msgid "Related Commits"
msgstr "Пов'Ñзані Коміти"
msgid "Related Deployed Jobs"
-msgstr "Пов’Ñзані розгорнуті задачі (Jobs)"
+msgstr "Пов’Ñзані розгорнуті Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ (Jobs)"
msgid "Related Issues"
msgstr "Пов’Ñзані Проблеми (Issues)"
msgid "Related Jobs"
-msgstr "Пов’Ñзані Задачі (Jobs)"
+msgstr "Пов’Ñзані Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ (Jobs)"
msgid "Related Merge Requests"
msgstr "Пов'Ñзані запити на злиттÑ"
msgid "Related Merged Requests"
-msgstr "Пов'Ñзані об'єднані запити"
+msgstr ""
msgid "Remind later"
msgstr "Ðагадати пізніше"
@@ -1736,10 +2108,10 @@ msgid "Reset runners registration token"
msgstr "Скинути реєÑтраційний токен runner-ів"
msgid "Revert this commit"
-msgstr "СкаÑувати цей коміт"
+msgstr "Ðнулювати цей коміт"
msgid "Revert this merge request"
-msgstr "СкаÑувати цей запит на злиттÑ"
+msgstr "Ðнулювати цей запит на злиттÑ"
msgid "SSH Keys"
msgstr "Ключі SSH"
@@ -1751,7 +2123,7 @@ msgid "Save changes"
msgstr "Зберегти зміни"
msgid "Save pipeline schedule"
-msgstr "Зберегти Розклад Конвеєра"
+msgstr ""
msgid "Schedule a new pipeline"
msgstr "Розклад нового конвеєра"
@@ -1762,6 +2134,9 @@ msgstr "Розклади"
msgid "Scheduling Pipelines"
msgstr "ÐŸÐ»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
+msgid "Scoped issue boards"
+msgstr "Тематичні дошки проблем"
+
msgid "Search branches and tags"
msgstr "Пошук гілок та тегів"
@@ -1783,11 +2158,17 @@ msgstr "Вибрати чаÑовий поÑÑ"
msgid "Select target branch"
msgstr "Вибір цільової гілки"
+msgid "Sep"
+msgstr "вер."
+
+msgid "September"
+msgstr "вереÑень"
+
msgid "Service Templates"
msgstr "Ð¡ÐµÑ€Ð²Ñ–Ñ ÑˆÐ°Ð±Ð»Ð¾Ð½Ñ–Ð²"
msgid "Set a password on your account to pull or push via %{protocol}."
-msgstr "Ð’Ñтановіть пароль Ñвого облікового запиÑу, щоб відправлÑти або отримувати код через %{protocol}."
+msgstr ""
msgid "Set up CI"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
@@ -1816,14 +2197,29 @@ msgstr[0] "Показано %d подію"
msgstr[1] "Показано %d події"
msgstr[2] "Показано %d подій"
+msgid "Sidebar|Change weight"
+msgstr "Змінити вагу"
+
+msgid "Sidebar|Edit"
+msgstr "Редагувати"
+
+msgid "Sidebar|No"
+msgstr "ÐÑ–"
+
+msgid "Sidebar|None"
+msgstr "Ðемає"
+
+msgid "Sidebar|Weight"
+msgstr "Вага"
+
msgid "Snippets"
-msgstr "Фрагменти"
+msgstr ""
msgid "Something went wrong on our end."
msgstr "ЩоÑÑŒ пішло не так з нашого боку"
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "ЩоÑÑŒ пішло не так, намагаючиÑÑŒ змінити ÑÑ‚Ð°Ñ‚ÑƒÑ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgstr "ЩоÑÑŒ пішло не так, при Ñпробі зміни Ñтану Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ ${this.issuableDisplayName}"
msgid "Something went wrong while fetching the projects."
msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
@@ -1874,7 +2270,7 @@ msgid "SortOptions|Least popular"
msgstr "Ðайменш популÑрний"
msgid "SortOptions|Less weight"
-msgstr "Ðайменша вага"
+msgstr "Менша вага"
msgid "SortOptions|Milestone"
msgstr "Етап"
@@ -1886,7 +2282,7 @@ msgid "SortOptions|Milestone due soon"
msgstr "Етап запланований незабаром"
msgid "SortOptions|More weight"
-msgstr "Ðайбільша вага"
+msgstr "Більша вага"
msgid "SortOptions|Most popular"
msgstr "Ðайбільш популÑрний"
@@ -1930,9 +2326,15 @@ msgstr "Розпочатий нещодавно"
msgid "SortOptions|Weight"
msgstr "Вага"
+msgid "Source"
+msgstr "Джерело"
+
msgid "Source code"
msgstr "Код"
+msgid "Source is not available"
+msgstr "Джерело недоÑтупне"
+
msgid "Spam Logs"
msgstr "Спам-журнал"
@@ -1951,6 +2353,9 @@ msgstr "Почати %{new_merge_request} з цими змінами"
msgid "Start the Runner!"
msgstr "ЗапуÑÑ‚Ñ–Ñ‚ÑŒ Runner!"
+msgid "Stopped"
+msgstr "Зупинено"
+
msgid "Subgroups"
msgstr "Підгрупи"
@@ -1958,10 +2363,10 @@ msgid "Subscribe"
msgstr "ПідпиÑатиÑÑ"
msgid "Switch branch/tag"
-msgstr "тег"
+msgstr ""
msgid "System Hooks"
-msgstr "СиÑтемні Hook'и"
+msgstr ""
msgid "Tag"
msgid_plural "Tags"
@@ -1972,6 +2377,75 @@ msgstr[2] "Тегів"
msgid "Tags"
msgstr "Теги"
+msgid "TagsPage|Browse commits"
+msgstr "ПереглÑнути коміти"
+
+msgid "TagsPage|Browse files"
+msgstr "ПереглÑнути файли"
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr "Ðеможливо знайти HEAD коміт до цього тегу"
+
+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 "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ‚ÐµÐ³Ð° %{tag_name} не може бути ÑкаÑовано. Ви впевнені?"
+
+msgid "TagsPage|Edit release notes"
+msgstr "Редагувати Ð¾Ð¿Ð¸Ñ Ñ€ÐµÐ»Ñ–Ð·Ñƒ"
+
+msgid "TagsPage|Existing branch name, tag, or commit SHA"
+msgstr "Ім'Ñ Ñ–Ñнуючої гілки, тега або SHA коміта"
+
+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 "ВикориÑтовуйте команду git tag, щоб додати новий:"
+
+msgid "TagsPage|Write your release notes or drag files here..."
+msgstr "Ðапишіть Ñвій Ð¾Ð¿Ð¸Ñ Ñ€ÐµÐ»Ñ–Ð·Ñƒ або перетÑгніть файли Ñюди..."
+
+msgid "TagsPage|protected"
+msgstr "захищений"
+
msgid "Target Branch"
msgstr "Цільова гілка"
@@ -1982,22 +2456,22 @@ 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 - це потужний інÑтрумент Ñкий заощаджує ваш чаÑ. ЗаміÑÑ‚ÑŒ Ð´ÑƒÐ±Ð»ÑŽÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ Ñ– витрати чаÑу, ви можете шукати код вÑередині інших команд, Ñкий може допомогти у вашому проекті."
+msgstr "Розширений глобальний пошук в GitLab - це потужний інÑтрумент Ñкий заощаджує ваш чаÑ. ЗаміÑÑ‚ÑŒ Ð´ÑƒÐ±Ð»ÑŽÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ Ñ– витрати чаÑу, ви можете шукати код інших команд, Ñкий може допомогти у вашому проекті."
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "Поріг Ð¿Ñ€Ð¸Ð·ÑƒÐ¿Ð¸Ð½ÐµÐ½Ð½Ñ circuitbreaker має бути нижчий за поріг повного відключеннÑ"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
-msgstr "Ðа Ñтадії напиÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ, показує Ñ‡Ð°Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ коміту до ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на об'єднаннÑ. Дані будуть автоматично додані піÑÐ»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ першого запиту на об'єднаннÑ."
+msgstr ""
msgid "The collection of events added to the data gathered for that stage."
-msgstr "ÐšÐ¾Ð»ÐµÐºÑ†Ñ–Ñ Ð¿Ð¾Ð´Ñ–Ð¹ додана до даних, зібраних Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ етапу."
+msgstr ""
msgid "The fork relationship has been removed."
-msgstr "Зв'Ñзок форка видалена."
+msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
-msgstr "Етап випуÑку показує, Ñкільки чаÑу потрібно від ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ до приÑÐ²Ð¾Ñ”Ð½Ð½Ñ Ð²Ð¸Ð¿ÑƒÑку, або Ð´Ð¾Ð´Ð°Ð²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ в вашу дошку проблем. Почніть Ñтворювати проблеми, щоб переглÑдати дані Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ етапу."
+msgstr ""
msgid "The number of attempts GitLab will make to access a storage."
msgstr "КількіÑÑ‚ÑŒ Ñпроб, Ñкі зробить GitLab Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу до Ñховища даних."
@@ -2015,10 +2489,10 @@ msgid "The pipelines schedule runs pipelines in the future, repeatedly, for spec
msgstr "Розклад конвеєрів запуÑкає в майбутньому конвеєри, Ð´Ð»Ñ Ð¿ÐµÐ²Ð½Ð¸Ñ… гілок або тегів. Заплановані конвеєри уÑпадковують Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð½Ð° доÑтуп до проекту на оÑнові пов'Ñзаного з ними кориÑтувача."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
-msgstr "Ðа етапі Ð¿Ð»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶Ð°Ñ”Ñ‚ÑŒÑÑ Ñ‡Ð°Ñ Ð²Ñ–Ð´ попереднього кроку до першого коміту. ДодаєтьÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡Ð½Ð¾, Ñк тільки відправитÑÑ Ð¿ÐµÑ€ÑˆÐ¸Ð¹ коміт."
+msgstr ""
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
-msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ ÐŸÐ ÐžÐ”Ð°ÐºÑˆÐ¸Ð½ показує загальний Ñ‡Ð°Ñ Ð¼Ñ–Ð¶ ÑтвореннÑм проблеми та розгортаннÑм коду у ПРОДакшині. Дані будуть автоматично додані піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ð½Ð¾Ñ— ідеї до ПРОДакшин циклу."
+msgstr ""
msgid "The project can be accessed by any logged in user."
msgstr "ДоÑтуп до проекту можливий будь-Ñким зареєÑтрованим кориÑтувачем."
@@ -2030,13 +2504,13 @@ msgid "The repository for this project does not exist."
msgstr "Репозиторій Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту не Ñ–Ñнує."
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
-msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ð¾Ð³Ð»Ñду показує Ñ‡Ð°Ñ Ð²Ñ–Ð´ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ про об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð´Ð¾ його виконаннÑ. Дані будуть автоматично додані піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ запиту на злиттÑ."
+msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
-msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ð”Ð•Ð’ показує Ñ‡Ð°Ñ Ð¼Ñ–Ð¶ злиттÑм \"MR\" та розгортаннÑм коду у ПРОДакшин. Дані автоматично додаютьÑÑ Ð¿Ñ–ÑÐ»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñƒ ПРОДакшин вперше."
+msgstr ""
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
-msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ñ‚ÐµÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾ÐºÐ°Ð·ÑƒÑ” чаÑ, Ñкий GitLab CI виконує Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку кожного конвеєра Ð´Ð»Ñ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ð½Ð¾Ð³Ð¾ запиту злиттÑ. Дані будуть автоматично додані піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ конвеєра."
+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 зберігає інформацію про збої. Якщо протÑгом цього періоду жодних збоїв не відбуваєтьÑÑ, Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ точку Ð¼Ð¾Ð½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑкидаєтьÑÑ."
@@ -2045,13 +2519,16 @@ msgid "The time in seconds GitLab will try to access storage. After this time a
msgstr "КількіÑÑ‚ÑŒ Ñекунд, протÑгом Ñкої GitLab намагатиметьÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ доÑтуп до Ñховища даних. По завершенню цього періоду буде згенерована помилка про Ð¿ÐµÑ€ÐµÐ²Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð»Ñ–Ð¼Ñ–Ñ‚Ñƒ чаÑу."
msgid "The time taken by each data entry gathered by that stage."
-msgstr "ЧаÑ, витрачений на кожен елемент, зібраний на цьому етапі."
+msgstr ""
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr "Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3, 5, 9, Ñередніми 5, між 3, 5, 7, 8, Ñередніми (5 + 7) / 2 = 6."
msgid "There are problems accessing Git storage: "
-msgstr "Є проблеми з доÑтупом до Ñховища: "
+msgstr ""
+
+msgid "This board\\'s scope is reduced"
+msgstr "ВидиміÑÑ‚ÑŒ цієї дошки обмежена"
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° була змінена піÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾ моменту, коли ви почали Ñ—Ñ— редагувати. Ви хотіли б Ñтворити нову?"
@@ -2069,11 +2546,14 @@ msgid "This issue is locked."
msgstr "Ð¦Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð° заблокована."
msgid "This means you can not push code until you create an empty repository or import existing one."
-msgstr "Це означає, що ви не можете відправлÑти код, поки не Ñтворите порожній репозиторій або ÐЕ імпортуєте Ñ–Ñнуючий."
+msgstr ""
msgid "This merge request is locked."
msgstr "Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾."
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Ð§Ð°Ñ Ð´Ð¾ початку потраплÑÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ в планувальник"
@@ -2081,7 +2561,7 @@ msgid "Time before an issue starts implementation"
msgstr "Ð§Ð°Ñ Ð´Ð¾ початку роботи над проблемою"
msgid "Time between merge request creation and merge/close"
-msgstr "Ð§Ð°Ñ Ð¼Ñ–Ð¶ ÑтвореннÑм запиту Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– злиттÑм або закриттÑм"
+msgstr ""
msgid "Time until first merge request"
msgstr "Ð§Ð°Ñ Ð´Ð¾ першого запиту на злиттÑ"
@@ -2224,14 +2704,26 @@ msgstr[2] "хвилин"
msgid "Time|s"
msgstr "Ñекунд(а)"
+msgid "Title"
+msgstr "Ðазва"
+
msgid "Total Time"
msgstr "Загальний чаÑ"
+msgid "Total issue time spent"
+msgstr "Загальний витрачений Ñ‡Ð°Ñ Ð½Ð° проблему"
+
msgid "Total test time for all commits/merges"
msgstr "Загальний чаÑ, щоб перевірити вÑÑ– коміти/злиттÑ"
msgid "Track activity with Contribution Analytics."
-msgstr "ВідÑтежувати активніÑÑ‚ÑŒ за допомогою Ðналітики контриб’юторів."
+msgstr ""
+
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr "ВідÑтежуйте групи проблем зі Ñпільною темою з різних проектів та етапів"
+
+msgid "Turn on Service Desk"
+msgstr "Ввімкнути Service Desk"
msgid "Unlock"
msgstr "Розблокувати"
@@ -2249,16 +2741,16 @@ msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "Перейдіть на вищий тарифний план щоб активувати Покращений Глобальний Пошук."
msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "Перейдіть на вищий тарифний план Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— Ðналітики контриб’юторів."
+msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "Перейдіть на вищий тарифний план щоб активувати Групові Webhook’и."
+msgstr ""
msgid "Upgrade your plan to activate Issue weight."
-msgstr "Підвищіть план щоб активувати вагу обговорень."
+msgstr ""
msgid "Upgrade your plan to improve Issue boards."
-msgstr "Підвищіть Ñвій план, щоб покращити дошки обговорень."
+msgstr ""
msgid "Upload New File"
msgstr "Завантажити новий файл"
@@ -2269,6 +2761,9 @@ msgstr "Завантажити файл"
msgid "UploadLink|click to upload"
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 the following registration token during setup:"
msgstr "ВикориÑтовувати токен під Ñ‡Ð°Ñ ÑƒÑтановки:"
@@ -2300,10 +2795,13 @@ 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 "Ми не маємо доÑтатньо даних Ð´Ð»Ñ Ð¿Ð¾ÐºÐ°Ð·Ñƒ цього етапу."
+msgstr ""
+
+msgid "We want to be sure it is you, please confirm you are not a robot."
+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 ""
msgid "Weight"
msgstr "Вага"
@@ -2330,7 +2828,7 @@ msgid "WikiClone|Start Gollum and edit locally"
msgstr "ЗапуÑÑ‚Ñ–Ñ‚ÑŒ Gollum Ñ– редагуйте локально"
msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr "Ви не можете Ñтворювати вікі-Ñторінки"
+msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "Це — Ñтара верÑÑ–Ñ Ñторінки."
@@ -2360,7 +2858,7 @@ msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We wi
msgstr "Порада: можна вказати повний шлÑÑ… до нового файлу. Ми автоматично Ñтворимо вÑÑ– відÑутні каталоги."
msgid "WikiNewPageTitle|New Wiki Page"
-msgstr "Ðова Вікі-Ñторінка"
+msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "Ви дійÑно бажаєте видалити цю Ñторінку?"
@@ -2390,7 +2888,7 @@ msgid "Wiki|Create page"
msgstr "Створити Ñторінку"
msgid "Wiki|Edit Page"
-msgstr "Редагувати Ñторінку"
+msgstr "Ð ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ cторінки"
msgid "Wiki|Empty page"
msgstr "ÐŸÐ¾Ñ€Ð¾Ð¶Ð½Ñ Ñторінка"
@@ -2411,10 +2909,10 @@ msgid "Wiki|Pages"
msgstr "Сторінки"
msgid "Wiki|Wiki Pages"
-msgstr "Вікі-Ñторінки"
+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 ""
msgid "Withdraw Access Request"
msgstr "СкаÑувати запит доÑтупу"
@@ -2431,14 +2929,8 @@ msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ зв'Ñзок з форка Ð
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Ви збираєтеÑÑ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‚Ð¸ проект %{project_name_with_namespace} іншому влаÑнику. Ви ÐБСОЛЮТÐО впевнені?"
-msgid "You are on a read-only GitLab instance."
-msgstr "Ви знаходитеÑÑ Ð½Ð° інÑтанÑÑ– Gitlab \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\"."
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr "Ви знаходитеÑÑ Ð½Ð° інÑтанÑÑ– Gitlab \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\". Якщо ви хочете внеÑти зміни, вам необхідно перейти на %{link_to_primary_node}."
-
msgid "You can only add files when you are on a branch"
-msgstr "Ви можете додавати тільки файли, коли перебуваєте в гілці"
+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}."
@@ -2471,10 +2963,13 @@ msgid "You will receive notifications only for comments in which you were @menti
msgstr "Ви будете отримувати Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñ‚Ñ–Ð»ÑŒÐºÐ¸ Ð´Ð»Ñ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ–Ð², в Ñких ви були @згадані"
msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
-msgstr "Ви не зможете отримувати Ñ– відправлÑти код проекту через %{protocol} поки %{set_password_link} в ваш аккаунт"
+msgstr ""
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
-msgstr "Ви не зможете отримувати Ñ– відправлÑти код проекту через SSH поки %{add_ssh_key_link} в ваш профіль."
+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 "Your comment will not be visible to the public."
msgstr "Ваш коментар не буде видимим Ð´Ð»Ñ Ð²ÑÑ–Ñ…."
@@ -2488,6 +2983,12 @@ msgstr "Ваше ім'Ñ"
msgid "Your projects"
msgstr "Ваші проекти"
+msgid "branch name"
+msgstr "ім'Ñ Ð³Ñ–Ð»ÐºÐ¸"
+
+msgid "by"
+msgstr "від"
+
msgid "commit"
msgstr "коміт"
@@ -2505,9 +3006,9 @@ msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾ÑŽ поштою"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "джерело"
-msgstr[1] "джерела"
-msgstr[2] "джерел"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
msgid "password"
msgstr "пароль"
@@ -2515,8 +3016,11 @@ msgstr "пароль"
msgid "personal access token"
msgstr "оÑобиÑтий токен доÑтупу"
+msgid "source"
+msgstr "джерело"
+
msgid "to help your contributors communicate effectively!"
-msgstr "щоб допомогти вашим контриб’юторам ефективно ÑпілкуватиÑÑ!"
+msgstr ""
msgid "username"
msgstr "ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index e1bc9219908..f0a5453f224 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-23 02:44-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:42-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -51,6 +51,9 @@ msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}:已 %{failed_attempts} 次å°è¯•è®¿é—®å­˜å‚¨å¤±è´¥ï¼š"
+msgid "%{text} is available"
+msgstr "%{text}å¯ç”¨"
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如需了解更多的安装信æ¯ï¼Œè¯·æŸ¥çœ‹ %{link})"
@@ -104,14 +107,11 @@ msgid "Add Contribution guide"
msgstr "添加贡献指å—"
msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr "添加æ¥è‡ª Webhooks 或者 GitLab ä¼ä¸šç‰ˆçš„团队。"
+msgstr "添加组Webhookså’ŒGitLabä¼ä¸šç‰ˆã€‚"
msgid "Add License"
msgstr "添加许å¯è¯"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "新建一个用于推é€æˆ–拉å–çš„ SSH 秘钥到账å·ä¸­ã€‚"
-
msgid "Add new directory"
msgstr "添加目录"
@@ -124,6 +124,15 @@ msgstr "高级设置"
msgid "All"
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 fetching sidebar data"
+msgstr "获å–侧边æ æ•°æ®æ—¶å‘生错误"
+
msgid "An error occurred. Please try again."
msgstr "å‘生了错误,请å†è¯•ä¸€æ¬¡ã€‚"
@@ -133,6 +142,12 @@ msgstr "外观"
msgid "Applications"
msgstr "应用程åº"
+msgid "Apr"
+msgstr "å››"
+
+msgid "April"
+msgstr "四月"
+
msgid "Archived project! Repository is read-only"
msgstr "项目已归档ï¼å­˜å‚¨åº“为åªè¯»çŠ¶æ€"
@@ -160,6 +175,12 @@ msgstr "产物"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此处或者 %{upload_link}"
+msgid "Aug"
+msgstr "å…«"
+
+msgid "August"
+msgstr "八月"
+
msgid "Authentication Log"
msgstr "认è¯æ—¥å¿—"
@@ -193,50 +214,53 @@ msgstr "想了解更多请访问 %{link_to_documentation}"
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "您å¯ä»¥ä¸ºæ­¤é¡¹ç›®æ¿€æ´» %{link_to_settings}。"
+msgid "Available"
+msgstr "å¯ç”¨çš„"
+
msgid "Billing"
msgstr "è´¦å•"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "%{group_name} 正在使用 %{plan_link} 方案。"
+msgstr "%{group_name}ç›®å‰ä½äºŽ%{plan_link}计划中。"
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "在当å‰æ–¹æ¡ˆä¸­ä¸å¯ä½¿ç”¨è‡ªåŠ¨é™çº§æˆ–自动å‡çº§ã€‚"
+msgstr "自动é™çº§ã€å‡çº§åˆ°æŸäº›è®¡åˆ’ç›®å‰ä¸å¯ç”¨ã€‚"
msgid "BillingPlans|Current plan"
-msgstr "当å‰æ–¹æ¡ˆ"
+msgstr "当å‰è®¡åˆ’"
msgid "BillingPlans|Customer Support"
-msgstr "用户支æŒ"
+msgstr "客户支æŒ"
msgid "BillingPlans|Downgrade"
msgstr "é™çº§"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "阅读 %{faq_link} 以了解更多信æ¯ã€‚"
+msgstr "通过阅读我们的%{faq_link}了解有关æ¯ä¸ªè®¡åˆ’的更多信æ¯ã€‚"
msgid "BillingPlans|Manage plan"
-msgstr "管ç†æ–¹æ¡ˆ"
+msgstr "管ç†è®¡åˆ’"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "请è”ç³» %{customer_support_link}"
+msgstr "在这ç§æƒ…况下请è”ç³»%{customer_support_link}。"
msgid "BillingPlans|See all %{plan_name} features"
-msgstr "查看所有 %{plan_name} 功能"
+msgstr "查看所有%{plan_name}功能"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "使用与其父项目一致的方案"
+msgstr "该群组使用其父群组相关è”的计划。"
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "访问 %{parent_billing_page_link} 以管ç†è¯¥é¡¹ç›®çš„计费方案。"
+msgstr "è¦ç®¡ç†æ­¤ç¾¤ç»„的计划,请访问%{parent_billing_page_link}çš„è´¦å•éƒ¨åˆ†ã€‚"
msgid "BillingPlans|Upgrade"
msgstr "å‡çº§"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "您目å‰æ­£åœ¨ä½¿ç”¨ %{plan_link} 方案。"
+msgstr "您目å‰æ­£åœ¨ä½¿ç”¨%{plan_link}计划。"
msgid "BillingPlans|frequently asked questions"
-msgstr "常è§é—®é¢˜"
+msgstr "常问问题"
msgid "BillingPlans|monthly"
msgstr "æ¯æœˆ"
@@ -245,7 +269,7 @@ msgid "BillingPlans|paid annually at %{price_per_year}"
msgstr "æ¯å¹´æ”¯ä»˜ %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr "æ¯ä¸ªç”¨æˆ·"
+msgstr "æ¯ç”¨æˆ·"
msgid "Branch"
msgid_plural "Branches"
@@ -257,6 +281,12 @@ msgstr "已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部
msgid "Branch has changed"
msgstr "分支已有新å˜æ›´"
+msgid "Branch is already taken"
+msgstr "分支已被采用"
+
+msgid "Branch name"
+msgstr "分支å称"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -318,7 +348,7 @@ msgid "Branches|Sort by"
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 "无法删除默认分支"
@@ -333,13 +363,13 @@ 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 "è‹¥è¦æ”¾å¼ƒæœ¬åœ°æ›´æ”¹å¹¶ä½¿ç”¨ä¸Šæ¸¸ç‰ˆæœ¬è¦†ç›–本分支,请先删除并“立å³æ›´æ–°â€ã€‚"
+msgstr "è¦æ”¾å¼ƒæœ¬åœ°æ›´æ”¹å¹¶è¦†ç›–上游版本的分支,请在此处将其删除,然åŽé€‰æ‹©ä¸Šé¢çš„“立å³æ›´æ–°â€ã€‚"
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "å°†è¦æ°¸ä¹…删除å—ä¿æŠ¤ %{branch_name} 分支。"
msgid "Branches|diverged from upstream"
-msgstr "与上游存在差异"
+msgstr "上游分支"
msgid "Branches|merged"
msgstr "å·²åˆå¹¶çš„"
@@ -381,7 +411,7 @@ msgid "Cancel edit"
msgstr "å–消编辑"
msgid "Change Weight"
-msgstr "å˜æ›´æƒé‡"
+msgstr "改å˜æƒé‡"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "选择分支"
@@ -404,6 +434,12 @@ msgstr "统计图"
msgid "Chat"
msgstr "å³æ—¶é€šè®¯"
+msgid "Checking %{text} availability…"
+msgstr "正在检查%{text}çš„å¯ç”¨æ€§..."
+
+msgid "Checking branch availability..."
+msgstr "正在检查分支的å¯ç”¨æ€§..."
+
msgid "Cherry-pick this commit"
msgstr "优选此æ交"
@@ -479,8 +515,41 @@ msgstr "关闭"
msgid "Cluster"
msgstr "集群"
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr "必须在此å¸æˆ·ä¸‹åˆ›å»º %{link_to_container_project}"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr "%{appList}å·²æˆåŠŸå®‰è£…在您的群集上"
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr "%{boldNotice}这会增加一些é¢å¤–的资æºï¼Œå¦‚è´Ÿè½½å‡è¡¡å™¨ï¼Œè¿™ä¼šäº§ç”Ÿé¢å¤–çš„æˆæœ¬ã€‚请å‚阅%{pricingLink}"
+
+msgid "ClusterIntegration|API URL"
+msgstr "API地å€"
+
+msgid "ClusterIntegration|Active"
+msgstr "å¯ç”¨"
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr "添加一个现有的集群"
+
+msgid "ClusterIntegration|Add cluster"
+msgstr "添加集群"
+
+msgid "ClusterIntegration|All"
+msgstr "所有"
+
+msgid "ClusterIntegration|Applications"
+msgstr "应用程åº"
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr "CAè¯ä¹¦"
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr "è¯ä¹¦æŽˆæƒåŒ…(PEMæ ¼å¼)"
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr "选择如何设置集群集æˆ"
+
+msgid "ClusterIntegration|Cluster"
+msgstr "集群"
msgid "ClusterIntegration|Cluster details"
msgstr "集群详情"
@@ -498,26 +567,59 @@ msgid "ClusterIntegration|Cluster integration is enabled for this project. Disab
msgstr "此项目已å¯ç”¨é›†ç¾¤é›†æˆã€‚ç¦ç”¨æ­¤é›†æˆä¸ä¼šå½±å“您的集群,它åªä¼šæš‚时关闭 GitLab 的连接。"
msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
-msgstr "集群正在 Google Kubernetes Engine 上创建..."
+msgstr "群集正在Google Kubernetes Engine上创建..."
msgid "ClusterIntegration|Cluster name"
msgstr "集群å称"
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
-msgstr "集群已在 Google Kubernetes Engine 上æˆåŠŸåˆ›å»º"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr "集群已在Google Kubernetes Engine上æˆåŠŸåˆ›å»ºã€‚刷新页é¢ä»¥æŸ¥çœ‹é›†ç¾¤çš„详细信æ¯"
+
+msgid "ClusterIntegration|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 "集群å…许您使用审阅应用程åºã€éƒ¨ç½²åº”用程åºã€è¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚%{link_to_help_page}"
+
+msgid "ClusterIntegration|Copy API URL"
+msgstr "å¤åˆ¶API地å€"
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr "å¤åˆ¶CAè¯ä¹¦"
+
+msgid "ClusterIntegration|Copy Token"
+msgstr "å¤åˆ¶ä»¤ç‰Œ"
msgid "ClusterIntegration|Copy cluster name"
msgstr "å¤åˆ¶é›†ç¾¤å称"
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr "在 GitLab 上创建一个 Google Engine 集群"
+
msgid "ClusterIntegration|Create cluster"
msgstr "创建集群"
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
-msgstr "在 Google Kubernetes Engine 上创建新集群"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr "在 Google Kubernetes Engine 上创建集群"
+
+msgid "ClusterIntegration|Create on GKE"
+msgstr "在GKE中创建"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "å¯ç”¨é›†ç¾¤é›†æˆ"
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr "输入现有的 Kubernetes 集群详细信æ¯"
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr "输入您的集群详细信æ¯"
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr "环境模å¼"
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr "GKEä»·æ ¼"
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr "GitLab Runner"
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Google 云平å°é¡¹ç›®ID"
@@ -527,27 +629,75 @@ msgstr "Google Kubernetes Engine"
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr "Google Kubernetes Engine 项目"
+msgid "ClusterIntegration|Helm Tiller"
+msgstr "Helm Tiller"
+
+msgid "ClusterIntegration|Inactive"
+msgstr "待用"
+
+msgid "ClusterIntegration|Ingress"
+msgstr "å…¥å£"
+
+msgid "ClusterIntegration|Install"
+msgstr "安装"
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr "在集群上安装应用程åºã€‚阅读更多关于%{helpLink}"
+
+msgid "ClusterIntegration|Installed"
+msgstr "已安装"
+
+msgid "ClusterIntegration|Installing"
+msgstr "安装中"
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr "集群自动化集æˆ"
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "了解详细%{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr "了解更多集群的信æ¯"
+
msgid "ClusterIntegration|Machine type"
msgstr "机器类型"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "ç¡®ä¿æ‚¨çš„å¸æˆ·ç¬¦åˆåˆ›å»ºé›†ç¾¤çš„%{link_to_requirements}"
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
-msgstr "管ç†æ‚¨çš„ GitLab 项目集群集æˆ"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
+msgstr "在 GitLab 项目上管ç†é›†ç¾¤é›†æˆ"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "访问%{link_gke}æ¥ç®¡ç†æ‚¨çš„集群"
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr "GitLabä¼ä¸šé«˜çº§ç‰ˆå’Œæ——舰版æ供了多个集群"
+
+msgid "ClusterIntegration|Note:"
+msgstr "注æ„:"
+
msgid "ClusterIntegration|Number of nodes"
msgstr "节点数é‡"
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr "请为您的群集输入访问信æ¯ã€‚如果您需è¦å¸®åŠ©ï¼Œå¯ä»¥é˜…读我们关于集群的 %{link_to_help_page}"
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "请确ä¿æ‚¨çš„ Google å¸æˆ·ç¬¦åˆä»¥ä¸‹è¦æ±‚:"
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr "设置集群时出现问题"
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr "设置集群列表时出现问题"
+
+msgid "ClusterIntegration|Project ID"
+msgstr "项目 ID"
+
+msgid "ClusterIntegration|Project namespace"
+msgstr "项目命å空间"
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "项目命å空间(å¯é€‰ï¼Œå”¯ä¸€)"
@@ -560,8 +710,14 @@ msgstr "删除集群集æˆ"
msgid "ClusterIntegration|Remove integration"
msgstr "删除集æˆ"
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr "删除集群集æˆå°†åˆ é™¤æ‚¨æ·»åŠ åˆ°æ­¤é¡¹ç›®çš„集群é…置。它ä¸ä¼šåˆ é™¤æ‚¨çš„项目。"
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr "删除集群集æˆå°†åˆ é™¤å·²æ·»åŠ åˆ°æ­¤é¡¹ç›®çš„集群é…置。它ä¸ä¼šåˆ é™¤ Google Kubernetes Engine 上的集群。"
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr "请求安装失败"
+
+msgid "ClusterIntegration|Save changes"
+msgstr "ä¿å­˜æ›´æ”¹"
msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "查看并编辑集群的详细信æ¯"
@@ -575,20 +731,38 @@ msgstr "看到您的项目"
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr "在 Google Kubernetes Engine 上创建集群时å‘生错误"
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr "安装 %{title} æ—¶å‘生故障"
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr "没有è¦æ˜¾ç¤ºçš„集群"
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr "æ­¤å¸æˆ·å¿…须有æƒåœ¨ä¸‹é¢æŒ‡å®šçš„%{link_to_container_project}中创建集群"
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr "切æ¢é›†ç¾¤"
+msgid "ClusterIntegration|Token"
+msgstr "令牌"
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "使用与此项目关è”的集群,您å¯ä»¥ä½¿ç”¨å®¡é˜…应用程åºï¼Œéƒ¨ç½²åº”用程åºï¼Œè¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚"
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
-msgstr "您的å¸æˆ·å¿…须有%{link_to_kubernetes_engine}"
+msgstr "您的å¸æˆ·å¿…须拥有%{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "区域"
@@ -599,9 +773,15 @@ msgstr "访问 Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "集群"
+msgid "ClusterIntegration|documentation"
+msgstr "文档"
+
msgid "ClusterIntegration|help page"
msgstr "帮助页é¢"
+msgid "ClusterIntegration|installing applications"
+msgstr "安装应用程åº"
+
msgid "ClusterIntegration|meets the requirements"
msgstr "符åˆè¦æ±‚"
@@ -615,10 +795,6 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] "æ交 %d 个文件"
-
msgid "Commit Message"
msgstr "æ交消æ¯"
@@ -700,11 +876,20 @@ msgstr "贡献指å—"
msgid "Contributors"
msgstr "贡献者"
+msgid "ContributorsPage|Building repository graph."
+msgstr "构建存储库图标。"
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+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 的最大并å‘性"
+msgstr "控制此次è¦èŠ‚点的 LFS/attachment 的最大并å‘"
msgid "Control the maximum concurrency of repository backfill for this secondary node"
-msgstr "控制此次è¦èŠ‚点的存储库的最大并å‘"
+msgstr "控制此次è¦èŠ‚点的存储库最大并å‘"
msgid "Copy SSH public key to clipboard"
msgstr "å¤åˆ¶ SSH 公钥到剪贴æ¿"
@@ -727,6 +912,9 @@ msgstr "创建目录"
msgid "Create empty bare repository"
msgstr "创建空的存储库"
+msgid "Create epic"
+msgstr "创建EPIC"
+
msgid "Create file"
msgstr "创建文件"
@@ -754,6 +942,9 @@ msgstr "标签"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "创建个人访问令牌"
+msgid "Creating epic"
+msgstr "创建EPIC中"
+
msgid "Cron Timezone"
msgstr "Cron 时区"
@@ -799,6 +990,12 @@ msgstr "所有"
msgid "DashboardProjects|Personal"
msgstr "个人"
+msgid "Dec"
+msgstr "å二"
+
+msgid "December"
+msgstr "å二月"
+
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 语法定义自定义模å¼"
@@ -816,7 +1013,7 @@ msgid "Description"
msgstr "æè¿°"
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
-msgstr "æ述模æ¿å…许您为项目的议题和åˆå¹¶è¯·æ±‚在创建时选择特定的模版。"
+msgstr "æ述模æ¿å…许您为项目的问题和åˆå¹¶è¯·æ±‚定义æ述字段的特定模æ¿ã€‚"
msgid "Details"
msgstr "详情"
@@ -872,6 +1069,72 @@ msgstr "编辑 %{id} æµæ°´çº¿è®¡åˆ’"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr "EPIC将被删除!是å¦ç¡®å®šï¼Ÿ"
+
+msgid "Epics"
+msgstr "EPIC"
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr "EPIC让你更有效率地管ç†ä½ çš„项目组åˆï¼Œè€Œä¸”ä¸è´¹å¹ç°ä¹‹åŠ›"
+
+msgid "Error creating epic"
+msgstr "创建EPIC时出错"
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr "切æ¢é€šçŸ¥è®¢é˜…æ—¶å‘生错误"
+
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -911,6 +1174,12 @@ msgstr "无法å˜æ›´æ‰€æœ‰è€…"
msgid "Failed to remove the pipeline schedule"
msgstr "无法删除æµæ°´çº¿è®¡åˆ’"
+msgid "Feb"
+msgstr "二"
+
+msgid "February"
+msgstr "二月"
+
msgid "File name"
msgstr "文件å"
@@ -957,14 +1226,29 @@ msgstr "GPG 密钥"
msgid "Geo Nodes"
msgstr "Geo 节点"
+msgid "GeoNodeSyncStatus|Failed"
+msgstr "失败"
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr "节点出现故障或æŸå。"
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr "节点è¿è¡Œç¼“æ…¢ã€è¶…è½½, 或者在åœæœºåŽåˆšåˆšæ¢å¤ã€‚"
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr "未åŒæ­¥"
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr "å·²åŒæ­¥"
+
msgid "Geo|File sync capacity"
-msgstr "文件åŒæ­¥å®¹é‡"
+msgstr "文件åŒæ­¥é‡"
msgid "Geo|Groups to replicate"
-msgstr "è¦å¤åˆ¶çš„群组"
+msgstr "å¤åˆ¶ç¾¤ç»„"
msgid "Geo|Repository sync capacity"
-msgstr "存储库åŒæ­¥å®¹é‡"
+msgstr "存储库åŒæ­¥é‡"
msgid "Geo|Select groups to replicate."
msgstr "选择è¦å¤åˆ¶çš„群组。"
@@ -1020,9 +1304,6 @@ msgstr "找ä¸åˆ°ç¾¤ç»„"
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "您å¯ä»¥ç®¡ç†ç¾¤ç»„æˆå‘˜çš„æƒé™å¹¶è®¿é—®ç¾¤ç»„中的æ¯ä¸ªé¡¹ç›®ã€‚"
-msgid "GroupsTreeRole|as"
-msgstr "çš„"
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr "您确定è¦ç¦»å¼€ç¾¤ç»„“${this.group.fullName}â€å—?"
@@ -1053,6 +1334,9 @@ msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ä»»ä½•ç¬¦åˆçš„群组"
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰ä»»ä½•ç¾¤ç»„或项目符åˆæ‚¨çš„æœç´¢"
+msgid "Have your users email"
+msgstr "有你的用户邮件"
+
msgid "Health Check"
msgstr "å¥åº·æ£€æŸ¥"
@@ -1111,9 +1395,6 @@ msgstr "周期分æžç®€ä»‹"
msgid "Issue board focus mode"
msgstr "议题看æ¿æ¨¡å¼"
-msgid "Issue boards with milestones"
-msgstr "议题看æ¿ä¸Žé‡Œç¨‹ç¢‘"
-
msgid "Issue events"
msgstr "议题事件"
@@ -1126,6 +1407,24 @@ msgstr "看æ¿"
msgid "Issues"
msgstr "议题"
+msgid "Jan"
+msgstr "一"
+
+msgid "January"
+msgstr "一月"
+
+msgid "Jul"
+msgstr "七"
+
+msgid "July"
+msgstr "七月"
+
+msgid "Jun"
+msgstr "å…­"
+
+msgid "June"
+msgstr "六月"
+
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -1197,9 +1496,18 @@ msgstr "å·²é”定文件"
msgid "Login"
msgstr "登录"
+msgid "Mar"
+msgstr "三"
+
+msgid "March"
+msgstr "三月"
+
msgid "Maximum git storage failures"
msgstr "最大 git 存储失败"
+msgid "May"
+msgstr "五"
+
msgid "Median"
msgstr "中ä½æ•°"
@@ -1243,9 +1551,15 @@ msgstr "创建æµæ°´çº¿è®¡åˆ’"
msgid "New branch"
msgstr "新建分支"
+msgid "New branch unavailable"
+msgstr "新分支ä¸å¯ç”¨"
+
msgid "New directory"
msgstr "新建目录"
+msgid "New epic"
+msgstr "æ–°EPIC"
+
msgid "New file"
msgstr "新建文件"
@@ -1282,6 +1596,9 @@ msgstr "没有存储库"
msgid "No schedules"
msgstr "没有计划"
+msgid "No time spent"
+msgstr "没有花费时间"
+
msgid "None"
msgstr "æ— "
@@ -1348,18 +1665,33 @@ msgstr "关注"
msgid "Notifications"
msgstr "通知"
+msgid "Nov"
+msgstr "å一"
+
+msgid "November"
+msgstr "å一月"
+
msgid "Number of access attempts"
msgstr "å°è¯•è®¿é—®æ¬¡æ•°"
msgid "Number of failures before backing off"
msgstr "退出å‰çš„失败次数"
+msgid "Oct"
+msgstr "å"
+
+msgid "October"
+msgstr "å月"
+
msgid "OfSearchInADropdown|Filter"
msgstr "筛选"
msgid "Only project members can comment."
msgstr "åªæœ‰é¡¹ç›®æˆå‘˜å¯ä»¥å‘表评论。"
+msgid "Opened"
+msgstr "已打开"
+
msgid "OpenedNDaysAgo|Opened"
msgstr "创建于"
@@ -1492,6 +1824,9 @@ msgstr "于阶段"
msgid "Pipeline|with stages"
msgstr "于阶段"
+msgid "Please solve the reCAPTCHA"
+msgstr "请填写验è¯ç ã€‚"
+
msgid "Preferences"
msgstr "å好设置"
@@ -1597,9 +1932,15 @@ msgstr "分支图"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr "è”系管ç†å‘˜æ›´æ”¹æ­¤è®¾ç½®ã€‚"
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr "ç«‹å³åœ¨é»˜è®¤åˆ†æ”¯ä¸Šè¿è¡Œæµæ°´çº¿"
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr "åªæœ‰å·²ç­¾ç½²æ交æ‰å¯ä»¥æŽ¨é€åˆ°æ­¤å­˜å‚¨åº“。"
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr "设置CI/CD时出现JavaScript问题"
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr "此设置已应用于æœåŠ¡å™¨çº§åˆ«ï¼Œå¯ç”±ç®¡ç†å‘˜è¦†ç›–。"
@@ -1636,6 +1977,39 @@ msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ç¬¦åˆæ¡ä»¶çš„项目"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "此功能需è¦æµè§ˆå™¨æ”¯æŒ localStorage"
+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 "默认情况下,Prometheus ä¾¦å¬ â€˜http://localhost:9090’。ä¸å»ºè®®æ›´æ”¹é»˜è®¤åœ°å€å’Œç«¯å£ï¼Œå› ä¸ºè¿™å¯èƒ½ä¼šå½±å“或冲çªåœ¨ GitLab æœåŠ¡å™¨ä¸Šè¿è¡Œçš„其他æœåŠ¡ã€‚"
+
+msgid "PrometheusService|Finding and configuring metrics..."
+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|Monitored"
+msgstr "监测"
+
+msgid "PrometheusService|More information"
+msgstr "更多的信æ¯"
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr "没有监测指标。è¦å¼€å§‹ç›‘测,请部署到环境中。"
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr "Prometheus API 地å€ï¼Œä¾‹å¦‚ http://prometheus.example.com/"
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr "Prometheus 监测"
+
+msgid "PrometheusService|View environments"
+msgstr "查看环境"
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "公开 - 群组和任何公共项目å¯ä»¥åœ¨æ²¡æœ‰ä»»ä½•èº«ä»½éªŒè¯çš„情况下查看。"
@@ -1732,6 +2106,9 @@ msgstr "日程"
msgid "Scheduling Pipelines"
msgstr "æµæ°´çº¿è®¡åˆ’"
+msgid "Scoped issue boards"
+msgstr "议题看æ¿èŒƒå›´"
+
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ ‡ç­¾"
@@ -1753,6 +2130,12 @@ msgstr "选择时区"
msgid "Select target branch"
msgstr "选择目标分支"
+msgid "Sep"
+msgstr "ä¹"
+
+msgid "September"
+msgstr "ä¹æœˆ"
+
msgid "Service Templates"
msgstr "æœåŠ¡æ¨¡æ¿"
@@ -1784,14 +2167,29 @@ msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "显示 %d 个事件"
+msgid "Sidebar|Change weight"
+msgstr "编辑宽度"
+
+msgid "Sidebar|Edit"
+msgstr "编辑"
+
+msgid "Sidebar|No"
+msgstr "æ— "
+
+msgid "Sidebar|None"
+msgstr "æ— "
+
+msgid "Sidebar|Weight"
+msgstr "宽度"
+
msgid "Snippets"
msgstr "代ç ç‰‡æ®µ"
msgid "Something went wrong on our end."
msgstr "å‘生了错误。"
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "å°è¯•æ›´æ”¹ ${this.issuableDisplayName(this.issuableType)} çš„é”定状æ€æ—¶å‘生错误"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgstr "è¯•å›¾æ”¹å˜ ${this.issuableDisplayName} çš„é”定状æ€æ—¶å‡ºé”™äº†"
msgid "Something went wrong while fetching the projects."
msgstr "拉å–项目时å‘生错误。"
@@ -1854,7 +2252,7 @@ msgid "SortOptions|Milestone due soon"
msgstr "å³å°†æˆªæ­¢çš„里程碑"
msgid "SortOptions|More weight"
-msgstr "更大的æƒé‡"
+msgstr "最高æƒé‡"
msgid "SortOptions|Most popular"
msgstr "最å—欢迎"
@@ -1898,9 +2296,15 @@ msgstr "现在开始"
msgid "SortOptions|Weight"
msgstr "æƒé‡"
+msgid "Source"
+msgstr "æº"
+
msgid "Source code"
msgstr "æºä»£ç "
+msgid "Source is not available"
+msgstr "æºä¸å¯ç”¨"
+
msgid "Spam Logs"
msgstr "垃圾信æ¯æ—¥å¿—"
@@ -1919,6 +2323,9 @@ msgstr "由此更改 %{new_merge_request}"
msgid "Start the Runner!"
msgstr "å¯åŠ¨ Runner!"
+msgid "Stopped"
+msgstr "å·²åœæ­¢"
+
msgid "Subgroups"
msgstr "å­ç¾¤ç»„"
@@ -1938,6 +2345,75 @@ msgstr[0] "标签"
msgid "Tags"
msgstr "标签"
+msgid "TagsPage|Browse commits"
+msgstr "æµè§ˆæ交"
+
+msgid "TagsPage|Browse files"
+msgstr "æµè§ˆæ–‡ä»¶"
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr "无法找到此标记的HEADæ交"
+
+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 "删除 %{tag_name} åŽå°†æ— æ³•æ¢å¤ï¼Œæ‚¨ç¡®å®šï¼Ÿ"
+
+msgid "TagsPage|Edit release notes"
+msgstr "编辑å‘行记录"
+
+msgid "TagsPage|Existing branch name, tag, or commit SHA"
+msgstr "已存在分支å称,标记或æ交SHA"
+
+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 "(å¯é€‰)å°†å‘行说明添加到标签。它们将被存储在GitLabæ•°æ®åº“中并显示在标签页上。"
+
+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 "使用git tag命令添加一个:"
+
+msgid "TagsPage|Write your release notes or drag files here..."
+msgstr "撰写å‘行说明或拖放文件到这里..."
+
+msgid "TagsPage|protected"
+msgstr "å·²ä¿æŠ¤"
+
msgid "Target Branch"
msgstr "目标分支"
@@ -1945,10 +2421,10 @@ msgid "Team"
msgstr "团队"
msgid "Thanks! Don't show me this again"
-msgstr "ä¸å†æ˜¾ç¤ºè¯¥æ示"
+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 中的高级全局æœç´¢åŠŸèƒ½æ˜¯éžå¸¸å¼ºå¤§çš„æœç´¢æœåŠ¡ã€‚您å¯ä»¥æœç´¢å…¶ä»–团队的代ç ä»¥å¸®åŠ©æ‚¨å®Œå–„自己项目中的代ç ã€‚从而é¿å…创建é‡å¤çš„代ç æˆ–浪费时间。"
+msgstr "GitLab 中的高级全局æœç´¢åŠŸèƒ½æ˜¯éžå¸¸å¼ºå¤§çš„æœç´¢æœåŠ¡ã€‚您å¯ä»¥æœç´¢å…¶ä»–团队的代ç ä»¥å¸®åŠ©æ‚¨å®Œå–„自己项目中的代ç ã€‚从而é¿å…创建é‡å¤çš„代ç å’Œæµªè´¹æ—¶é—´ã€‚"
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "断路器关闭阈值应该低于故障计数阈值"
@@ -2019,6 +2495,9 @@ msgstr "中ä½æ•°æ˜¯ä¸€ä¸ªæ•°åˆ—中最中间的值。例如在 3ã€5ã€9 之间ï
msgid "There are problems accessing Git storage: "
msgstr "访问 Git 存储时出现问题:"
+msgid "This board\\'s scope is reduced"
+msgstr "这个看æ¿çš„范围缩å°äº†"
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "自您开始编辑åŽ, 此分支已更改。您想创建一个新的分支å—?"
@@ -2040,6 +2519,9 @@ msgstr "在创建一个空的存储库或导入现有存储库之å‰ï¼Œå°†æ— æ³•
msgid "This merge request is locked."
msgstr "æ­¤åˆå¹¶è¯·æ±‚å·²é”定。"
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr "这些电å­é‚®ä»¶è‡ªåŠ¨ç”Ÿæˆä¸ºé—®é¢˜(评论生æˆä¸ºç”µå­é‚®ä»¶å¯¹è¯)在这里列出。"
+
msgid "Time before an issue gets scheduled"
msgstr "议题被列入日程表的时间"
@@ -2186,14 +2668,26 @@ msgstr[0] "分钟"
msgid "Time|s"
msgstr "秒"
+msgid "Title"
+msgstr "标题"
+
msgid "Total Time"
msgstr "总时间"
+msgid "Total issue time spent"
+msgstr "议题花费时间总计"
+
msgid "Total test time for all commits/merges"
msgstr "所有æ交和åˆå¹¶çš„总测试时间"
msgid "Track activity with Contribution Analytics."
-msgstr "跟踪分æžè´¡çŒ®ä¸Žæ´»åŠ¨ã€‚"
+msgstr "跟踪活动与贡献的分æžã€‚"
+
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr "在项目和里程碑之间跟踪共享主题的议题组"
+
+msgid "Turn on Service Desk"
+msgstr "打开æœåŠ¡å°"
msgid "Unlock"
msgstr "解é”"
@@ -2231,6 +2725,9 @@ msgstr "上传文件"
msgid "UploadLink|click to upload"
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 the following registration token during setup:"
msgstr "在安装过程中使用以下注册令牌:"
@@ -2264,6 +2761,9 @@ 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 "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 。"
@@ -2393,12 +2893,6 @@ msgstr "å³å°†åˆ é™¤ä¸Žæºé¡¹ç›® %{forked_from_project} 的派生关系。确定
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å³å°† %{project_name_with_namespace} 转移给å¦ä¸€ä¸ªæ‰€æœ‰è€…。确定继续å—?"
-msgid "You are on a read-only GitLab instance."
-msgstr "您在一个åªè¯» GitLab 实例上。"
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr "您在一个åªè¯»çš„ GitLab 实例上。如果您想进行任何更改,您必须访问%{link_to_primary_node}。"
-
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
@@ -2438,6 +2932,9 @@ msgstr "在账å·ä¸­ %{set_password_link} 之å‰å°†æ— æ³•é€šè¿‡ %{protocol} 拉å
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "在账å·ä¸­ %{add_ssh_key_link} 之å‰å°†æ— æ³•é€šè¿‡ SSH 拉å–或推é€ä»£ç ã€‚"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr "在您的个人资料中添加SSH密钥之å‰ï¼Œæ‚¨ä¸èƒ½é€šè¿‡SSHæ¥æ‹‰å–或推é€é¡¹ç›®ä»£ç ã€‚"
+
msgid "Your comment will not be visible to the public."
msgstr "您的评论将ä¸ä¼šå…¬å¼€æ˜¾ç¤ºã€‚"
@@ -2450,6 +2947,12 @@ msgstr "您的åå­—"
msgid "Your projects"
msgstr "您的项目"
+msgid "branch name"
+msgstr "分支å称"
+
+msgid "by"
+msgstr "æ¥è‡ª"
+
msgid "commit"
msgstr "æ交"
@@ -2473,6 +2976,9 @@ msgstr "密ç "
msgid "personal access token"
msgstr "个人访问令牌"
+msgid "source"
+msgstr "æº"
+
msgid "to help your contributors communicate effectively!"
msgstr "帮助您的贡献者进行有效沟通ï¼"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index b851809fc7c..b368487ac71 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-15 02:54-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:42-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Language: zh_HK\n"
@@ -51,6 +51,9 @@ msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}:已訪å•æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(想了解更多的安è£è¨Šæ¯è«‹æŸ¥çœ‹ %{link})"
@@ -109,9 +112,6 @@ msgstr ""
msgid "Add License"
msgstr "添加許å¯è­‰"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "新增壹個用於推é€æˆ–拉å–çš„ SSH 秘鑰到賬號中。"
-
msgid "Add new directory"
msgstr "添加新目錄"
@@ -124,6 +124,15 @@ msgstr ""
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -133,6 +142,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "歸檔項目ï¼å­˜å„²åº«ç‚ºåªè®€"
@@ -160,6 +175,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此處或者 %{upload_link}"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -193,6 +214,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -257,6 +281,12 @@ msgstr "分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -404,6 +434,12 @@ msgstr "統計圖"
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "優é¸æ­¤æ交"
@@ -479,7 +515,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -503,21 +572,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -527,27 +629,75 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -560,7 +710,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -575,15 +731,33 @@ msgstr ""
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -599,9 +773,15 @@ msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -615,10 +795,6 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] ""
-
msgid "Commit Message"
msgstr ""
@@ -700,6 +876,15 @@ msgstr "è²¢ç»æŒ‡å—"
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
@@ -727,6 +912,9 @@ msgstr "創建目錄"
msgid "Create empty bare repository"
msgstr "創建空的存儲庫"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr ""
@@ -754,6 +942,9 @@ msgstr "標籤"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "創建個人訪å•ä»¤ç‰Œ"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Cron 時å€"
@@ -799,6 +990,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 語法定義自定義模å¼"
@@ -872,6 +1069,72 @@ msgstr "編輯 %{id} æµæ°´ç·šè¨ˆåŠƒ"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -911,6 +1174,12 @@ msgstr "無法變更所有者"
msgid "Failed to remove the pipeline schedule"
msgstr "無法刪除æµæ°´ç·šè¨ˆåŠƒ"
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -957,6 +1226,21 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
+
msgid "Geo|File sync capacity"
msgstr ""
@@ -1020,9 +1304,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTreeRole|as"
-msgstr ""
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr ""
@@ -1053,6 +1334,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr "å¥åº·æª¢æŸ¥ (Health Check)"
@@ -1111,9 +1395,6 @@ msgstr "週期分æžç°¡ä»‹"
msgid "Issue board focus mode"
msgstr ""
-msgid "Issue boards with milestones"
-msgstr ""
-
msgid "Issue events"
msgstr "議題事件 (issue event)"
@@ -1126,6 +1407,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -1197,9 +1496,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•¸"
@@ -1243,9 +1551,15 @@ msgstr "創建æµæ°´ç·šè¨ˆåŠƒ"
msgid "New branch"
msgstr "新增分支"
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "新增目錄"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "新增文件"
@@ -1282,6 +1596,9 @@ msgstr "沒有存儲庫"
msgid "No schedules"
msgstr "沒有計劃"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr ""
@@ -1348,18 +1665,33 @@ msgstr "關注"
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
msgid "Only project members can comment."
msgstr ""
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
@@ -1492,6 +1824,9 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -1597,9 +1932,15 @@ msgstr "分支圖"
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
@@ -1636,6 +1977,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1732,6 +2106,9 @@ msgstr ""
msgid "Scheduling Pipelines"
msgstr "æµæ°´ç·šè¨ˆåŠƒ"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ¨™ç±¤"
@@ -1753,6 +2130,12 @@ msgstr "é¸æ“‡æ™‚å€"
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯"
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1784,13 +2167,28 @@ msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1898,9 +2296,15 @@ msgstr ""
msgid "SortOptions|Weight"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr "æºä»£ç¢¼"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1919,6 +2323,9 @@ msgstr "由此更改 %{new_merge_request}"
msgid "Start the Runner!"
msgstr "é‹ä½œ Runner!"
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -1938,6 +2345,75 @@ msgstr[0] "標籤"
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 "目標分支"
@@ -2019,6 +2495,9 @@ msgstr "中ä½æ•¸æ˜¯å£¹å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "è¨ªå• Git 存儲時出ç¾å•é¡Œï¼š"
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
@@ -2040,6 +2519,9 @@ msgstr "在創建壹個空的存儲庫或導入ç¾æœ‰å­˜å„²åº«ä¹‹å‰ï¼Œæ‚¨å°‡ç„¡
msgid "This merge request is locked."
msgstr ""
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "議題被列入日程表的時間"
@@ -2186,15 +2668,27 @@ msgstr[0] "分é˜"
msgid "Time|s"
msgstr "秒"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "總時間"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "所有æ交和åˆä½µçš„總測試時間"
msgid "Track activity with Contribution Analytics."
msgstr ""
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -2231,6 +2725,9 @@ msgstr "上傳文件"
msgid "UploadLink|click to upload"
msgstr "點擊上傳"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨ä»¥ä¸‹è¨»å†Šä»¤ç‰Œï¼š"
@@ -2264,6 +2761,9 @@ 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 "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 ""
@@ -2393,12 +2893,6 @@ msgstr "å³å°‡åˆªé™¤èˆ‡æºé …ç›® %{forked_from_project} 的派生關系。確定
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å³å°‡ %{project_name_with_namespace} 轉義給å¦å£¹å€‹æ‰€æœ‰è€…。確定繼續嗎?"
-msgid "You are on a read-only GitLab instance."
-msgstr ""
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr ""
-
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
@@ -2438,6 +2932,9 @@ msgstr "在賬號上 %{set_password_link} 之å‰å°‡ç„¡æ³•é€šéŽ %{protocol} 拉
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "在賬號中 %{add_ssh_key_link} 之å‰å°‡ç„¡æ³•é€šéŽ SSH 拉å–或推é€ä»£ç¢¼ã€‚"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2450,6 +2947,12 @@ msgstr "您的åå­—"
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
msgstr ""
@@ -2473,6 +2976,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr ""
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index b6d4ed27487..76c1e598433 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-02 14:42+0100\n"
-"PO-Revision-Date: 2017-11-20 03:59-0500\n"
+"POT-Creation-Date: 2017-12-12 18:31+0000\n"
+"PO-Revision-Date: 2018-01-05 04:42-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
@@ -51,6 +51,9 @@ msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "%{storage_name}:已存å–此主機失敗 %{failed_attempts} 次"
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如何安è£è«‹åƒé–± %{link})"
@@ -95,7 +98,7 @@ msgid "Activity"
msgstr "活動"
msgid "Add"
-msgstr "增加"
+msgstr ""
msgid "Add Changelog"
msgstr "新增更新日誌"
@@ -104,14 +107,11 @@ msgid "Add Contribution guide"
msgstr "新增å”作指å—"
msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr "加入來自 Webhooks 或者是 GitLab ä¼æ¥­ç‰ˆçš„群組"
+msgstr ""
msgid "Add License"
msgstr "新增授權æ¢æ¬¾"
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "å°‡ SSH 金鑰新增至您的個人帳號後, å³å¯é€éŽ SSH 來上傳 (push) 或下載 (pull) 。"
-
msgid "Add new directory"
msgstr "新增目錄"
@@ -124,6 +124,15 @@ msgstr "進階設定"
msgid "All"
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 fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr "發生錯誤,請å†è©¦ä¸€æ¬¡ã€‚"
@@ -133,6 +142,12 @@ msgstr "外觀"
msgid "Applications"
msgstr "應用程å¼"
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "此專案已å°å­˜ï¼æª”案庫 (repository) 為唯讀狀態"
@@ -160,6 +175,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放檔案到此處或者 %{upload_link}"
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr "登入紀錄"
@@ -193,59 +214,62 @@ msgstr "了解更多於 %{link_to_documentation}"
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "ä½ å¯ä»¥ç‚ºæ­¤å°ˆæ¡ˆå•Ÿå‹• %{link_to_settings}"
+msgid "Available"
+msgstr ""
+
msgid "Billing"
-msgstr "方案"
+msgstr ""
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr "%{group_name} ç›®å‰ä½¿ç”¨ %{plan_link} 方案。"
+msgstr ""
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr "ç›®å‰ç„¡æ³•è‡ªå‹•å‡ç´šæˆ–é™ç´šè‡³å…¶ä»–方案。"
+msgstr ""
msgid "BillingPlans|Current plan"
-msgstr "ç›®å‰æ–¹æ¡ˆ"
+msgstr ""
msgid "BillingPlans|Customer Support"
-msgstr "客戶æœå‹™"
+msgstr ""
msgid "BillingPlans|Downgrade"
-msgstr "é™ç´š"
+msgstr ""
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr "了解更多我們的方案,或是閱讀 %{faq_link}"
+msgstr ""
msgid "BillingPlans|Manage plan"
-msgstr "管ç†æ–¹æ¡ˆ"
+msgstr ""
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr "è«‹è¯ç¹« %{customer_support_link}"
+msgstr ""
msgid "BillingPlans|See all %{plan_name} features"
-msgstr "查看更多 %{plan_name} 功能"
+msgstr ""
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "此群組與上層群組使用相åŒçš„方案。"
+msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr "請至 %{parent_billing_page_link} 來管ç†æ­¤ç¾¤çµ„的方案。"
+msgstr ""
msgid "BillingPlans|Upgrade"
-msgstr "å‡ç´š"
+msgstr ""
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr "ç›®å‰ä½¿ç”¨ %{plan_link} 方案。"
+msgstr ""
msgid "BillingPlans|frequently asked questions"
-msgstr "常見å•é¡Œ"
+msgstr ""
msgid "BillingPlans|monthly"
-msgstr "æ¯å€‹æœˆ"
+msgstr ""
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr "æ¯å¹´æ”¶å– %{price_per_year}"
+msgstr ""
msgid "BillingPlans|per user"
-msgstr "æ¯ä½ä½¿ç”¨è€…"
+msgstr ""
msgid "Branch"
msgid_plural "Branches"
@@ -257,6 +281,12 @@ msgstr "已建立分支 (branch) <strong>%{branch_name}</strong> 。如需設定
msgid "Branch has changed"
msgstr "分支(branch)已變更"
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœå°‹åˆ†æ”¯ (branches)"
@@ -318,7 +348,7 @@ msgid "Branches|Sort by"
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 "無法刪除é è¨­åˆ†æ”¯"
@@ -339,7 +369,7 @@ msgid "Branches|You’re about to permanently delete the protected branch %{bran
msgstr "你將永久刪除å—ä¿è­·çš„ %{branch_name} 分支。"
msgid "Branches|diverged from upstream"
-msgstr "與上游分歧"
+msgstr ""
msgid "Branches|merged"
msgstr "å·²åˆä½µ"
@@ -381,7 +411,7 @@ msgid "Cancel edit"
msgstr "å–消編輯"
msgid "Change Weight"
-msgstr "變更權é‡"
+msgstr ""
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑é¸åˆ°åˆ†æ”¯ (branch) "
@@ -404,6 +434,12 @@ msgstr "統計圖"
msgid "Chat"
msgstr "å³æ™‚通訊"
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "挑é¸æ­¤æ›´å‹•è¨˜éŒ„ (commit) "
@@ -411,7 +447,7 @@ msgid "Cherry-pick this merge request"
msgstr "挑é¸æ­¤åˆä½µè«‹æ±‚ (merge request) "
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
-msgstr "é¸æ“‡ä½ æƒ³è¦è¤‡è£½åˆ°ç¬¬äºŒç¯€é»žçš„群組。若留白則會複製全部的群組。"
+msgstr ""
msgid "CiStatusLabel|canceled"
msgstr "å·²å–消"
@@ -474,13 +510,46 @@ msgid "Clone repository"
msgstr "複製(clone)檔案庫(repository)"
msgid "Close"
-msgstr "關閉"
+msgstr ""
msgid "Cluster"
msgstr "å¢é›†"
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr "必須在此帳號下建立 %{link_to_container_project}"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
+msgstr ""
msgid "ClusterIntegration|Cluster details"
msgstr "å¢é›†è©³æƒ…"
@@ -498,56 +567,137 @@ msgid "ClusterIntegration|Cluster integration is enabled for this project. Disab
msgstr "此專案已啟用å¢é›†æ•´åˆã€‚ç¦æ­¢å¢é›†æ•´åˆä¸æœƒå½±éŸ¿æ‚¨çš„å¢é›†ï¼Œå®ƒåªæ˜¯æš«æ™‚關閉 GitLab 的連接。"
msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
-msgstr "在 Google 容器引擎中建立新的å¢é›†"
+msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr "å¢é›†å稱"
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
-msgstr "在 Google 容器引擎上æˆåŠŸå»ºç«‹å¢é›†"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|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|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
+msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr "複製å¢é›†å稱"
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr "建立å¢é›†"
-msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
-msgstr "在 Google 容器引擎中建立新的å¢é›†"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
+msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr "å•Ÿå‹•å¢é›†æ•´åˆ"
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Google 雲端專案 ID"
msgid "ClusterIntegration|Google Kubernetes Engine"
-msgstr "Google 容器引擎"
+msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
-msgstr "Google 容器引擎專案"
+msgstr ""
+
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "學習更多有關於%{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr "機器型別"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "請確èªæ‚¨çš„帳戶中%{link_to_requirements} 是å¦å»ºç«‹å¢é›†"
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
-msgstr "在你的 GitLab 專案上管ç†å¢é›†æ•´åˆ"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
+msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "請至 %{link_gke} 管ç†ä½ çš„å¢é›†"
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr "所有的端點數é‡"
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "請確èªä½ çš„ Google 帳號是å¦ç¬¦åˆé€™äº›æ¢ä»¶"
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "專案命å空間(é¸å¡«ï¼Œä¸å¯é‡è¤‡ï¼‰"
@@ -560,8 +710,14 @@ msgstr "刪除å¢é›†æ•´åˆ"
msgid "ClusterIntegration|Remove integration"
msgstr "刪除整åˆ"
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr "當刪除å¢é›†éœ€è¦åŠ å…¥å°ˆæ¡ˆçš„定義組態檔,會刪除å¢é›†æ•´åˆã€‚這並ä¸æœƒåˆªé™¤ä½ çš„專案。 刪除å¢é›†çš„åŒæ™‚,將一起刪除已加入此專案的定義組態檔,但你的專案ä¸æœƒå› æ­¤è¢«åˆªé™¤ã€‚"
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
+msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "查看與編輯你的å¢é›†å…§å®¹"
@@ -575,33 +731,57 @@ msgstr "查看您的專案"
msgid "ClusterIntegration|See zones"
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 cluster on Google Kubernetes Engine"
-msgstr "在 Google Kubernetes Engine 上建立å¢é›†æ™‚發生了錯誤"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr "å¢é›†é–‹é—œ"
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "當å¢é›†é€£çµåˆ°æ­¤å°ˆæ¡ˆï¼Œä½ å¯ä»¥ä½¿ç”¨è¤‡é–±æ‡‰ç”¨ (review apps),部署你的應用程å¼ï¼ŒåŸ·è¡Œä½ çš„æµæ°´ç·š (pipelines),還有更多容易上手的方å¼å¯ä»¥ä½¿ç”¨ã€‚"
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
-msgstr "您的帳號必須有 %{link_to_kubernetes_engine}"
+msgstr ""
msgid "ClusterIntegration|Zone"
msgstr "å€åŸŸ"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
-msgstr "å­˜å– Google Kubernetes Engine"
+msgstr ""
msgid "ClusterIntegration|cluster"
msgstr "å¢é›†"
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr "說明é é¢"
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr "符åˆéœ€æ±‚"
@@ -615,10 +795,6 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "更動記錄 (commit) "
-msgid "Commit %d file"
-msgid_plural "Commit %d files"
-msgstr[0] "æ交 %d 個檔案"
-
msgid "Commit Message"
msgstr "更動訊æ¯"
@@ -700,14 +876,23 @@ msgstr "å”作指å—"
msgid "Contributors"
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 "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
-msgstr "控制次節點 (secondary node) åŒæ­¥ LFS 和附檔的最大並行率 (concurrency)"
+msgstr ""
msgid "Control the maximum concurrency of repository backfill for this secondary node"
-msgstr "控制次節點 (secondary node) åŒæ­¥æª”案庫 (repository) 的最大並行率 (concurrency)"
+msgstr ""
msgid "Copy SSH public key to clipboard"
-msgstr "複製 SSH 金鑰到剪貼簿"
+msgstr ""
msgid "Copy URL to clipboard"
msgstr "複製網å€åˆ°å‰ªè²¼ç°¿"
@@ -727,6 +912,9 @@ msgstr "建立目錄"
msgid "Create empty bare repository"
msgstr "建立一個新的 bare repository"
+msgid "Create epic"
+msgstr ""
+
msgid "Create file"
msgstr "新增檔案"
@@ -754,6 +942,9 @@ msgstr "建立標籤"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "建立個人存å–憑證 (access token)"
+msgid "Creating epic"
+msgstr ""
+
msgid "Cron Timezone"
msgstr "Cron 時å€"
@@ -799,6 +990,12 @@ msgstr "全部"
msgid "DashboardProjects|Personal"
msgstr "個人"
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 語法自訂排程"
@@ -816,7 +1013,7 @@ msgid "Description"
msgstr "æè¿°"
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
-msgstr "æ述範本 (Description templates) 讓你在建立專案的議題 (Issue) å’Œåˆä½µè«‹æ±‚時å¯ä»¥é¸æ“‡ç‰¹å®šçš„範本。"
+msgstr ""
msgid "Details"
msgstr "細節"
@@ -872,6 +1069,72 @@ msgstr "編輯 %{id} æµæ°´ç·š (pipeline) 排程"
msgid "Emails"
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|Environments are places where code gets deployed, such as staging or production."
+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 "Epic will be removed! Are you sure?"
+msgstr ""
+
+msgid "Epics"
+msgstr ""
+
+msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
+msgstr ""
+
+msgid "Error creating epic"
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr "顯示全部"
@@ -911,6 +1174,12 @@ msgstr "無法變更所有權"
msgid "Failed to remove the pipeline schedule"
msgstr "無法刪除æµæ°´ç·š (pipeline) 排程"
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
msgid "File name"
msgstr "檔案å稱"
@@ -955,19 +1224,34 @@ msgid "GPG Keys"
msgstr "GPG 金鑰"
msgid "Geo Nodes"
-msgstr "Geo 節點"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Failed"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is failing or broken."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Out of sync"
+msgstr ""
+
+msgid "GeoNodeSyncStatus|Synced"
+msgstr ""
msgid "Geo|File sync capacity"
-msgstr "檔案åŒæ­¥å®¹é‡"
+msgstr ""
msgid "Geo|Groups to replicate"
-msgstr "è¦è¤‡è£½çš„群組"
+msgstr ""
msgid "Geo|Repository sync capacity"
-msgstr "檔案庫(repository)åŒæ­¥é‡"
+msgstr ""
msgid "Geo|Select groups to replicate."
-msgstr "é¸æ“‡æ¬²è¤‡è£½ä¹‹ç¾¤çµ„。"
+msgstr ""
msgid "Git storage health information has been reset"
msgstr "Git 儲存空間å¥åº·æŒ‡æ•¸å·²é‡ç½®"
@@ -1020,9 +1304,6 @@ msgstr "找ä¸åˆ°ç¾¤çµ„"
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "ä½ å¯ä»¥ç®¡ç†ç¾¤çµ„內所有æˆå“¡çš„æ¯å€‹å°ˆæ¡ˆçš„å­˜å–權é™"
-msgid "GroupsTreeRole|as"
-msgstr "çš„"
-
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr "你確定è¦é›¢é–‹ç¾¤çµ„ \"${this.group.fullName}\" 嗎?"
@@ -1053,6 +1334,9 @@ msgstr "ä¸å¥½æ„æ€ï¼Œæ²’有æœå°‹åˆ°ä»»ä½•ç¬¦åˆæ¢ä»¶çš„群組"
msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "ä¸å¥½æ„æ€ï¼Œæ²’有æœå°‹åˆ°ä»»ä½•ç¬¦åˆæ¢ä»¶çš„群組或專案"
+msgid "Have your users email"
+msgstr ""
+
msgid "Health Check"
msgstr "å¥åº·æª¢æŸ¥"
@@ -1081,20 +1365,20 @@ msgid "Import repository"
msgstr "匯入檔案庫 (repository)"
msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr "å”助改進 GitLab ä¼æ¥­ç‰ˆçš„議題看æ¿ï¼ˆissue boards)"
+msgstr ""
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr "å”助改進 GitLab ä¼æ¥­ç‰ˆçš„議題管ç†èˆ‡æ¬Šé‡ã€‚"
+msgstr ""
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
-msgstr "å”助改進 GitLab ä¼æ¥­ç‰ˆçš„æœå°‹ & 進階全局æœå°‹ã€‚"
+msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr "安è£èˆ‡ GitLab CI 相容的 Runner"
msgid "Instance"
msgid_plural "Instances"
-msgstr[0] "主機"
+msgstr[0] ""
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "內部 - 任何登入的使用者都å¯ä»¥æŸ¥çœ‹è©²ç¾¤çµ„åŠå…¶å°ˆæ¡ˆ"
@@ -1109,10 +1393,7 @@ msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
msgid "Issue board focus mode"
-msgstr "議題看æ¿ï¼ˆissue boards)模å¼"
-
-msgid "Issue boards with milestones"
-msgstr "議題看æ¿ï¼ˆissue boards)與里程碑"
+msgstr ""
msgid "Issue events"
msgstr "議題 (issue) 事件"
@@ -1121,11 +1402,29 @@ msgid "IssueBoards|Board"
msgstr "看æ¿"
msgid "IssueBoards|Boards"
-msgstr "看æ¿"
+msgstr ""
msgid "Issues"
msgstr "議題"
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -1179,7 +1478,7 @@ msgid "Leave project"
msgstr "退出專案"
msgid "License"
-msgstr "授權"
+msgstr ""
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
@@ -1192,14 +1491,23 @@ msgid "Locked"
msgstr "鎖定"
msgid "Locked Files"
-msgstr "被鎖定的檔案"
+msgstr ""
msgid "Login"
msgstr "登入"
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr "最大 git 儲存失敗"
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•¸"
@@ -1228,7 +1536,7 @@ msgid "More information is available|here"
msgstr "å¥åº·æª¢æŸ¥"
msgid "Multiple issue boards"
-msgstr "å¤šå€‹è­°é¡Œçœ‹æ¿ (issue boards)"
+msgstr ""
msgid "New Cluster"
msgstr "æ–°å¢é›†"
@@ -1243,9 +1551,15 @@ msgstr "建立æµæ°´ç·š (pipeline) 排程"
msgid "New branch"
msgstr "新分支 (branch) "
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr "新增目錄"
+msgid "New epic"
+msgstr ""
+
msgid "New file"
msgstr "新增檔案"
@@ -1282,6 +1596,9 @@ msgstr "找ä¸åˆ°æª”案庫 (repository)"
msgid "No schedules"
msgstr "沒有排程"
+msgid "No time spent"
+msgstr ""
+
msgid "None"
msgstr "ç„¡"
@@ -1348,18 +1665,33 @@ msgstr "關注"
msgid "Notifications"
msgstr "通知"
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
msgid "Number of access attempts"
msgstr "嘗試存å–的次數"
msgid "Number of failures before backing off"
msgstr ""
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
msgid "Only project members can comment."
msgstr "åªæœ‰ç¾¤çµ„æˆå“¡æ‰èƒ½ç•™è¨€ã€‚"
+msgid "Opened"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
@@ -1406,7 +1738,7 @@ msgid "Pipeline Schedules"
msgstr "æµæ°´ç·š (pipeline) 排程"
msgid "Pipeline quota"
-msgstr "æµæ°´ç·šé¡åº¦"
+msgstr ""
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -1492,6 +1824,9 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Please solve the reCAPTCHA"
+msgstr ""
+
msgid "Preferences"
msgstr "å好設定"
@@ -1595,22 +1930,28 @@ msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr "è¯çµ¡ç®¡ç†å“¡ä»¥è®Šæ›´è¨­å®šã€‚"
+msgstr ""
+
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
-msgstr "åªæœ‰å·²ç°½ç« çš„變更æ‰èƒ½è¢«æŽ¨é€åˆ°æª”案庫(repository)。"
+msgstr ""
+
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr "此設定已經套用於伺æœå™¨å±¤ç´šï¼Œä¸¦ä¸”å¯è¢«ç®¡ç†å“¡è¦†å¯«ã€‚"
+msgstr ""
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr "此設定已經套用至伺æœå™¨å±¤ç´šï¼Œä½†æ­¤å°ˆæ¡ˆä½¿ç”¨å¦ä¸€å€‹è¨­å®šã€‚"
+msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr "此設定將套用至所有專案,除éžè¢«ç®¡ç†å“¡è¦†å¯«ã€‚"
+msgstr ""
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr "使用者推é€çš„修改 (commits) åªèƒ½ä½¿ç”¨ä»–們自己的電å­éƒµä»¶ã€‚"
+msgstr ""
msgid "Projects"
msgstr "專案"
@@ -1636,6 +1977,39 @@ msgstr "抱歉,沒有符åˆæœå°‹æ¢ä»¶çš„專案"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "此功能需è¦ç€è¦½å™¨æ”¯æ´ localStorage"
+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|Finding and configuring metrics..."
+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|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "公開 - 未登入的情æ³ä¸‹ä¾ç„¶å¯ä»¥æŸ¥çœ‹ä»»ä½•å…¬é–‹å°ˆæ¡ˆ"
@@ -1643,13 +2017,13 @@ msgid "Public - The project can be accessed without any authentication."
msgstr "公開 - 無須任何身份驗證å³å¯å­˜å–該專案"
msgid "Push Rules"
-msgstr "æŽ¨é€ [Push] è¦å‰‡"
+msgstr ""
msgid "Push events"
msgstr "æŽ¨é€ (push) 事件"
msgid "PushRule|Committer restriction"
-msgstr "æ交é™åˆ¶"
+msgstr ""
msgid "Read more"
msgstr "瞭解更多"
@@ -1664,7 +2038,7 @@ msgid "RefSwitcher|Tags"
msgstr "標籤"
msgid "Registry"
-msgstr "登錄表"
+msgstr ""
msgid "Related Commits"
msgstr "相關的更動記錄 (commit) "
@@ -1732,6 +2106,9 @@ msgstr "排程"
msgid "Scheduling Pipelines"
msgstr "æµæ°´ç·š (pipeline) 排程"
+msgid "Scoped issue boards"
+msgstr ""
+
msgid "Search branches and tags"
msgstr "æœå°‹åˆ†æ”¯ (branch) 和標籤"
@@ -1753,6 +2130,12 @@ msgstr "é¸æ“‡æ™‚å€"
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯ (branch) "
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr "æœå‹™ç¯„本"
@@ -1784,14 +2167,29 @@ msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
+msgid "Sidebar|Change weight"
+msgstr ""
+
+msgid "Sidebar|Edit"
+msgstr ""
+
+msgid "Sidebar|No"
+msgstr ""
+
+msgid "Sidebar|None"
+msgstr ""
+
+msgid "Sidebar|Weight"
+msgstr ""
+
msgid "Snippets"
msgstr "文字片段"
msgid "Something went wrong on our end."
msgstr "發生了錯誤。"
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "有個地方出錯了,因為他嘗試去變更 ${this.issuableDisplayName(this.issuableType)} 的鎖定狀態。"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgstr ""
msgid "Something went wrong while fetching the projects."
msgstr "讀å–專案時發生錯誤。"
@@ -1842,7 +2240,7 @@ msgid "SortOptions|Least popular"
msgstr "最ä¸å—æ­¡è¿Ž"
msgid "SortOptions|Less weight"
-msgstr "最低權é‡"
+msgstr ""
msgid "SortOptions|Milestone"
msgstr "里程碑"
@@ -1854,7 +2252,7 @@ msgid "SortOptions|Milestone due soon"
msgstr "å³å°‡æˆªæ­¢çš„里程碑"
msgid "SortOptions|More weight"
-msgstr "更大的權é‡"
+msgstr ""
msgid "SortOptions|Most popular"
msgstr "最å—æ­¡è¿Ž"
@@ -1896,11 +2294,17 @@ msgid "SortOptions|Start soon"
msgstr "ç¾åœ¨é–‹å§‹"
msgid "SortOptions|Weight"
-msgstr "權é‡"
+msgstr ""
+
+msgid "Source"
+msgstr ""
msgid "Source code"
msgstr "原始碼"
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr "垃圾訊æ¯è¨˜éŒ„"
@@ -1919,6 +2323,9 @@ msgstr "以這些改動建立一個新的 %{new_merge_request} "
msgid "Start the Runner!"
msgstr "å•Ÿå‹• Runner!"
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr "å­ç¾¤çµ„"
@@ -1938,6 +2345,75 @@ msgstr[0] "標籤"
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 "目標分支 (branch) "
@@ -1945,10 +2421,10 @@ msgid "Team"
msgstr "團隊"
msgid "Thanks! Don't show me this again"
-msgstr "æ„Ÿè¬ï¼è«‹ä¸è¦å†æ¬¡é¡¯ç¤º"
+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 的進階全局æœå°‹åŠŸèƒ½æ˜¯éžå¸¸å¼·å¤§çš„æœå°‹æœå‹™ã€‚您å¯ä»¥æœå°‹å…¶ä»–團隊的代碼以幫助您完善自己項目中的代碼。從而é¿å…建立é‡è¤‡çš„代碼浪費時間。"
+msgstr ""
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr "é™æµé˜»æ–·å…ƒä»¶çš„觸發門檻應低於計數錯誤門檻"
@@ -2019,6 +2495,9 @@ msgstr "中ä½æ•¸æ˜¯ä¸€å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "å­˜å– Git 儲存空間時出ç¾å•é¡Œï¼š"
+msgid "This board\\'s scope is reduced"
+msgstr ""
+
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "在您編輯後,此分支已被更改,您想è¦å»ºç«‹ä¸€å€‹æ–°çš„分支嗎?"
@@ -2040,6 +2519,9 @@ msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一å
msgid "This merge request is locked."
msgstr "這個åˆä½µè«‹æ±‚已被鎖定。"
+msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "議題 (issue) 被列入日程表的時間"
@@ -2186,14 +2668,26 @@ msgstr[0] "分é˜"
msgid "Time|s"
msgstr "秒"
+msgid "Title"
+msgstr ""
+
msgid "Total Time"
msgstr "總時間"
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr "åˆä½µ (merge) 與更動記錄 (commit) 的總測試時間"
msgid "Track activity with Contribution Analytics."
-msgstr "追蹤分æžè²¢ç»èˆ‡æ´»å‹•ã€‚"
+msgstr ""
+
+msgid "Track groups of issues that share a theme, across projects and milestones"
+msgstr ""
+
+msgid "Turn on Service Desk"
+msgstr ""
msgid "Unlock"
msgstr "解鎖"
@@ -2208,19 +2702,19 @@ msgid "Unsubscribe"
msgstr "å–消訂閱"
msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr "å‡ç´šæ‚¨çš„方案以啟用進階全局æœå°‹ã€‚"
+msgstr ""
msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr "å‡ç´šæ‚¨çš„方案以啟用貢ç»åˆ†æžã€‚"
+msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr "å‡ç´šæ‚¨çš„方案以啟用群組 Webhooks。"
+msgstr ""
msgid "Upgrade your plan to activate Issue weight."
-msgstr "å‡ç´šæ‚¨çš„方案以啟用å•é¡Œæ¬Šé‡ã€‚"
+msgstr ""
msgid "Upgrade your plan to improve Issue boards."
-msgstr "å‡ç´šæ‚¨çš„方案以使用議題看æ¿ï¼ˆissue boards)"
+msgstr ""
msgid "Upload New File"
msgstr "上傳新檔案"
@@ -2231,6 +2725,9 @@ msgstr "上傳檔案"
msgid "UploadLink|click to upload"
msgstr "點擊上傳"
+msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨æ­¤è¨»å†Šæ†‘è­‰ (registration token):"
@@ -2264,11 +2761,14 @@ 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 "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
msgid "Weight"
-msgstr "權é‡"
+msgstr ""
msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
msgstr "當存å–檔案庫 (repository) 失敗時, GitLab 將在此處指定的時間內防止檔案庫的存å–,以此等待檔案系統æ¢å¾©ã€‚å¤±æ•—çš„æª”æ¡ˆåº«åˆ†æµ (shard) 會暫時無法使用。"
@@ -2376,7 +2876,7 @@ 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 ""
msgid "Withdraw Access Request"
msgstr "å–消權é™ç”³è«‹"
@@ -2393,17 +2893,11 @@ msgstr "å°‡è¦åˆªé™¤æœ¬åˆ†æ”¯å°ˆæ¡ˆèˆ‡ä¸»å¹¹ %{forked_from_project} 的所有關
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å°‡è¦æŠŠ %{project_name_with_namespace} 的所有權轉移給å¦ä¸€å€‹äººã€‚真的「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
-msgid "You are on a read-only GitLab instance."
-msgstr "您在唯讀的 GitLab 主機上。"
-
-msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
-msgstr "您在唯讀的 GitLab 主機上,如果您想è¦é€²è¡Œä¿®æ”¹ï¼Œå¿…須到 %{link_to_primary_node}"
-
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ (branch) 上建立檔案"
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}。"
+msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr "您ä¸èƒ½ä¿®æ”¹é€™å€‹å”¯è®€çš„ GitLab 主機。"
@@ -2438,6 +2932,9 @@ msgstr "在帳號上 %{set_password_link} 之å‰ï¼Œ 將無法使用 %{protocol}
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "在個人帳號中 %{add_ssh_key_link} 之å‰ï¼Œ 將無法使用 SSH 上傳 (push) 或下載 (pull) 程å¼ç¢¼ã€‚"
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr "你的留言將ä¸æœƒè¢«å…¬é–‹ã€‚"
@@ -2450,8 +2947,14 @@ msgstr "您的åå­—"
msgid "Your projects"
msgstr "你的計劃"
+msgid "branch name"
+msgstr ""
+
+msgid "by"
+msgstr ""
+
msgid "commit"
-msgstr "æ›´å‹•"
+msgstr ""
msgid "day"
msgid_plural "days"
@@ -2473,8 +2976,11 @@ msgstr "密碼"
msgid "personal access token"
msgstr "ç§äººå­˜å–憑證 (access token)"
+msgid "source"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
-msgstr "幫助你的貢ç»è€…進行有效的æºé€šï¼"
+msgstr ""
msgid "username"
msgstr "使用者å稱"
diff --git a/package.json b/package.json
index c0a4db122bd..1f83f62e7f7 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,14 @@
"core-js": "^2.4.1",
"cropper": "^2.3.0",
"css-loader": "^0.28.0",
- "d3": "^3.5.11",
+ "d3-array": "^1.2.1",
+ "d3-axis": "^1.0.8",
+ "d3-brush": "^1.0.4",
+ "d3-scale": "^1.0.7",
+ "d3-selection": "^1.2.0",
+ "d3-shape": "^1.2.0",
+ "d3-time": "^1.0.8",
+ "d3-time-format": "^2.1.1",
"deckar01-task_list": "^2.0.0",
"diff": "^3.4.0",
"document-register-element": "1.3.0",
@@ -72,6 +79,7 @@
"vue": "^2.5.8",
"vue-loader": "^13.5.0",
"vue-resource": "^1.3.4",
+ "vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.8",
"vuex": "^3.0.1",
"webpack": "^3.5.5",
@@ -80,7 +88,7 @@
"worker-loader": "^1.1.0"
},
"devDependencies": {
- "@gitlab-org/gitlab-svgs": "^1.1.1",
+ "@gitlab-org/gitlab-svgs": "^1.5.0",
"babel-plugin-istanbul": "^4.1.5",
"eslint": "^3.10.1",
"eslint-config-airbnb-base": "^10.0.1",
@@ -100,6 +108,7 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.4",
"nodemon": "^1.11.0",
+ "prettier": "1.9.2",
"webpack-dev-server": "^2.6.1"
}
}
diff --git a/qa/Dockerfile b/qa/Dockerfile
index 9b6ffff7c4d..ed2ee73bea0 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:2.3
+FROM ruby:2.4
LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
ENV DEBIAN_FRONTEND noninteractive
diff --git a/qa/Gemfile b/qa/Gemfile
index ff29824529f..4c866a3f893 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -1,8 +1,8 @@
source 'https://rubygems.org'
-gem 'pry-byebug', '~> 3.4.1', platform: :mri
-gem 'capybara', '~> 2.12.1'
-gem 'capybara-screenshot', '~> 1.0.14'
-gem 'rake', '~> 12.0.0'
-gem 'rspec', '~> 3.5'
-gem 'selenium-webdriver', '~> 2.53'
+gem 'pry-byebug', '~> 3.5.1', platform: :mri
+gem 'capybara', '~> 2.16.1'
+gem 'capybara-screenshot', '~> 1.0.18'
+gem 'rake', '~> 12.3.0'
+gem 'rspec', '~> 3.7'
+gem 'selenium-webdriver', '~> 3.8.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 22d12b479cb..88d5fe834a0 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -1,78 +1,72 @@
GEM
remote: https://rubygems.org/
specs:
- addressable (2.5.0)
- public_suffix (~> 2.0, >= 2.0.2)
- byebug (9.0.6)
- capybara (2.12.1)
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
+ byebug (9.1.0)
+ capybara (2.16.1)
addressable
- mime-types (>= 1.16)
+ mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- capybara-screenshot (1.0.14)
+ capybara-screenshot (1.0.18)
capybara (>= 1.0, < 3)
launchy
- childprocess (0.7.0)
+ childprocess (0.8.0)
ffi (~> 1.0, >= 1.0.11)
- coderay (1.1.1)
+ coderay (1.1.2)
diff-lcs (1.3)
ffi (1.9.18)
launchy (2.4.3)
addressable (~> 2.3)
- method_source (0.8.2)
- mime-types (3.1)
- mime-types-data (~> 3.2015)
- mime-types-data (3.2016.0521)
+ method_source (0.9.0)
+ mini_mime (1.0.0)
mini_portile2 (2.3.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
- pry (0.10.4)
+ pry (0.11.3)
coderay (~> 1.1.0)
- method_source (~> 0.8.1)
- slop (~> 3.4)
- pry-byebug (3.4.2)
- byebug (~> 9.0)
+ method_source (~> 0.9.0)
+ pry-byebug (3.5.1)
+ byebug (~> 9.1)
pry (~> 0.10)
- public_suffix (2.0.5)
- rack (2.0.1)
- rack-test (0.6.3)
- rack (>= 1.0)
- rake (12.0.0)
- rspec (3.5.0)
- rspec-core (~> 3.5.0)
- rspec-expectations (~> 3.5.0)
- rspec-mocks (~> 3.5.0)
- rspec-core (3.5.4)
- rspec-support (~> 3.5.0)
- rspec-expectations (3.5.0)
+ public_suffix (3.0.1)
+ rack (2.0.3)
+ rack-test (0.8.2)
+ rack (>= 1.0, < 3)
+ rake (12.3.0)
+ rspec (3.7.0)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-core (3.7.0)
+ rspec-support (~> 3.7.0)
+ rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.5.0)
- rspec-mocks (3.5.0)
+ rspec-support (~> 3.7.0)
+ rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.5.0)
- rspec-support (3.5.0)
+ rspec-support (~> 3.7.0)
+ rspec-support (3.7.0)
rubyzip (1.2.1)
- selenium-webdriver (2.53.4)
+ selenium-webdriver (3.8.0)
childprocess (~> 0.5)
rubyzip (~> 1.0)
- websocket (~> 1.0)
- slop (3.6.0)
- websocket (1.2.4)
- xpath (2.0.0)
+ xpath (2.1.0)
nokogiri (~> 1.3)
PLATFORMS
ruby
DEPENDENCIES
- capybara (~> 2.12.1)
- capybara-screenshot (~> 1.0.14)
- pry-byebug (~> 3.4.1)
- rake (~> 12.0.0)
- rspec (~> 3.5)
- selenium-webdriver (~> 2.53)
+ capybara (~> 2.16.1)
+ capybara-screenshot (~> 1.0.18)
+ pry-byebug (~> 3.5.1)
+ rake (~> 12.3.0)
+ rspec (~> 3.7)
+ selenium-webdriver (~> 3.8.0)
BUNDLED WITH
- 1.15.4
+ 1.16.0
diff --git a/qa/README.md b/qa/README.md
index 1cfbbdd9d42..7f2dd39ff63 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -33,7 +33,14 @@ You can also supply specific tests to run as another parameter. For example, to
test the EE license specs, you can run:
```
-EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/ee
+EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/specs/features/ee
+```
+
+Since the arguments would be passed to `rspec`, you could use all `rspec`
+options there. For example, passing `--backtrace` and also line number:
+
+```
+bin/qa Test::Instance http://localhost qa/specs/features/login/standard_spec.rb:3 --backtrace
```
### Overriding the authenticated user
diff --git a/qa/qa.rb b/qa/qa.rb
index 06b6a76489b..71b80a6adcb 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -9,6 +9,32 @@ module QA
autoload :User, 'qa/runtime/user'
autoload :Namespace, 'qa/runtime/namespace'
autoload :Scenario, 'qa/runtime/scenario'
+ autoload :Browser, 'qa/runtime/browser'
+ autoload :Env, 'qa/runtime/env'
+ end
+
+ ##
+ # GitLab QA fabrication mechanisms
+ #
+ module Factory
+ autoload :Base, 'qa/factory/base'
+ autoload :Dependency, 'qa/factory/dependency'
+ autoload :Product, 'qa/factory/product'
+
+ module Resource
+ autoload :Sandbox, 'qa/factory/resource/sandbox'
+ autoload :Group, 'qa/factory/resource/group'
+ autoload :Project, 'qa/factory/resource/project'
+ autoload :DeployKey, 'qa/factory/resource/deploy_key'
+ end
+
+ module Repository
+ autoload :Push, 'qa/factory/repository/push'
+ end
+
+ module Settings
+ autoload :HashedStorage, 'qa/factory/settings/hashed_storage'
+ end
end
##
@@ -33,27 +59,6 @@ module QA
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
end
end
-
- ##
- # GitLab instance scenarios.
- #
- module Gitlab
- module Group
- autoload :Create, 'qa/scenario/gitlab/group/create'
- end
-
- module Project
- autoload :Create, 'qa/scenario/gitlab/project/create'
- end
-
- module Sandbox
- autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare'
- end
-
- module Admin
- autoload :HashedStorage, 'qa/scenario/gitlab/admin/hashed_storage'
- end
- end
end
##
@@ -65,12 +70,16 @@ module QA
autoload :Base, 'qa/page/base'
module Main
- autoload :Entry, 'qa/page/main/entry'
autoload :Login, 'qa/page/main/login'
- autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth'
end
+ module Menu
+ autoload :Main, 'qa/page/menu/main'
+ autoload :Side, 'qa/page/menu/side'
+ autoload :Admin, 'qa/page/menu/admin'
+ end
+
module Dashboard
autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups'
@@ -84,10 +93,15 @@ module QA
module Project
autoload :New, 'qa/page/project/new'
autoload :Show, 'qa/page/project/show'
+
+ module Settings
+ autoload :Common, 'qa/page/project/settings/common'
+ autoload :Repository, 'qa/page/project/settings/repository'
+ autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
+ end
end
module Admin
- autoload :Menu, 'qa/page/admin/menu'
autoload :Settings, 'qa/page/admin/settings'
end
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
new file mode 100644
index 00000000000..00851a7bece
--- /dev/null
+++ b/qa/qa/factory/base.rb
@@ -0,0 +1,35 @@
+module QA
+ module Factory
+ class Base
+ def fabricate!(*_args)
+ raise NotImplementedError
+ end
+
+ def self.fabricate!(*args)
+ Factory::Product.populate!(new) do |factory|
+ yield factory if block_given?
+
+ dependencies.each do |name, signature|
+ Factory::Dependency.new(name, factory, signature).build!
+ end
+
+ factory.fabricate!(*args)
+ end
+ end
+
+ def self.dependencies
+ @dependencies ||= {}
+ end
+
+ def self.dependency(factory, as:, &block)
+ as.tap do |name|
+ class_eval { attr_accessor name }
+
+ Dependency::Signature.new(factory, block).tap do |signature|
+ dependencies.store(name, signature)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb
new file mode 100644
index 00000000000..d0e85a68237
--- /dev/null
+++ b/qa/qa/factory/dependency.rb
@@ -0,0 +1,38 @@
+module QA
+ module Factory
+ class Dependency
+ Signature = Struct.new(:factory, :block)
+
+ def initialize(name, factory, signature)
+ @name = name
+ @factory = factory
+ @signature = signature
+ end
+
+ def overridden?
+ !!@factory.public_send(@name)
+ end
+
+ def build!
+ return if overridden?
+
+ Builder.new(@signature).fabricate!.tap do |product|
+ @factory.public_send("#{@name}=", product)
+ end
+ end
+
+ class Builder
+ def initialize(signature)
+ @factory = signature.factory
+ @block = signature.block
+ end
+
+ def fabricate!
+ @factory.fabricate! do |factory|
+ @block&.call(factory)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
new file mode 100644
index 00000000000..df35bbbb443
--- /dev/null
+++ b/qa/qa/factory/product.rb
@@ -0,0 +1,26 @@
+require 'capybara/dsl'
+
+module QA
+ module Factory
+ class Product
+ include Capybara::DSL
+
+ def initialize(factory)
+ @factory = factory
+ @location = current_url
+ end
+
+ def visit!
+ visit @location
+ end
+
+ def self.populate!(factory)
+ raise ArgumentError unless block_given?
+
+ yield factory
+
+ new(factory)
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
new file mode 100644
index 00000000000..2f4de4173d4
--- /dev/null
+++ b/qa/qa/factory/repository/push.rb
@@ -0,0 +1,40 @@
+module QA
+ module Factory
+ module Repository
+ class Push < Factory::Base
+ attr_writer :file_name, :file_content, :commit_message, :branch_name
+
+ 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}"
+ @branch_name = 'master'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Git::Repository.perform do |repository|
+ repository.location = Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location
+ end
+
+ repository.use_default_credentials
+ repository.clone
+ repository.configure_identity('GitLab QA', 'root@gitlab.com')
+
+ repository.add_file(@file_name, @file_content)
+ repository.commit(@commit_message)
+ repository.push_changes(@branch_name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
new file mode 100644
index 00000000000..7c58e70bcc4
--- /dev/null
+++ b/qa/qa/factory/resource/deploy_key.rb
@@ -0,0 +1,31 @@
+module QA
+ module Factory
+ module Resource
+ class DeployKey < Factory::Base
+ attr_accessor :title, :key
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-to-deploy'
+ project.description = 'project for adding deploy key test'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Menu::Side.act do
+ click_repository_setting
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |page|
+ page.fill_key_title(title)
+ page.fill_key_value(key)
+
+ page.add_key
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
new file mode 100644
index 00000000000..9f13e26f35c
--- /dev/null
+++ b/qa/qa/factory/resource/group.rb
@@ -0,0 +1,35 @@
+module QA
+ module Factory
+ module Resource
+ class Group < Factory::Base
+ attr_writer :path, :description
+
+ dependency Factory::Resource::Sandbox, as: :sandbox
+
+ def initialize
+ @path = Runtime::Namespace.name
+ @description = "QA test run at #{Runtime::Namespace.time}"
+ end
+
+ def fabricate!
+ sandbox.visit!
+
+ Page::Group::Show.perform do |page|
+ if page.has_subgroup?(@path)
+ page.go_to_subgroup(@path)
+ else
+ page.go_to_new_subgroup
+
+ Page::Group::New.perform do |group|
+ group.set_path(@path)
+ group.set_description(@description)
+ group.set_visibility('Private')
+ group.create
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
new file mode 100644
index 00000000000..07c2e3086d1
--- /dev/null
+++ b/qa/qa/factory/resource/project.rb
@@ -0,0 +1,31 @@
+require 'securerandom'
+
+module QA
+ module Factory
+ module Resource
+ class Project < Factory::Base
+ attr_writer :description
+
+ dependency Factory::Resource::Group, as: :group
+
+ def name=(name)
+ @name = "#{name}-#{SecureRandom.hex(8)}"
+ @description = 'My awesome project'
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.act { go_to_new_project }
+
+ Page::Project::New.perform do |page|
+ page.choose_test_namespace
+ page.choose_name(@name)
+ page.add_description(@description)
+ page.create_new_project
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
new file mode 100644
index 00000000000..ad376988e82
--- /dev/null
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -0,0 +1,34 @@
+module QA
+ module Factory
+ module Resource
+ ##
+ # Ensure we're in our sandbox namespace, either by navigating to it or by
+ # creating it if it doesn't yet exist.
+ #
+ class Sandbox < Factory::Base
+ def initialize
+ @name = Runtime::Namespace.sandbox_name
+ end
+
+ def fabricate!
+ Page::Menu::Main.act { go_to_groups }
+
+ Page::Dashboard::Groups.perform do |page|
+ if page.has_group?(@name)
+ page.go_to_group(@name)
+ else
+ page.go_to_new_group
+
+ Page::Group::New.perform do |group|
+ group.set_path(@name)
+ group.set_description('GitLab QA Sandbox')
+ group.set_visibility('Private')
+ group.create
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb
new file mode 100644
index 00000000000..13ce2435fe4
--- /dev/null
+++ b/qa/qa/factory/settings/hashed_storage.rb
@@ -0,0 +1,22 @@
+module QA
+ module Factory
+ module Settings
+ class HashedStorage < Factory::Base
+ def fabricate!(*traits)
+ raise ArgumentError unless traits.include?(:enabled)
+
+ Page::Main::Login.act { sign_in_using_credentials }
+ Page::Menu::Main.act { go_to_admin_area }
+ Page::Menu::Admin.act { go_to_settings }
+
+ Page::Admin::Settings.act do
+ enable_hashed_storage
+ save_settings
+ end
+
+ QA::Page::Menu::Main.act { sign_out }
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 59cd147e055..8f999511d58 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -23,7 +23,7 @@ module QA
def password=(pass)
@password = pass
- @uri.password = CGI.escape(pass)
+ @uri.password = CGI.escape(pass).gsub('+', '%20')
end
def use_default_credentials
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index f9a93ef051e..99eba02b6e3 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -10,6 +10,18 @@ module QA
visit current_url
end
+ def wait(css = '.application', time: 60)
+ Time.now.tap do |start|
+ while Time.now - start < time
+ break if page.has_css?(css, wait: 5)
+
+ refresh
+ end
+ end
+
+ yield if block_given?
+ end
+
def scroll_to(selector, text: nil)
page.execute_script <<~JS
var elements = Array.from(document.querySelectorAll('#{selector}'));
@@ -24,6 +36,10 @@ module QA
page.within(selector) { yield } if block_given?
end
+
+ def self.path
+ raise NotImplementedError
+ end
end
end
end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 8080deda675..0a16c07d64b 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -6,7 +6,13 @@ module QA
click_link name
end
+ def filter_by_name(name)
+ fill_in 'Filter by name...', with: name
+ end
+
def has_subgroup?(name)
+ filter_by_name(name)
+
page.has_link?(name)
end
@@ -15,6 +21,7 @@ module QA
find('.dropdown-toggle').click
find("li[data-value='new-subgroup']").click
end
+
find("input[data-action='new-subgroup']").click
end
@@ -23,6 +30,7 @@ module QA
find('.dropdown-toggle').click
find("li[data-value='new-project']").click
end
+
find("input[data-action='new-project']").click
end
end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
deleted file mode 100644
index ae6484b4bfe..00000000000
--- a/qa/qa/page/main/entry.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module QA
- module Page
- module Main
- class Entry < Page::Base
- def visit_login_page
- visit("#{Runtime::Scenario.gitlab_address}/users/sign_in")
- wait_for_instance_to_be_ready
- end
-
- private
-
- def wait_for_instance_to_be_ready
- # This resolves cold boot / background tasks problems
- #
- start = Time.now
-
- while Time.now - start < 240
- break if page.has_css?('.application', wait: 10)
-
- refresh
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 8b0111a78a2..f88325f408b 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -2,6 +2,10 @@ module QA
module Page
module Main
class Login < Page::Base
+ def initialize
+ wait('.application', time: 500)
+ end
+
def sign_in_using_credentials
if page.has_content?('Change your password')
fill_in :user_password, with: Runtime::User.password
@@ -13,6 +17,10 @@ module QA
fill_in :user_password, with: Runtime::User.password
click_button 'Sign in'
end
+
+ def self.path
+ '/users/sign_in'
+ end
end
end
end
diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb
index 42ab9c6f675..8ffd4fdad13 100644
--- a/qa/qa/page/mattermost/login.rb
+++ b/qa/qa/page/mattermost/login.rb
@@ -2,10 +2,6 @@ module QA
module Page
module Mattermost
class Login < Page::Base
- def initialize
- visit(Runtime::Scenario.mattermost_address + '/login')
- end
-
def sign_in_using_oauth
click_link class: 'btn btn-custom-login gitlab'
@@ -13,6 +9,10 @@ module QA
click_button 'Authorize'
end
end
+
+ def self.path
+ '/login'
+ end
end
end
end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/menu/admin.rb
index dd289ffe269..07fe40fda3a 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/menu/admin.rb
@@ -1,7 +1,7 @@
module QA
module Page
- module Admin
- class Menu < Page::Base
+ module Menu
+ class Admin < Page::Base
def go_to_license
click_link 'License'
end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/menu/main.rb
index bc9c4ec1215..b94c2c6c23d 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/menu/main.rb
@@ -1,7 +1,7 @@
module QA
module Page
- module Main
- class Menu < Page::Base
+ module Menu
+ class Main < Page::Base
def go_to_groups
within_top_menu { click_link 'Groups' }
end
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
new file mode 100644
index 00000000000..6c25aba4bac
--- /dev/null
+++ b/qa/qa/page/menu/side.rb
@@ -0,0 +1,29 @@
+module QA
+ module Page
+ module Menu
+ class Side < Page::Base
+ def click_repository_setting
+ hover_setting do
+ click_link('Repository')
+ end
+ end
+
+ private
+
+ def hover_setting
+ within_sidebar do
+ find('.nav-item-name', text: 'Settings').hover
+
+ yield
+ end
+ end
+
+ def within_sidebar
+ page.within('.sidebar-top-level-items') do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb
new file mode 100644
index 00000000000..5d1d5120929
--- /dev/null
+++ b/qa/qa/page/project/settings/common.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ module Common
+ def expand(selector)
+ page.within('#content-body') do
+ find(selector).click
+
+ yield
+ end
+ end
+ 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
new file mode 100644
index 00000000000..4028b8cccc5
--- /dev/null
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -0,0 +1,27 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class DeployKeys < Page::Base
+ def fill_key_title(title)
+ fill_in 'deploy_key_title', with: title
+ end
+
+ def fill_key_value(key)
+ fill_in 'deploy_key_key', with: key
+ end
+
+ def add_key
+ click_on 'Add key'
+ end
+
+ def has_key_title?(title)
+ page.within('.deploy-keys') do
+ page.find('.title', text: title)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
new file mode 100644
index 00000000000..034b0d09c1c
--- /dev/null
+++ b/qa/qa/page/project/settings/repository.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class Repository < Page::Base
+ include Common
+
+ def expand_deploy_keys(&block)
+ expand('.qa-expand-deploy-keys') do
+ DeployKeys.perform(&block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
new file mode 100644
index 00000000000..14b2a488760
--- /dev/null
+++ b/qa/qa/runtime/browser.rb
@@ -0,0 +1,139 @@
+require 'rspec/core'
+require 'capybara/rspec'
+require 'capybara-screenshot/rspec'
+require 'selenium-webdriver'
+
+module QA
+ module Runtime
+ class Browser
+ include QA::Scenario::Actable
+
+ def initialize
+ self.class.configure!
+ end
+
+ ##
+ # Visit a page that belongs to a GitLab instance under given address.
+ #
+ # Example:
+ #
+ # visit(:gitlab, Page::Main::Login)
+ # visit('http://gitlab.example/users/sign_in')
+ #
+ # In case of an address that is a symbol we will try to guess address
+ # based on `Runtime::Scenario#something_address`.
+ #
+ def visit(address, page, &block)
+ Browser::Session.new(address, page).tap do |session|
+ session.perform(&block)
+ end
+ end
+
+ def self.visit(address, page, &block)
+ new.visit(address, page, &block)
+ end
+
+ def self.configure!
+ return if Capybara.drivers.include?(:chrome)
+
+ Capybara.register_driver :chrome do |app|
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ # This enables access to logs with `page.driver.manage.get_log(:browser)`
+ loggingPrefs: {
+ browser: "ALL",
+ client: "ALL",
+ driver: "ALL",
+ server: "ALL"
+ }
+ )
+
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.add_argument("window-size=1240,1680")
+
+ # Chrome won't work properly in a Docker container in sandbox mode
+ options.add_argument("no-sandbox")
+
+ # Run headless by default unless CHROME_HEADLESS is false
+ if QA::Runtime::Env.chrome_headless?
+ options.add_argument("headless")
+
+ # Chrome documentation says this flag is needed for now
+ # https://developers.google.com/web/updates/2017/04/headless-chrome#cli
+ options.add_argument("disable-gpu")
+ end
+
+ # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
+ options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci?
+
+ Capybara::Selenium::Driver.new(
+ app,
+ browser: :chrome,
+ desired_capabilities: capabilities,
+ options: options
+ )
+ end
+
+ # Keep only the screenshots generated from the last failing test suite
+ Capybara::Screenshot.prune_strategy = :keep_last_run
+
+ # From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
+ Capybara::Screenshot.register_driver(:chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+ end
+
+ Capybara.configure do |config|
+ config.default_driver = :chrome
+ config.javascript_driver = :chrome
+ config.default_max_wait_time = 10
+ # https://github.com/mattheworiordan/capybara-screenshot/issues/164
+ config.save_path = 'tmp'
+ end
+ end
+
+ class Session
+ include Capybara::DSL
+
+ def initialize(instance, page = nil)
+ @instance = instance
+ @address = host + page&.path
+ end
+
+ def host
+ if @instance.is_a?(Symbol)
+ Runtime::Scenario.send("#{@instance}_address")
+ else
+ @instance.to_s
+ end
+ end
+
+ def perform(&block)
+ visit(@address)
+
+ 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?
+ end
+
+ ##
+ # Selenium allows to reset session cookies for current domain only.
+ #
+ # See gitlab-org/gitlab-qa#102
+ #
+ def clear!
+ visit(@address)
+ reset_session!
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
new file mode 100644
index 00000000000..d5c28e9a7db
--- /dev/null
+++ b/qa/qa/runtime/env.rb
@@ -0,0 +1,15 @@
+module QA
+ module Runtime
+ module Env
+ extend self
+
+ def chrome_headless?
+ (ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
+ end
+
+ def running_in_ci?
+ ENV['CI'] || ENV['CI_SERVER']
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb
index 7ef59046640..15d4112d347 100644
--- a/qa/qa/runtime/scenario.rb
+++ b/qa/qa/runtime/scenario.rb
@@ -6,13 +6,15 @@ module QA
module Scenario
extend self
- attr_reader :attributes
+ def attributes
+ @attributes ||= {}
+ end
def define(attribute, value)
- (@attributes ||= {}).store(attribute.to_sym, value)
+ attributes.store(attribute.to_sym, value)
define_singleton_method(attribute) do
- @attributes[attribute.to_sym].tap do |value|
+ attributes[attribute.to_sym].tap do |value|
if value.to_s.empty?
raise ArgumentError, "Empty `#{attribute}` attribute!"
end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
index 60027c89ab1..2832439d9e0 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -10,6 +10,17 @@ module QA
def password
ENV['GITLAB_PASSWORD'] || '5iveL!fe'
end
+
+ def ssh_key
+ <<~KEY.delete("\n")
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
+ 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
+ /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
+ M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
+ rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
+ 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
+ KEY
+ end
end
end
end
diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb
index b9d924651a0..ae099fd911e 100644
--- a/qa/qa/scenario/entrypoint.rb
+++ b/qa/qa/scenario/entrypoint.rb
@@ -8,7 +8,6 @@ module QA
include Bootable
def perform(address, *files)
- Specs::Config.act { configure_capybara! }
Runtime::Scenario.define(:gitlab_address, address)
##
diff --git a/qa/qa/scenario/gitlab/admin/hashed_storage.rb b/qa/qa/scenario/gitlab/admin/hashed_storage.rb
deleted file mode 100644
index ac2cd549829..00000000000
--- a/qa/qa/scenario/gitlab/admin/hashed_storage.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module QA
- module Scenario
- module Gitlab
- module Admin
- class HashedStorage < Scenario::Template
- def perform(*traits)
- raise ArgumentError unless traits.include?(:enabled)
-
- Page::Main::Entry.act { visit_login_page }
- Page::Main::Login.act { sign_in_using_credentials }
- Page::Main::Menu.act { go_to_admin_area }
- Page::Admin::Menu.act { go_to_settings }
-
- Page::Admin::Settings.act do
- enable_hashed_storage
- save_settings
- end
-
- QA::Page::Main::Menu.act { sign_out }
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/scenario/gitlab/group/create.rb b/qa/qa/scenario/gitlab/group/create.rb
deleted file mode 100644
index 8e6c7c7ad80..00000000000
--- a/qa/qa/scenario/gitlab/group/create.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'securerandom'
-
-module QA
- module Scenario
- module Gitlab
- module Group
- class Create < Scenario::Template
- attr_writer :path, :description
-
- def initialize
- @path = Runtime::Namespace.name
- @description = "QA test run at #{Runtime::Namespace.time}"
- end
-
- def perform
- Page::Group::New.perform do |group|
- group.set_path(@path)
- group.set_description(@description)
- group.set_visibility('Private')
- group.create
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
deleted file mode 100644
index bb3b9e19c0f..00000000000
--- a/qa/qa/scenario/gitlab/project/create.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'securerandom'
-
-module QA
- module Scenario
- module Gitlab
- module Project
- class Create < Scenario::Template
- attr_writer :description
-
- def name=(name)
- @name = "#{name}-#{SecureRandom.hex(8)}"
- end
-
- def perform
- Scenario::Gitlab::Sandbox::Prepare.perform
-
- Page::Group::Show.perform do |page|
- if page.has_subgroup?(Runtime::Namespace.name)
- page.go_to_subgroup(Runtime::Namespace.name)
- else
- page.go_to_new_subgroup
-
- Scenario::Gitlab::Group::Create.perform do |group|
- group.path = Runtime::Namespace.name
- end
- end
-
- page.go_to_new_project
- end
-
- Page::Project::New.perform do |page|
- page.choose_test_namespace
- page.choose_name(@name)
- page.add_description(@description)
- page.create_new_project
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/scenario/gitlab/sandbox/prepare.rb b/qa/qa/scenario/gitlab/sandbox/prepare.rb
deleted file mode 100644
index 990de456e20..00000000000
--- a/qa/qa/scenario/gitlab/sandbox/prepare.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module QA
- module Scenario
- module Gitlab
- module Sandbox
- # Ensure we're in our sandbox namespace, either by navigating to it or
- # by creating it if it doesn't yet exist
- class Prepare < Scenario::Template
- def perform
- Page::Main::Menu.act { go_to_groups }
-
- Page::Dashboard::Groups.perform do |page|
- if page.has_group?(Runtime::Namespace.sandbox_name)
- page.go_to_group(Runtime::Namespace.sandbox_name)
- else
- page.go_to_new_group
-
- Scenario::Gitlab::Group::Create.perform do |group|
- group.path = Runtime::Namespace.sandbox_name
- group.description = 'QA sandbox'
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
deleted file mode 100644
index bce7923e52d..00000000000
--- a/qa/qa/specs/config.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'rspec/core'
-require 'capybara/rspec'
-require 'capybara-screenshot/rspec'
-require 'selenium-webdriver'
-
-# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/LineLength
-
-module QA
- module Specs
- class Config < Scenario::Template
- include Scenario::Actable
-
- def perform
- configure_rspec!
- configure_capybara!
- end
-
- def configure_rspec!
- RSpec.configure do |config|
- config.expect_with :rspec do |expectations|
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
- end
-
- config.mock_with :rspec do |mocks|
- mocks.verify_partial_doubles = true
- end
-
- config.order = :random
- Kernel.srand config.seed
- config.formatter = :documentation
- config.color = true
- end
- end
-
- def configure_capybara!
- Capybara.register_driver :chrome do |app|
- capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
- 'chromeOptions' => {
- 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
- }
- )
-
- Capybara::Selenium::Driver
- .new(app, browser: :chrome, desired_capabilities: capabilities)
- end
-
- Capybara::Screenshot.register_driver(:chrome) do |driver, path|
- driver.browser.save_screenshot(path)
- end
-
- Capybara.configure do |config|
- config.default_driver = :chrome
- config.javascript_driver = :chrome
- config.default_max_wait_time = 10
-
- # https://github.com/mattheworiordan/capybara-screenshot/issues/164
- config.save_path = 'tmp'
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
index b155708c387..141ffa3cfb7 100644
--- a/qa/qa/specs/features/login/standard_spec.rb
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -1,13 +1,13 @@
module QA
- feature 'standard root login', :core do
+ feature 'standard user login', :core do
scenario 'user logs in using credentials' do
- Page::Main::Entry.act { visit_login_page }
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
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.
#
- Page::Main::Menu.perform do |menu|
+ Page::Menu::Main.perform do |menu|
expect(menu).to have_personal_area
end
end
diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb
index 853a9a6a4f4..2e27a285223 100644
--- a/qa/qa/specs/features/mattermost/group_create_spec.rb
+++ b/qa/qa/specs/features/mattermost/group_create_spec.rb
@@ -1,9 +1,9 @@
module QA
feature 'create a new group', :mattermost do
scenario 'creating a group with a mattermost team' do
- Page::Main::Entry.act { visit_login_page }
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Page::Main::Menu.act { go_to_groups }
+ Page::Menu::Main.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
page.go_to_new_group
diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb
index 1fde3f89a99..637bbdd643a 100644
--- a/qa/qa/specs/features/mattermost/login_spec.rb
+++ b/qa/qa/specs/features/mattermost/login_spec.rb
@@ -1,24 +1,17 @@
module QA
feature 'logging in to Mattermost', :mattermost do
scenario 'can use gitlab oauth' do
- Page::Main::Entry.act { visit_login_page }
- Page::Main::Login.act { sign_in_using_credentials }
- Page::Mattermost::Login.act { sign_in_using_oauth }
+ Runtime::Browser.visit(:gitlab, Page::Main::Login) do
+ Page::Main::Login.act { sign_in_using_credentials }
- Page::Mattermost::Main.perform do |page|
- expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
- end
- end
+ Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do
+ Page::Mattermost::Login.act { sign_in_using_oauth }
- ##
- # TODO, temporary workaround for gitlab-org/gitlab-qa#102.
- #
- after do
- visit Runtime::Scenario.mattermost_address
- reset_session!
-
- visit Runtime::Scenario.gitlab_address
- reset_session!
+ Page::Mattermost::Main.perform do |page|
+ expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
+ end
+ end
+ end
end
end
end
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
new file mode 100644
index 00000000000..43a85213501
--- /dev/null
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -0,0 +1,22 @@
+module QA
+ feature 'deploy keys support', :core do
+ given(:deploy_key_title) { 'deploy key title' }
+ given(:deploy_key_value) { Runtime::User.ssh_key }
+
+ scenario 'user adds a deploy key' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Factory::Resource::DeployKey.fabricate! do |deploy_key|
+ deploy_key.title = deploy_key_title
+ deploy_key.key = deploy_key_value
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |page|
+ expect(page).to have_key_title(deploy_key_title)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
index aba0c2b4c14..61c19378ae0 100644
--- a/qa/qa/specs/features/project/create_spec.rb
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -1,10 +1,10 @@
module QA
feature 'create a new project', :core do
scenario 'user creates a new project' do
- Page::Main::Entry.act { visit_login_page }
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Scenario::Gitlab::Project::Create.perform do |project|
+ Factory::Resource::Project.fabricate! do |project|
project.name = 'awesome-project'
project.description = 'create awesome project test'
end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index 5cc3b3b9c1b..2adb7524a46 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -9,10 +9,10 @@ module QA
end
before do
- Page::Main::Entry.act { visit_login_page }
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Scenario::Gitlab::Project::Create.perform do |scenario|
+ Factory::Resource::Project.fabricate! do |scenario|
scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests'
end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index 30935dc1e13..4f6ffe14c9f 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -2,29 +2,13 @@ module QA
feature 'push code to repository', :core do
context 'with regular account over http' do
scenario 'user pushes code to the repository' do
- Page::Main::Entry.act { visit_login_page }
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Scenario::Gitlab::Project::Create.perform do |scenario|
- scenario.name = 'project_with_code'
- scenario.description = 'project with repository'
- end
-
- Git::Repository.perform do |repository|
- repository.location = Page::Project::Show.act do
- choose_repository_clone_http
- repository_location
- end
-
- repository.use_default_credentials
-
- repository.act do
- clone
- configure_identity('GitLab QA', 'root@gitlab.com')
- add_file('README.md', '# This is test project')
- commit('Add README.md')
- push_changes
- end
+ Factory::Repository::Push.fabricate! do |push|
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
end
Page::Project::Show.act do
@@ -33,7 +17,7 @@ module QA
end
expect(page).to have_content('README.md')
- expect(page).to have_content('This is test project')
+ expect(page).to have_content('This is a test project')
end
end
end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index f98b8f88e9a..3f7b75df986 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -17,7 +17,7 @@ module QA
tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
args.push(files)
- Specs::Config.perform
+ Runtime::Browser.configure!
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
new file mode 100644
index 00000000000..a3ba0176819
--- /dev/null
+++ b/qa/spec/factory/base_spec.rb
@@ -0,0 +1,88 @@
+describe QA::Factory::Base do
+ describe '.fabricate!' do
+ subject { Class.new(described_class) }
+ let(:factory) { spy('factory') }
+ let(:product) { spy('product') }
+
+ before do
+ allow(QA::Factory::Product).to receive(:new).and_return(product)
+ end
+
+ it 'instantiates the factory and calls factory method' do
+ expect(subject).to receive(:new).and_return(factory)
+
+ subject.fabricate!('something')
+
+ expect(factory).to have_received(:fabricate!).with('something')
+ end
+
+ it 'returns fabrication product' do
+ allow(subject).to receive(:new).and_return(factory)
+ allow(factory).to receive(:fabricate!).and_return('something')
+
+ result = subject.fabricate!('something')
+
+ expect(result).to eq product
+ end
+
+ it 'yields factory before calling factory method' do
+ allow(subject).to receive(:new).and_return(factory)
+
+ subject.fabricate! do |factory|
+ factory.something!
+ end
+
+ expect(factory).to have_received(:something!).ordered
+ expect(factory).to have_received(:fabricate!).ordered
+ end
+ end
+
+ describe '.dependency' do
+ let(:dependency) { spy('dependency') }
+
+ before do
+ stub_const('Some::MyDependency', dependency)
+ end
+
+ subject do
+ Class.new(described_class) do
+ dependency Some::MyDependency, as: :mydep do |factory|
+ factory.something!
+ end
+ end
+ end
+
+ it 'appends a new dependency and accessors' do
+ expect(subject.dependencies).to be_one
+ end
+
+ it 'defines dependency accessors' do
+ expect(subject.new).to respond_to :mydep, :mydep=
+ end
+ end
+
+ describe 'building dependencies' do
+ let(:dependency) { double('dependency') }
+ let(:instance) { spy('instance') }
+
+ subject do
+ Class.new(described_class) do
+ dependency Some::MyDependency, as: :mydep
+ end
+ end
+
+ before do
+ stub_const('Some::MyDependency', dependency)
+
+ allow(subject).to receive(:new).and_return(instance)
+ allow(instance).to receive(:mydep).and_return(nil)
+ allow(QA::Factory::Product).to receive(:new)
+ end
+
+ it 'builds all dependencies first' do
+ expect(dependency).to receive(:fabricate!).once
+
+ subject.fabricate!
+ end
+ end
+end
diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb
new file mode 100644
index 00000000000..32405415126
--- /dev/null
+++ b/qa/spec/factory/dependency_spec.rb
@@ -0,0 +1,59 @@
+describe QA::Factory::Dependency do
+ let(:dependency) { spy('dependency' ) }
+ let(:factory) { spy('factory') }
+ let(:block) { spy('block') }
+
+ let(:signature) do
+ double('signature', factory: dependency, block: block)
+ end
+
+ subject do
+ described_class.new(:mydep, factory, signature)
+ end
+
+ describe '#overridden?' do
+ it 'returns true if factory has overridden dependency' do
+ allow(factory).to receive(:mydep).and_return('something')
+
+ expect(subject).to be_overridden
+ end
+
+ it 'returns false if dependency has not been overridden' do
+ allow(factory).to receive(:mydep).and_return(nil)
+
+ expect(subject).not_to be_overridden
+ end
+ end
+
+ describe '#build!' do
+ context 'when dependency has been overridden' do
+ before do
+ allow(subject).to receive(:overridden?).and_return(true)
+ end
+
+ it 'does not fabricate dependency' do
+ subject.build!
+
+ expect(dependency).not_to have_received(:fabricate!)
+ end
+ end
+
+ context 'when dependency has not been overridden' do
+ before do
+ allow(subject).to receive(:overridden?).and_return(false)
+ end
+
+ it 'fabricates dependency' do
+ subject.build!
+
+ expect(dependency).to have_received(:fabricate!)
+ end
+
+ it 'sets product in the factory' do
+ subject.build!
+
+ expect(factory).to have_received(:mydep=).with(dependency)
+ end
+ end
+ end
+end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
new file mode 100644
index 00000000000..3d9e86a641b
--- /dev/null
+++ b/qa/spec/factory/product_spec.rb
@@ -0,0 +1,44 @@
+describe QA::Factory::Product do
+ let(:factory) { spy('factory') }
+ let(:product) { spy('product') }
+
+ describe '.populate!' do
+ it 'instantiates and yields factory' do
+ expect(described_class).to receive(:new).with(factory)
+
+ described_class.populate!(factory) do |instance|
+ instance.something = 'string'
+ end
+
+ expect(factory).to have_received(:something=).with('string')
+ end
+
+ it 'returns a fabrication product' do
+ expect(described_class).to receive(:new)
+ .with(factory).and_return(product)
+
+ result = described_class.populate!(factory) do |instance|
+ instance.something = 'string'
+ end
+
+ expect(result).to be product
+ end
+
+ it 'raises unless block given' do
+ expect { described_class.populate!(factory) }
+ .to raise_error ArgumentError
+ end
+ end
+
+ describe '.visit!' do
+ it 'makes it possible to visit fabrication product' do
+ allow_any_instance_of(described_class)
+ .to receive(:current_url).and_return('some url')
+ allow_any_instance_of(described_class)
+ .to receive(:visit).and_return('visited some url')
+
+ expect(described_class.new(factory).visit!)
+ .to eq 'visited some url'
+ end
+ end
+end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
new file mode 100644
index 00000000000..57a72a04507
--- /dev/null
+++ b/qa/spec/runtime/env_spec.rb
@@ -0,0 +1,64 @@
+describe QA::Runtime::Env do
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ end
+
+ describe '.chrome_headless?' do
+ context 'when there is an env variable set' do
+ it 'returns false when falsey values specified' do
+ stub_env('CHROME_HEADLESS', 'false')
+ expect(described_class.chrome_headless?).to be_falsey
+
+ stub_env('CHROME_HEADLESS', 'no')
+ expect(described_class.chrome_headless?).to be_falsey
+
+ stub_env('CHROME_HEADLESS', '0')
+ expect(described_class.chrome_headless?).to be_falsey
+ end
+
+ it 'returns true when anything else specified' do
+ stub_env('CHROME_HEADLESS', 'true')
+ expect(described_class.chrome_headless?).to be_truthy
+
+ stub_env('CHROME_HEADLESS', '1')
+ expect(described_class.chrome_headless?).to be_truthy
+
+ stub_env('CHROME_HEADLESS', 'anything')
+ expect(described_class.chrome_headless?).to be_truthy
+ end
+ end
+
+ context 'when there is no env variable set' do
+ it 'returns the default, true' do
+ stub_env('CHROME_HEADLESS', nil)
+ expect(described_class.chrome_headless?).to be_truthy
+ end
+ end
+ end
+
+ describe '.running_in_ci?' do
+ context 'when there is an env variable set' do
+ it 'returns true if CI' do
+ stub_env('CI', 'anything')
+ expect(described_class.running_in_ci?).to be_truthy
+ end
+
+ it 'returns true if CI_SERVER' do
+ stub_env('CI_SERVER', 'anything')
+ expect(described_class.running_in_ci?).to be_truthy
+ end
+ end
+
+ context 'when there is no env variable set' do
+ it 'returns true' do
+ stub_env('CI', nil)
+ stub_env('CI_SERVER', nil)
+ expect(described_class.running_in_ci?).to be_falsey
+ end
+ end
+ end
+
+ def stub_env(name, value)
+ allow(ENV).to receive(:[]).with(name).and_return(value)
+ end
+end
diff --git a/rubocop/cop/active_record_dependent.rb b/rubocop/cop/active_record_dependent.rb
deleted file mode 100644
index 8d15f150885..00000000000
--- a/rubocop/cop/active_record_dependent.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require_relative '../model_helpers'
-
-module RuboCop
- module Cop
- # Cop that prevents the use of `dependent: ...` in ActiveRecord models.
- class ActiveRecordDependent < RuboCop::Cop::Cop
- include ModelHelpers
-
- MSG = 'Do not use `dependent: to remove associated data, ' \
- 'use foreign keys with cascading deletes instead'.freeze
-
- METHOD_NAMES = [:has_many, :has_one, :belongs_to].freeze
-
- def on_send(node)
- return unless in_model?(node)
- return unless METHOD_NAMES.include?(node.children[1])
-
- node.children.last.each_node(:pair) do |pair|
- key_name = pair.children[0].children[0]
-
- add_offense(pair, :expression) if key_name == :dependent
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/active_record_serialize.rb b/rubocop/cop/active_record_serialize.rb
deleted file mode 100644
index 204caf37f8b..00000000000
--- a/rubocop/cop/active_record_serialize.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require_relative '../model_helpers'
-
-module RuboCop
- module Cop
- # Cop that prevents the use of `serialize` in ActiveRecord models.
- class ActiveRecordSerialize < RuboCop::Cop::Cop
- include ModelHelpers
-
- MSG = 'Do not store serialized data in the database, use separate columns and/or tables instead'.freeze
-
- def on_send(node)
- return unless in_model?(node)
-
- add_offense(node, :selector) if node.children[1] == :serialize
- end
- end
- end
-end
diff --git a/rubocop/cop/custom_error_class.rb b/rubocop/cop/custom_error_class.rb
deleted file mode 100644
index 38d93acfe88..00000000000
--- a/rubocop/cop/custom_error_class.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-module RuboCop
- module Cop
- # This cop makes sure that custom error classes, when empty, are declared
- # with Class.new.
- #
- # @example
- # # bad
- # class FooError < StandardError
- # end
- #
- # # okish
- # class FooError < StandardError; end
- #
- # # good
- # FooError = Class.new(StandardError)
- class CustomErrorClass < RuboCop::Cop::Cop
- MSG = 'Use `Class.new(SuperClass)` to define an empty custom error class.'.freeze
-
- def on_class(node)
- _klass, parent, body = node.children
-
- return if body
-
- parent_klass = class_name_from_node(parent)
-
- return unless parent_klass && parent_klass.to_s.end_with?('Error')
-
- add_offense(node, :expression)
- end
-
- def autocorrect(node)
- klass, parent, _body = node.children
- replacement = "#{class_name_from_node(klass)} = Class.new(#{class_name_from_node(parent)})"
-
- lambda do |corrector|
- corrector.replace(node.source_range, replacement)
- end
- end
-
- private
-
- # The nested constant `Foo::Bar::Baz` looks like:
- #
- # s(:const,
- # s(:const,
- # s(:const, nil, :Foo), :Bar), :Baz)
- #
- # So recurse through that to get the name as written in the source.
- #
- def class_name_from_node(node, suffix = nil)
- return unless node&.type == :const
-
- name = node.children[1].to_s
- name = "#{name}::#{suffix}" if suffix
-
- if node.children[0]
- class_name_from_node(node.children[0], name)
- else
- name
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb
deleted file mode 100644
index e157d8e0791..00000000000
--- a/rubocop/cop/gem_fetcher.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module RuboCop
- module Cop
- # This cop prevents usage of the `git` and `github` arguments to `gem` in a
- # `Gemfile` in order to avoid additional points of failure beyond
- # rubygems.org.
- class GemFetcher < RuboCop::Cop::Cop
- MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'.freeze
-
- GIT_KEYS = [:git, :github].freeze
-
- def on_send(node)
- return unless gemfile?(node)
-
- func_name = node.children[1]
- return unless func_name == :gem
-
- node.children.last.each_node(:pair) do |pair|
- key_name = pair.children[0].children[0].to_sym
- if GIT_KEYS.include?(key_name)
- add_offense(node, pair.source_range, MSG)
- end
- end
- end
-
- private
-
- def gemfile?(node)
- node
- .location
- .expression
- .source_buffer
- .name
- .end_with?("Gemfile")
- end
- end
- end
-end
diff --git a/rubocop/cop/gitlab/module_with_instance_variables.rb b/rubocop/cop/gitlab/module_with_instance_variables.rb
new file mode 100644
index 00000000000..dd8bd2dfdf0
--- /dev/null
+++ b/rubocop/cop/gitlab/module_with_instance_variables.rb
@@ -0,0 +1,63 @@
+module RuboCop
+ module Cop
+ module Gitlab
+ class ModuleWithInstanceVariables < RuboCop::Cop::Cop
+ MSG = <<~EOL.freeze
+ Do not use instance variables in a module. Please read this
+ for the rationale behind it:
+
+ https://docs.gitlab.com/ee/development/module_with_instance_variables.html
+ EOL
+
+ def on_module(node)
+ check_method_definition(node)
+
+ # Not sure why some module would have an extra begin wrapping around
+ node.each_child_node(:begin) do |begin_node|
+ check_method_definition(begin_node)
+ end
+ end
+
+ private
+
+ def check_method_definition(node)
+ node.each_child_node(:def) do |definition|
+ # We allow this pattern:
+ #
+ # def f
+ # @f ||= true
+ # end
+ if only_ivar_or_assignment?(definition)
+ # We don't allow if any other ivar is used
+ definition.each_descendant(:ivar) do |offense|
+ add_offense(offense, location: :expression)
+ end
+ # We allow initialize method and single ivar
+ elsif !initialize_method?(definition) && !single_ivar?(definition)
+ definition.each_descendant(:ivar, :ivasgn) do |offense|
+ add_offense(offense, location: :expression)
+ end
+ end
+ end
+ end
+
+ def only_ivar_or_assignment?(definition)
+ node = definition.child_nodes.last
+
+ definition.child_nodes.size == 2 &&
+ node.or_asgn_type? && node.child_nodes.first.ivasgn_type?
+ end
+
+ def single_ivar?(definition)
+ node = definition.child_nodes.last
+
+ definition.child_nodes.size == 2 && node.ivar_type?
+ end
+
+ def initialize_method?(definition)
+ definition.children.first == :initialize
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/in_batches.rb b/rubocop/cop/in_batches.rb
deleted file mode 100644
index c0240187e66..00000000000
--- a/rubocop/cop/in_batches.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require_relative '../model_helpers'
-
-module RuboCop
- module Cop
- # Cop that prevents the use of `in_batches`
- class InBatches < RuboCop::Cop::Cop
- MSG = 'Do not use `in_batches`, use `each_batch` from the EachBatch module instead'.freeze
-
- def on_send(node)
- return unless node.children[1] == :in_batches
-
- add_offense(node, :selector)
- end
- end
- end
-end
diff --git a/rubocop/cop/include_sidekiq_worker.rb b/rubocop/cop/include_sidekiq_worker.rb
new file mode 100644
index 00000000000..8da4a147219
--- /dev/null
+++ b/rubocop/cop/include_sidekiq_worker.rb
@@ -0,0 +1,29 @@
+require_relative '../spec_helpers'
+
+module RuboCop
+ module Cop
+ # Cop that makes sure workers include `ApplicationWorker`, not `Sidekiq::Worker`.
+ class IncludeSidekiqWorker < RuboCop::Cop::Cop
+ include SpecHelpers
+
+ MSG = 'Include `ApplicationWorker`, not `Sidekiq::Worker`.'.freeze
+
+ def_node_matcher :includes_sidekiq_worker?, <<~PATTERN
+ (send nil? :include (const (const nil? :Sidekiq) :Worker))
+ PATTERN
+
+ def on_send(node)
+ return if in_spec?(node)
+ return unless includes_sidekiq_worker?(node)
+
+ add_offense(node.arguments.first, location: :expression)
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ corrector.replace(node.source_range, 'ApplicationWorker')
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/line_break_after_guard_clauses.rb b/rubocop/cop/line_break_after_guard_clauses.rb
deleted file mode 100644
index 67477f064ab..00000000000
--- a/rubocop/cop/line_break_after_guard_clauses.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-module RuboCop
- module Cop
- # Ensures a line break after guard clauses.
- #
- # @example
- # # bad
- # return unless condition
- # do_stuff
- #
- # # good
- # return unless condition
- #
- # do_stuff
- #
- # # bad
- # raise if condition
- # do_stuff
- #
- # # good
- # raise if condition
- #
- # do_stuff
- #
- # Multiple guard clauses are allowed without
- # line break.
- #
- # # good
- # return unless condition_a
- # return unless condition_b
- #
- # do_stuff
- #
- # Guard clauses in case statement are allowed without
- # line break.
- #
- # # good
- # case model
- # when condition_a
- # return true unless condition_b
- # when
- # ...
- # end
- #
- # Guard clauses before end are allowed without
- # line break.
- #
- # # good
- # if condition_a
- # do_something
- # else
- # do_something_else
- # return unless condition
- # end
- #
- # do_something_more
- class LineBreakAfterGuardClauses < RuboCop::Cop::Cop
- MSG = 'Add a line break after guard clauses'
-
- def_node_matcher :guard_clause_node?, <<-PATTERN
- [{(send nil? {:raise :fail :throw} ...) return break next} single_line?]
- PATTERN
-
- def on_if(node)
- return unless node.single_line?
- return unless guard_clause?(node)
- return if next_line(node).blank? || clause_last_line?(next_line(node)) || guard_clause?(next_sibling(node))
-
- add_offense(node, :expression, MSG)
- end
-
- def autocorrect(node)
- lambda do |corrector|
- corrector.insert_after(node.loc.expression, "\n")
- end
- end
-
- private
-
- def guard_clause?(node)
- return false unless node.if_type?
-
- guard_clause_node?(node.if_branch)
- end
-
- def next_sibling(node)
- node.parent.children[node.sibling_index + 1]
- end
-
- def next_line(node)
- processed_source[node.loc.line]
- end
-
- def clause_last_line?(line)
- line =~ /^\s*(?:end|elsif|else|when|rescue|ensure)/
- end
- end
- end
-end
diff --git a/rubocop/cop/migration/add_column.rb b/rubocop/cop/migration/add_column.rb
index d2cf36c454a..2530d6477e8 100644
--- a/rubocop/cop/migration/add_column.rb
+++ b/rubocop/cop/migration/add_column.rb
@@ -29,7 +29,7 @@ module RuboCop
opts.each_node(:pair) do |pair|
if hash_key_type(pair) == :sym && hash_key_name(pair) == :default
- add_offense(node, :selector)
+ add_offense(node, location: :selector)
end
end
end
diff --git a/rubocop/cop/migration/add_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb
index d1fc94d55be..d78c7b9b043 100644
--- a/rubocop/cop/migration/add_concurrent_foreign_key.rb
+++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb
@@ -15,7 +15,7 @@ module RuboCop
name = node.children[1]
- add_offense(node, :selector) if name == :add_foreign_key
+ add_offense(node, location: :selector) if name == :add_foreign_key
end
def method_name(node)
diff --git a/rubocop/cop/migration/add_concurrent_index.rb b/rubocop/cop/migration/add_concurrent_index.rb
index 69852f4d580..a2e4ac72565 100644
--- a/rubocop/cop/migration/add_concurrent_index.rb
+++ b/rubocop/cop/migration/add_concurrent_index.rb
@@ -21,7 +21,7 @@ module RuboCop
node.each_ancestor(:def) do |def_node|
next unless method_name(def_node) == :change
- add_offense(def_node, :name)
+ add_offense(def_node, location: :name)
end
end
diff --git a/rubocop/cop/migration/add_index.rb b/rubocop/cop/migration/add_index.rb
index fa21a0d6555..4aea3c0cce3 100644
--- a/rubocop/cop/migration/add_index.rb
+++ b/rubocop/cop/migration/add_index.rb
@@ -27,7 +27,7 @@ module RuboCop
# data in these tables yet.
next if new_tables.include?(first_arg)
- add_offense(send_node, :selector)
+ add_offense(send_node, location: :selector)
end
end
diff --git a/rubocop/cop/migration/add_timestamps.rb b/rubocop/cop/migration/add_timestamps.rb
index 08ddd91e54d..ba32d6a9960 100644
--- a/rubocop/cop/migration/add_timestamps.rb
+++ b/rubocop/cop/migration/add_timestamps.rb
@@ -13,7 +13,7 @@ module RuboCop
def on_send(node)
return unless in_migration?(node)
- add_offense(node, :selector) if method_name(node) == :add_timestamps
+ add_offense(node, location: :selector) if method_name(node) == :add_timestamps
end
def method_name(node)
diff --git a/rubocop/cop/migration/datetime.rb b/rubocop/cop/migration/datetime.rb
index 9cba3c35b26..03ad3f3f601 100644
--- a/rubocop/cop/migration/datetime.rb
+++ b/rubocop/cop/migration/datetime.rb
@@ -17,7 +17,7 @@ module RuboCop
method_name = node.children[1]
if method_name == :datetime || method_name == :timestamp
- add_offense(send_node, :selector, format(MSG, method_name))
+ add_offense(send_node, location: :selector, message: format(MSG, method_name))
end
end
end
@@ -32,7 +32,7 @@ module RuboCop
last_argument = descendant.children.last
if last_argument == :datetime || last_argument == :timestamp
- add_offense(node, :expression, format(MSG, last_argument))
+ add_offense(node, location: :expression, message: format(MSG, last_argument))
end
end
end
diff --git a/rubocop/cop/migration/hash_index.rb b/rubocop/cop/migration/hash_index.rb
index 2cc59691d84..3206b73bd3d 100644
--- a/rubocop/cop/migration/hash_index.rb
+++ b/rubocop/cop/migration/hash_index.rb
@@ -29,7 +29,7 @@ module RuboCop
hash_key_name(pair) == :using
if hash_key_value(pair).to_s == 'hash'
- add_offense(pair, :expression)
+ add_offense(pair, location: :expression)
end
end
end
diff --git a/rubocop/cop/migration/remove_column.rb b/rubocop/cop/migration/remove_column.rb
new file mode 100644
index 00000000000..fffb4ab7fab
--- /dev/null
+++ b/rubocop/cop/migration/remove_column.rb
@@ -0,0 +1,30 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if remove_column is used in a regular (not
+ # post-deployment) migration.
+ class RemoveColumn < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`remove_column` must only be used in post-deployment migrations'.freeze
+
+ def on_def(node)
+ def_method = node.children[0]
+
+ return unless in_migration?(node) && !in_post_deployment_migration?(node)
+ return unless def_method == :change || def_method == :up
+
+ node.each_descendant(:send) do |send_node|
+ send_method = send_node.children[1]
+
+ if send_method == :remove_column
+ add_offense(send_node, location: :selector)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/remove_concurrent_index.rb b/rubocop/cop/migration/remove_concurrent_index.rb
index 268c49865cb..2328740cf36 100644
--- a/rubocop/cop/migration/remove_concurrent_index.rb
+++ b/rubocop/cop/migration/remove_concurrent_index.rb
@@ -16,7 +16,7 @@ module RuboCop
return unless node.children[1] == :remove_concurrent_index
node.each_ancestor(:def) do |def_node|
- add_offense(def_node, :name) if method_name(def_node) == :change
+ add_offense(def_node, location: :name) if method_name(def_node) == :change
end
end
diff --git a/rubocop/cop/migration/remove_index.rb b/rubocop/cop/migration/remove_index.rb
index 613b35dd00d..4df3b1ba756 100644
--- a/rubocop/cop/migration/remove_index.rb
+++ b/rubocop/cop/migration/remove_index.rb
@@ -13,7 +13,7 @@ module RuboCop
return unless in_migration?(node)
node.each_descendant(:send) do |send_node|
- add_offense(send_node, :selector) if method_name(send_node) == :remove_index
+ add_offense(send_node, location: :selector) if method_name(send_node) == :remove_index
end
end
diff --git a/rubocop/cop/migration/reversible_add_column_with_default.rb b/rubocop/cop/migration/reversible_add_column_with_default.rb
index f413f06f39b..dd49188defa 100644
--- a/rubocop/cop/migration/reversible_add_column_with_default.rb
+++ b/rubocop/cop/migration/reversible_add_column_with_default.rb
@@ -9,7 +9,7 @@ module RuboCop
include MigrationHelpers
def_node_matcher :add_column_with_default?, <<~PATTERN
- (send nil :add_column_with_default $...)
+ (send nil? :add_column_with_default $...)
PATTERN
def_node_matcher :defines_change?, <<~PATTERN
@@ -26,7 +26,7 @@ module RuboCop
node.each_ancestor(:def) do |def_node|
next unless defines_change?(def_node)
- add_offense(def_node, :name)
+ add_offense(def_node, location: :name)
end
end
end
diff --git a/rubocop/cop/migration/safer_boolean_column.rb b/rubocop/cop/migration/safer_boolean_column.rb
index 0335c25d85d..dc5c55df6fb 100644
--- a/rubocop/cop/migration/safer_boolean_column.rb
+++ b/rubocop/cop/migration/safer_boolean_column.rb
@@ -28,7 +28,7 @@ module RuboCop
].freeze
def_node_matcher :add_column?, <<~PATTERN
- (send nil :add_column $...)
+ (send nil? :add_column $...)
PATTERN
def on_send(node)
@@ -54,7 +54,7 @@ module RuboCop
NULL_OFFENSE
end
- add_offense(node, :expression, format(offense, table)) if offense
+ add_offense(node, location: :expression, message: format(offense, table)) if offense
end
def no_default?(opts)
diff --git a/rubocop/cop/migration/timestamps.rb b/rubocop/cop/migration/timestamps.rb
index 71a9420cc3b..6cf5648b996 100644
--- a/rubocop/cop/migration/timestamps.rb
+++ b/rubocop/cop/migration/timestamps.rb
@@ -14,7 +14,7 @@ module RuboCop
return unless in_migration?(node)
node.each_descendant(:send) do |send_node|
- add_offense(send_node, :selector) if method_name(send_node) == :timestamps
+ add_offense(send_node, location: :selector) if method_name(send_node) == :timestamps
end
end
diff --git a/rubocop/cop/migration/update_column_in_batches.rb b/rubocop/cop/migration/update_column_in_batches.rb
index 3f886cbfea3..db2b5564297 100644
--- a/rubocop/cop/migration/update_column_in_batches.rb
+++ b/rubocop/cop/migration/update_column_in_batches.rb
@@ -18,7 +18,7 @@ module RuboCop
spec_path = spec_filename(node)
unless File.exist?(File.expand_path(spec_path, rails_root))
- add_offense(node, :expression, format(MSG, spec_path))
+ add_offense(node, location: :expression, message: format(MSG, spec_path))
end
end
diff --git a/rubocop/cop/migration/update_large_table.rb b/rubocop/cop/migration/update_large_table.rb
index 3ae3fb1b68e..bb14d0f4f56 100644
--- a/rubocop/cop/migration/update_large_table.rb
+++ b/rubocop/cop/migration/update_large_table.rb
@@ -35,7 +35,7 @@ module RuboCop
].freeze
def_node_matcher :batch_update?, <<~PATTERN
- (send nil ${:add_column_with_default :update_column_in_batches} $(sym ...) ...)
+ (send nil? ${:add_column_with_default :update_column_in_batches} $(sym ...) ...)
PATTERN
def on_send(node)
@@ -49,7 +49,7 @@ module RuboCop
return unless LARGE_TABLES.include?(table)
- add_offense(node, :expression, format(MSG, update_method, table))
+ add_offense(node, location: :expression, message: format(MSG, update_method, table))
end
end
end
diff --git a/rubocop/cop/polymorphic_associations.rb b/rubocop/cop/polymorphic_associations.rb
deleted file mode 100644
index 7d554704550..00000000000
--- a/rubocop/cop/polymorphic_associations.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-require_relative '../model_helpers'
-
-module RuboCop
- module Cop
- # Cop that prevents the use of polymorphic associations
- class PolymorphicAssociations < RuboCop::Cop::Cop
- include ModelHelpers
-
- MSG = 'Do not use polymorphic associations, use separate tables instead'.freeze
-
- def on_send(node)
- return unless in_model?(node)
- return unless node.children[1] == :belongs_to
-
- node.children.last.each_node(:pair) do |pair|
- key_name = pair.children[0].children[0]
-
- add_offense(pair, :expression) if key_name == :polymorphic
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/project_path_helper.rb b/rubocop/cop/project_path_helper.rb
index 3e1ce71ac06..f3810622eb1 100644
--- a/rubocop/cop/project_path_helper.rb
+++ b/rubocop/cop/project_path_helper.rb
@@ -17,7 +17,7 @@ module RuboCop
return unless method_name(namespace_expr) == :namespace
return unless receiver(namespace_expr) == project_expr
- add_offense(node, :selector)
+ add_offense(node, location: :selector)
end
def autocorrect(node)
diff --git a/rubocop/cop/redirect_with_status.rb b/rubocop/cop/redirect_with_status.rb
deleted file mode 100644
index 36810642c88..00000000000
--- a/rubocop/cop/redirect_with_status.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-module RuboCop
- module Cop
- # This cop prevents usage of 'redirect_to' in actions 'destroy' without specifying 'status'.
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/31840
- class RedirectWithStatus < RuboCop::Cop::Cop
- MSG = 'Do not use "redirect_to" without "status" in "destroy" action'.freeze
-
- def on_def(node)
- return unless in_controller?(node)
- return unless destroy?(node) || destroy_all?(node)
-
- node.each_descendant(:send) do |def_node|
- next unless redirect_to?(def_node)
-
- methods = []
-
- def_node.children.last.each_node(:pair) do |pair|
- methods << pair.children.first.children.first
- end
-
- add_offense(def_node, :selector) unless methods.include?(:status)
- end
- end
-
- private
-
- def in_controller?(node)
- node.location.expression.source_buffer.name.end_with?('_controller.rb')
- end
-
- def destroy?(node)
- node.children.first == :destroy
- end
-
- def destroy_all?(node)
- node.children.first == :destroy_all
- end
-
- def redirect_to?(node)
- node.children[1] == :redirect_to
- end
- end
- end
-end
diff --git a/rubocop/cop/rspec/env_assignment.rb b/rubocop/cop/rspec/env_assignment.rb
index 257454af0e1..8b61fa8e264 100644
--- a/rubocop/cop/rspec/env_assignment.rb
+++ b/rubocop/cop/rspec/env_assignment.rb
@@ -1,4 +1,3 @@
-require 'rubocop-rspec'
require_relative '../../spec_helpers'
module RuboCop
@@ -17,7 +16,7 @@ module RuboCop
# before do
# stub_env('FOO', 'bar')
# end
- class EnvAssignment < Cop
+ class EnvAssignment < RuboCop::Cop::Cop
include SpecHelpers
MESSAGE = "Don't assign to ENV, use `stub_env` instead.".freeze
@@ -32,7 +31,7 @@ module RuboCop
return unless in_spec?(node)
return unless env_assignment?(node)
- add_offense(node, :expression, MESSAGE)
+ add_offense(node, location: :expression, message: MESSAGE)
end
def autocorrect(node)
diff --git a/rubocop/cop/rspec/single_line_hook.rb b/rubocop/cop/rspec/single_line_hook.rb
deleted file mode 100644
index be611054323..00000000000
--- a/rubocop/cop/rspec/single_line_hook.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'rubocop-rspec'
-
-module RuboCop
- module Cop
- module RSpec
- # This cop checks for single-line hook blocks
- #
- # @example
- #
- # # bad
- # before { do_something }
- # after(:each) { undo_something }
- #
- # # good
- # before do
- # do_something
- # end
- #
- # after(:each) do
- # undo_something
- # end
- class SingleLineHook < Cop
- MESSAGE = "Don't use single-line hook blocks.".freeze
-
- def_node_search :rspec_hook?, <<~PATTERN
- (send nil {:after :around :before} ...)
- PATTERN
-
- def on_block(node)
- return unless rspec_hook?(node)
- return unless node.single_line?
-
- add_offense(node, :expression, MESSAGE)
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/rspec/verbose_include_metadata.rb b/rubocop/cop/rspec/verbose_include_metadata.rb
deleted file mode 100644
index 58390622d60..00000000000
--- a/rubocop/cop/rspec/verbose_include_metadata.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop-rspec'
-
-module RuboCop
- module Cop
- module RSpec
- # Checks for verbose include metadata used in the specs.
- #
- # @example
- # # bad
- # describe MyClass, js: true do
- # end
- #
- # # good
- # describe MyClass, :js do
- # end
- class VerboseIncludeMetadata < Cop
- MSG = 'Use `%s` instead of `%s`.'
-
- SELECTORS = %i[describe context feature example_group it specify example scenario its].freeze
-
- def_node_matcher :include_metadata, <<-PATTERN
- (send {(const nil :RSpec) nil} {#{SELECTORS.map(&:inspect).join(' ')}}
- !const
- ...
- (hash $...))
- PATTERN
-
- def_node_matcher :invalid_metadata?, <<-PATTERN
- (pair
- (sym $...)
- (true))
- PATTERN
-
- def on_send(node)
- invalid_metadata_matches(node) do |match|
- add_offense(node, :expression, format(MSG, good(match), bad(match)))
- end
- end
-
- def autocorrect(node)
- lambda do |corrector|
- invalid_metadata_matches(node) do |match|
- corrector.replace(match.loc.expression, good(match))
- end
- end
- end
-
- private
-
- def invalid_metadata_matches(node)
- include_metadata(node) do |matches|
- matches.select(&method(:invalid_metadata?)).each do |match|
- yield match
- end
- end
- end
-
- def bad(match)
- "#{metadata_key(match)}: true"
- end
-
- def good(match)
- ":#{metadata_key(match)}"
- end
-
- def metadata_key(match)
- match.children[0].source
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/sidekiq_options_queue.rb b/rubocop/cop/sidekiq_options_queue.rb
new file mode 100644
index 00000000000..253d2958866
--- /dev/null
+++ b/rubocop/cop/sidekiq_options_queue.rb
@@ -0,0 +1,27 @@
+require_relative '../spec_helpers'
+
+module RuboCop
+ module Cop
+ # Cop that prevents manually setting a queue in Sidekiq workers.
+ class SidekiqOptionsQueue < RuboCop::Cop::Cop
+ include SpecHelpers
+
+ MSG = 'Do not manually set a queue; `ApplicationWorker` sets one automatically.'.freeze
+
+ def_node_matcher :sidekiq_options?, <<~PATTERN
+ (send nil? :sidekiq_options $...)
+ PATTERN
+
+ def on_send(node)
+ return if in_spec?(node)
+ return unless sidekiq_options?(node)
+
+ node.arguments.first.each_node(:pair) do |pair|
+ key_name = pair.key.children[0]
+
+ add_offense(pair, location: :expression) if key_name == :queue
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/migration_helpers.rb b/rubocop/migration_helpers.rb
index c3473771178..c066d424437 100644
--- a/rubocop/migration_helpers.rb
+++ b/rubocop/migration_helpers.rb
@@ -7,5 +7,11 @@ module RuboCop
dirname.end_with?('db/migrate', 'db/post_migrate')
end
+
+ def in_post_deployment_migration?(node)
+ dirname = File.dirname(node.location.expression.source_buffer.name)
+
+ dirname.end_with?('db/post_migrate')
+ end
end
end
diff --git a/rubocop/model_helpers.rb b/rubocop/model_helpers.rb
deleted file mode 100644
index 309723dc34c..00000000000
--- a/rubocop/model_helpers.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module RuboCop
- module ModelHelpers
- # Returns true if the given node originated from the models directory.
- def in_model?(node)
- path = node.location.expression.source_buffer.name
- models_path = File.join(Dir.pwd, 'app', 'models')
-
- path.start_with?(models_path)
- end
- end
-end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 7621ea50da9..2f63babc425 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,12 +1,5 @@
-require_relative 'cop/active_record_dependent'
-require_relative 'cop/active_record_serialize'
-require_relative 'cop/custom_error_class'
-require_relative 'cop/gem_fetcher'
-require_relative 'cop/in_batches'
-require_relative 'cop/line_break_after_guard_clauses'
-require_relative 'cop/polymorphic_associations'
-require_relative 'cop/project_path_helper'
-require_relative 'cop/redirect_with_status'
+require_relative 'cop/gitlab/module_with_instance_variables'
+require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index'
@@ -14,6 +7,7 @@ require_relative 'cop/migration/add_index'
require_relative 'cop/migration/add_timestamps'
require_relative 'cop/migration/datetime'
require_relative 'cop/migration/hash_index'
+require_relative 'cop/migration/remove_column'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
require_relative 'cop/migration/reversible_add_column_with_default'
@@ -21,6 +15,6 @@ require_relative 'cop/migration/safer_boolean_column'
require_relative 'cop/migration/timestamps'
require_relative 'cop/migration/update_column_in_batches'
require_relative 'cop/migration/update_large_table'
+require_relative 'cop/project_path_helper'
require_relative 'cop/rspec/env_assignment'
-require_relative 'cop/rspec/single_line_hook'
-require_relative 'cop/rspec/verbose_include_metadata'
+require_relative 'cop/sidekiq_options_queue'
diff --git a/scripts/add-code-formatters b/scripts/add-code-formatters
new file mode 100755
index 00000000000..56bb8754d80
--- /dev/null
+++ b/scripts/add-code-formatters
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Check if file exists with -f. Check if in in the gdk rook directory.
+if [ ! -f ../GDK_ROOT ]; then
+ echo "Please run script from gitlab (e.g. gitlab-development-kit/gitlab) root directory."
+ exit 1
+fi
+
+PRECOMMIT=$(git rev-parse --git-dir)/hooks/pre-commit
+
+# Check if symlink exists with -L. Check if script was already installed.
+if [ -L $PRECOMMIT ]; then
+ echo "Pre-commit script already installed."
+ exit 1
+fi
+
+ln -s ./pre-commit $PRECOMMIT
+echo "Pre-commit script installed successfully"
diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh
index 28f6cfb50ae..286b1325f1d 100644
--- a/scripts/create_mysql_user.sh
+++ b/scripts/create_mysql_user.sh
@@ -1,7 +1,7 @@
#!/bin/bash
mysql --user=root --host=mysql <<EOF
-CREATE DATABASE IF NOT EXISTS gitlabhq_test;
+CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index 8e05eca8d7e..ecb68c6acc6 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -1,7 +1,8 @@
#!/usr/bin/env ruby
gitaly_dir = 'tmp/tests/gitaly'
-env = { 'HOME' => File.expand_path('tmp/tests') }
+env = { 'HOME' => File.expand_path('tmp/tests'),
+ 'GEM_PATH' => Gem.path.join(':') }
args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml]
# Print the PID of the spawned process
diff --git a/scripts/lint-changelog-yaml b/scripts/lint-changelog-yaml
index cce5f1c7667..6553e02ffca 100755
--- a/scripts/lint-changelog-yaml
+++ b/scripts/lint-changelog-yaml
@@ -8,11 +8,13 @@ invalid_changelogs = Dir['changelogs/**/*'].reject do |changelog|
begin
YAML.load_file(changelog)
- rescue
+ rescue => exception
+ puts exception
end
end
if invalid_changelogs.any?
+ puts
puts "Invalid changelogs found!\n"
puts invalid_changelogs.sort
exit 1
diff --git a/scripts/pre-commit b/scripts/pre-commit
new file mode 100644
index 00000000000..48935e90a87
--- /dev/null
+++ b/scripts/pre-commit
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Check if file exists with -f. Check if in in the gdk rook directory.
+if [ ! -f ../GDK_ROOT ]; then
+ echo "Please run pre-commit from gitlab (e.g. gitlab-development-kit/gitlab) root directory."
+ exit 1
+fi
+
+jsfiles=$(git diff --cached --name-only --diff-filter=ACM "*.js" | tr '\n' ' ')
+[ -z "$jsfiles" ] && exit 0
+
+# Prettify all staged .js files
+echo "$jsfiles" | xargs ./node_modules/.bin/prettier --write
+
+# Add back the modified/prettified files to staging
+echo "$jsfiles" | xargs git add
+
+exit 0
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 51a2fd81a79..2a2bc67800d 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -3,12 +3,10 @@
require ::File.expand_path('../lib/gitlab/popen', __dir__)
tasks = [
- %w[bundle exec bundle-audit check --update],
%w[bundle exec rake config_lint],
%w[bundle exec rake flay],
%w[bundle exec rake haml_lint],
%w[bundle exec rake scss_lint],
- %w[bundle exec rake brakeman],
%w[bundle exec license_finder],
%w[yarn run eslint],
%w[bundle exec rubocop --parallel],
diff --git a/scripts/trigger-build-omnibus b/scripts/trigger-build-omnibus
index 3c5c22c9372..85ea4aa74ac 100755
--- a/scripts/trigger-build-omnibus
+++ b/scripts/trigger-build-omnibus
@@ -21,6 +21,7 @@ module Omnibus
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
@@ -39,7 +40,9 @@ module Omnibus
"ref" => ENV["OMNIBUS_BRANCH"] || "master",
"variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
"variables[ALTERNATIVE_SOURCES]" => true,
- "variables[ee]" => ee? ? 'true' : 'false'
+ "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
@@ -63,14 +66,14 @@ module Omnibus
def wait!
loop do
- raise 'Pipeline timeout!' if timeout?
+ raise "Pipeline timed out after waiting for #{duration} minutes!" if timeout?
case status
- when :pending, :running
- puts "Waiting another #{INTERVAL} seconds ..."
+ when :created, :pending, :running
+ print "."
sleep INTERVAL
when :success
- puts "Omnibus pipeline succeeded!"
+ puts "Omnibus pipeline succeeded in #{duration} minutes!"
break
else
raise "Omnibus pipeline did not succeed!"
@@ -84,6 +87,10 @@ module Omnibus
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']
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index f044a068938..f350641a643 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -13,7 +13,7 @@ describe Admin::UsersController do
let!(:issue) { create(:issue, author: user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'deletes user and ghosts their contributions' do
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 44d504d5852..79bbc29e80d 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -13,8 +13,8 @@ describe Boards::IssuesController do
let!(:list2) { create(:list, board: board, label: development, position: 1) }
before do
- project.team << [user, :master]
- project.team << [guest, :guest]
+ project.add_master(user)
+ project.add_guest(guest)
end
describe 'GET index' do
@@ -221,7 +221,7 @@ describe Boards::IssuesController do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
end
it 'returns a forbidden 403 response' do
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb
index a2b432af23a..71d45a22d91 100644
--- a/spec/controllers/boards/lists_controller_spec.rb
+++ b/spec/controllers/boards/lists_controller_spec.rb
@@ -7,8 +7,8 @@ describe Boards::ListsController do
let(:guest) { create(:user) }
before do
- project.team << [user, :master]
- project.team << [guest, :guest]
+ project.add_master(user)
+ project.add_guest(guest)
end
describe 'GET index' do
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index 2f3d7be9abe..60547db82b6 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.team << [user, :master]
+ project.add_master(user)
end
it_behaves_like 'milestone tabs'
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index f9faa4fa59a..b4a731fd3a3 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -8,7 +8,7 @@ describe Dashboard::TodosController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe 'GET #index' do
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index 566d8515198..97c2c3fb940 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 9c6d584f59b..362d5cc4514 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -62,6 +62,25 @@ describe Groups::GroupMembersController do
end
end
+ describe 'PUT update' do
+ let(:requester) { create(:group_member, :access_request, group: group) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ Gitlab::Access.options.each do |label, value|
+ it "can change the access level to #{label}" do
+ xhr :put, :update, group_member: { access_level: value },
+ group_id: group,
+ id: requester
+
+ expect(requester.reload.human_access).to eq(label)
+ end
+ end
+ end
+
describe 'DELETE destroy' do
let(:member) { create(:group_member, :developer, group: group) }
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index c1aba46be04..733386500ca 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.team << [user, :master]
+ project.add_master(user)
end
describe '#index' do
diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb
index 9014b8b5084..e133950e684 100644
--- a/spec/controllers/notification_settings_controller_spec.rb
+++ b/spec/controllers/notification_settings_controller_spec.rb
@@ -6,7 +6,7 @@ describe NotificationSettingsController do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe '#create' do
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 581b3e4e4ab..46d618fa682 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -111,6 +111,8 @@ describe Projects::ArtifactsController do
end
describe 'GET raw' do
+ subject { get(:raw, namespace_id: project.namespace, project_id: project, job_id: job, path: path) }
+
context 'when the file exists' do
let(:path) { 'ci_artifacts.txt' }
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index f5ea097af8b..3bbe168f6d5 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::AvatarsController do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index 54282aa4001..88d4f4e9cd0 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.team << [user, :master]
+ project.add_master(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 6a1c07b4a0b..00a7df6ccc8 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -89,7 +89,7 @@ describe Projects::BlobController do
end
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -147,7 +147,7 @@ describe Projects::BlobController do
let(:developer) { create(:user) }
before do
- project.team << [developer, :developer]
+ project.add_developer(developer)
sign_in(developer)
get :edit, default_params
end
@@ -161,7 +161,7 @@ describe Projects::BlobController do
let(:master) { create(:user) }
before do
- project.team << [master, :master]
+ project.add_master(master)
sign_in(master)
get :edit, default_params
end
@@ -190,7 +190,7 @@ describe Projects::BlobController do
end
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index d6ccb92c54b..4d765229bde 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -55,6 +55,16 @@ describe Projects::BoardsController do
end
end
+ context 'issues are disabled' do
+ let(:project) { create(:project, :issues_disabled) }
+
+ it 'returns a not found 404 response' do
+ list_boards
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
def list_boards(format: :html)
get :index, namespace_id: project.namespace,
project_id: project,
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index d731200f70f..734396ddf7b 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -6,8 +6,8 @@ describe Projects::BranchesController do
let(:developer) { create(:user) }
before do
- project.team << [user, :master]
- project.team << [user, :developer]
+ project.add_master(user)
+ project.add_developer(user)
allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
@@ -148,6 +148,20 @@ describe Projects::BranchesController do
end
end
+ context 'when create branch service fails' do
+ let(:branch) { "./invalid-branch-name" }
+
+ it "doesn't post a system note" do
+ expect(SystemNoteService).not_to receive(:new_issue_branch)
+
+ post :create,
+ namespace_id: project.namespace,
+ project_id: project,
+ branch_name: branch,
+ issue_iid: issue.iid
+ end
+ end
+
context 'without issue feature access' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
index 8b460646059..99fdff5f846 100644
--- a/spec/controllers/projects/clusters/applications_controller_spec.rb
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -52,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do
context 'when application is already installing' do
before do
- create(:cluster_applications_helm, :installing, cluster: cluster)
+ create(:clusters_applications_helm, :installing, cluster: cluster)
end
it 'returns 400' do
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
index ee7928beb7e..be19fa93183 100644
--- a/spec/controllers/projects/clusters/gcp_controller_spec.rb
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -17,7 +17,6 @@ describe Projects::Clusters::GcpController do
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
@@ -78,6 +77,8 @@ describe Projects::Clusters::GcpController do
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)
@@ -138,7 +139,11 @@ describe Projects::Clusters::GcpController do
stub_google_api_validate_token
end
- context 'when creates a cluster on gke' do
+ context 'when google project billing is enabled' do
+ before do
+ stub_google_project_billing_status
+ end
+
it 'creates a new cluster' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
@@ -148,6 +153,15 @@ describe Projects::Clusters::GcpController do
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[:error]
+ expect(response).to render_template('new')
+ end
+ end
end
context 'when access token is expired' do
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 280b7e4d8b9..a3b13647c92 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -27,13 +27,6 @@ describe Projects::ClustersController do
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
- it 'assigns counters to correct values' do
- go
-
- expect(assigns(:active_count)).to eq(1)
- expect(assigns(:inactive_count)).to eq(1)
- end
-
context 'when page is specified' do
let(:last_page) { project.clusters.page.total_pages }
@@ -48,20 +41,6 @@ describe Projects::ClustersController do
expect(assigns(:clusters).current_page).to eq(last_page)
end
end
-
- context 'when only enabled clusters are requested' do
- it 'returns only enabled clusters' do
- get :index, namespace_id: project.namespace, project_id: project, scope: 'active'
- expect(assigns(:clusters)).to all(have_attributes(enabled: true))
- end
- end
-
- context 'when only disabled clusters are requested' do
- it 'returns only disabled clusters' do
- get :index, namespace_id: project.namespace, project_id: project, scope: 'inactive'
- expect(assigns(:clusters)).to all(have_attributes(enabled: false))
- end
- end
end
context 'when project does not have a cluster' do
@@ -74,13 +53,6 @@ describe Projects::ClustersController do
expect(response).to render_template(:index, partial: :empty_state)
expect(assigns(:clusters)).to eq([])
end
-
- it 'assigns counters to zero' do
- go
-
- expect(assigns(:active_count)).to eq(0)
- expect(assigns(:inactive_count)).to eq(0)
- end
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index c459d732507..73fb90d73ec 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.team << [user, :master]
+ project.add_master(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 fe5818da0bc..046ce027965 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::CompareController do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
it 'compare shows some diffs' do
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 6fae52edbad..7c708a418a7 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.team << [user, :master]
+ project.add_master(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 c3208357694..97db69427e9 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -48,7 +48,7 @@ describe Projects::DeployKeysController do
end
before do
- project2.team << [user, :developer]
+ project2.add_developer(user)
end
it 'returns json in a correct format' do
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index 3164fd5c143..73e7921fab7 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index 3bf676637a2..00328d3ea51 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -31,7 +31,7 @@ describe Projects::DiscussionsController do
context "when the user is authorized to resolve the discussion" do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context "when the discussion is not resolvable" do
@@ -92,7 +92,7 @@ describe Projects::DiscussionsController do
context "when the user is authorized to resolve the discussion" do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context "when the discussion is not resolvable" do
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index 6a5433bcc9c..505fe82851a 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.team << [user, :master]
+ project.add_master(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 1bedb8ebdff..c4b32dc3a09 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -51,7 +51,7 @@ describe Projects::ForksController do
context 'when user is a member of the Project' do
before do
- forked_project.team << [project.creator, :developer]
+ forked_project.add_developer(project.creator)
end
it 'sees the project listed' do
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index 5af03ae118c..c3605555fe7 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.team << [user, :master]
+ project.add_master(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 f8c792cd0f0..5bfc3d31401 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index 07174660f46..aba70c6d4c1 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 2a5ec6d584b..7fb4c1b7425 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -9,7 +9,7 @@ describe Projects::ImportsController do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
it 'renders template' do
@@ -30,7 +30,7 @@ describe Projects::ImportsController do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
context 'when import is in progress' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 4dbbaecdd6d..6b7db947216 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -37,7 +37,7 @@ describe Projects::IssuesController do
context 'internal issue tracker' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it_behaves_like "issuables list meta-data", :issue
@@ -69,7 +69,7 @@ describe Projects::IssuesController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
@@ -116,7 +116,7 @@ describe Projects::IssuesController do
context 'internal issue tracker' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'builds a new issue' do
@@ -127,7 +127,7 @@ describe Projects::IssuesController do
it 'fills in an issue for a merge request' do
project_with_repository = create(:project, :repository)
- project_with_repository.team << [user, :developer]
+ project_with_repository.add_developer(user)
mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_to_resolve_discussions_of: mr.iid
@@ -153,7 +153,7 @@ describe Projects::IssuesController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
external = double
allow(project).to receive(:external_issue_tracker).and_return(external)
@@ -272,6 +272,20 @@ describe Projects::IssuesController do
expect(response).to have_http_status(:ok)
expect(issue.reload.title).to eq('New title')
end
+
+ context 'when Akismet is enabled and the issue is identified as spam' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ end
+
+ it 'renders json with recaptcha_html' do
+ subject
+
+ expect(JSON.parse(response.body)).to have_key('recaptcha_html')
+ end
+ end
end
context 'when user does not have access to update issue' do
@@ -315,7 +329,7 @@ describe Projects::IssuesController do
it 'does not list confidential issues for project members with guest role' do
sign_in(member)
- project.team << [member, :guest]
+ project.add_guest(member)
get_issues
@@ -340,7 +354,7 @@ describe Projects::IssuesController do
it 'lists confidential issues for project members' do
sign_in(member)
- project.team << [member, :developer]
+ project.add_developer(member)
get_issues
@@ -380,7 +394,7 @@ describe Projects::IssuesController do
it 'returns 404 for project members with guest role' do
sign_in(member)
- project.team << [member, :guest]
+ project.add_guest(member)
go(id: unescaped_parameter_value.to_param)
expect(response).to have_gitlab_http_status :not_found
@@ -402,7 +416,7 @@ describe Projects::IssuesController do
it "returns #{http_status[:success]} for project members" do
sign_in(member)
- project.team << [member, :developer]
+ project.add_developer(member)
go(id: unescaped_parameter_value.to_param)
expect(response).to have_gitlab_http_status http_status[:success]
@@ -436,7 +450,7 @@ describe Projects::IssuesController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it_behaves_like 'restricted action', success: 200
@@ -504,17 +518,16 @@ describe Projects::IssuesController do
expect(spam_logs.first.recaptcha_verified).to be_falsey
end
- it 'renders json errors' do
+ it 'renders recaptcha_html json response' do
update_issue
- expect(json_response)
- .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
+ expect(json_response).to have_key('recaptcha_html')
end
- it 'returns 422 status' do
+ it 'returns 200 status' do
update_issue
- expect(response).to have_gitlab_http_status(422)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -581,7 +594,7 @@ describe Projects::IssuesController do
let(:deleted_user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
issue.update!(last_edited_by: deleted_user, last_edited_at: Time.now)
@@ -625,7 +638,7 @@ describe Projects::IssuesController do
def post_new_issue(issue_attrs = {}, additional_params = {})
sign_in(user)
project = create(:project, :public)
- project.team << [user, :developer]
+ project.add_developer(user)
post :create, {
namespace_id: project.namespace.to_param,
@@ -642,7 +655,7 @@ describe Projects::IssuesController do
let(:project) { merge_request.source_project }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
@@ -816,7 +829,7 @@ describe Projects::IssuesController do
def post_spam
admin = create(:admin)
create(:user_agent_detail, subject: issue)
- project.team << [admin, :master]
+ project.add_master(admin)
sign_in(admin)
post :mark_as_spam, {
namespace_id: project.namespace,
@@ -861,7 +874,7 @@ describe Projects::IssuesController do
end
it 'delegates the update of the todos count cache to TodoService' do
- expect_any_instance_of(TodoService).to receive(:destroy_issuable).with(issue, owner).once
+ expect_any_instance_of(TodoService).to receive(:destroy_target).with(issue).once
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 7490f8fefce..e6a4e7c8257 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -374,7 +374,7 @@ describe Projects::JobsController do
let(:role) { :master }
before do
- project.team << [user, role]
+ project.add_role(user, role)
sign_in(user)
post_erase
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index cf83f2f3265..452d7e23983 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 33d48ff94d1..c5ac0be27bb 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index 7fdddc02fd3..7e2366847f4 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::MergeRequests::CreationsController do
let(:fork_project) { create(:forked_project_with_submodules) }
before do
- fork_project.team << [user, :master]
+ fork_project.add_master(user)
sign_in(user)
end
@@ -86,7 +86,7 @@ describe Projects::MergeRequests::CreationsController do
let(:other_project) { create(:project, :repository) }
before do
- other_project.team << [user, :master]
+ other_project.add_master(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 ba97ccfbbd4..5d297c654bf 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -151,7 +151,7 @@ describe Projects::MergeRequests::DiffsController do
let(:other_project) { create(:project) }
before do
- other_project.team << [user, :master]
+ other_project.add_master(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 51d5d6a52b3..c8cc6b374f6 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -91,11 +91,19 @@ describe Projects::MergeRequestsController do
end
end
- context 'without basic serializer param' do
- it 'renders the merge request in the json format' do
- go(format: :json)
+ context 'with widget serializer param' do
+ it 'renders widget MR entity as json' do
+ go(serializer: 'widget', format: :json)
- expect(response).to match_response_schema('entities/merge_request')
+ expect(response).to match_response_schema('entities/merge_request_widget')
+ end
+ end
+
+ context 'when no serialiser was passed' do
+ it 'renders widget MR entity as json' do
+ go(serializer: nil, format: :json)
+
+ expect(response).to match_response_schema('entities/merge_request_widget')
end
end
end
@@ -468,7 +476,7 @@ describe Projects::MergeRequestsController do
end
it 'delegates the update of the todos count cache to TodoService' do
- expect_any_instance_of(TodoService).to receive(:destroy_issuable).with(merge_request, owner).once
+ expect_any_instance_of(TodoService).to receive(:destroy_target).with(merge_request).once
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
end
@@ -676,4 +684,62 @@ describe Projects::MergeRequestsController do
format: :json
end
end
+
+ describe 'POST #rebase' do
+ let(:viewer) { user }
+
+ def post_rebase
+ post :rebase, namespace_id: project.namespace, project_id: project, id: merge_request
+ end
+
+ def expect_rebase_worker_for(user)
+ expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id)
+ end
+
+ context 'successfully' do
+ it 'enqeues a RebaseWorker' do
+ expect_rebase_worker_for(viewer)
+
+ post_rebase
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'with a forked project' do
+ let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:fork_owner) { fork_project.owner }
+
+ before do
+ merge_request.update!(source_project: fork_project)
+ fork_project.add_reporter(user)
+ end
+
+ context 'user cannot push to source branch' do
+ it 'returns 404' do
+ expect_rebase_worker_for(viewer).never
+
+ post_rebase
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'user can push to source branch' do
+ before do
+ project.add_reporter(fork_owner)
+
+ sign_in(fork_owner)
+ end
+
+ it 'returns 200' do
+ expect_rebase_worker_for(fork_owner)
+
+ post_rebase
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 209979e642d..00cf464ec5b 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.team << [user, :master]
+ project.add_master(user)
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 37e9f863fc4..de132dfaa21 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -32,7 +32,7 @@ describe Projects::NotesController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'passes last_fetched_at from headers to NotesFinder' do
@@ -351,7 +351,7 @@ describe Projects::NotesController do
before do
sign_in(note.author)
- project.team << [note.author, :developer]
+ project.add_developer(note.author)
end
it "updates the note" do
@@ -372,7 +372,7 @@ describe Projects::NotesController do
context 'user is the author of a note' do
before do
sign_in(note.author)
- project.team << [note.author, :developer]
+ project.add_developer(note.author)
end
it "returns status 200 for html" do
@@ -389,7 +389,7 @@ describe Projects::NotesController do
context 'user is not the author of a note' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it "returns status 404" do
@@ -403,7 +403,7 @@ describe Projects::NotesController do
describe 'POST toggle_award_emoji' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it "toggles the award emoji" do
@@ -445,7 +445,7 @@ describe Projects::NotesController do
context "when the user is authorized to resolve the note" do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context "when the note is not resolvable" do
@@ -506,7 +506,7 @@ describe Projects::NotesController do
context "when the user is authorized to resolve the note" do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context "when the note is not resolvable" do
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 4e52e261920..966ffdf6996 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -3,10 +3,12 @@ require 'spec_helper'
describe Projects::PipelineSchedulesController do
include AccessMatchersForController
- set(:project) { create(:project, :public) }
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+ set(:project) { create(:project, :public, :repository) }
+ set(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
describe 'GET #index' do
+ render_views
+
let(:scope) { nil }
let!(:inactive_pipeline_schedule) do
create(:ci_pipeline_schedule, :inactive, project: project)
@@ -96,7 +98,7 @@ describe Projects::PipelineSchedulesController do
end
end
- context 'when variables_attributes has two variables and duplicted' do
+ context 'when variables_attributes has two variables and duplicated' do
let(:schedule) do
basic_param.merge({
variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }]
@@ -364,6 +366,65 @@ describe Projects::PipelineSchedulesController do
end
end
+ describe 'POST #play', :clean_gitlab_redis_cache do
+ set(:user) { create(:user) }
+ let(:ref) { 'master' }
+
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+ end
+
+ context 'when an anonymous user makes the request' do
+ before do
+ sign_out(user)
+ end
+
+ it 'does not allow pipeline to be executed' do
+ expect(RunPipelineScheduleWorker).not_to receive(:perform_async)
+
+ post :play, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when a developer makes the request' do
+ it 'executes a new pipeline' do
+ expect(RunPipelineScheduleWorker).to receive(:perform_async).with(pipeline_schedule.id, user.id).and_return('job-123')
+
+ post :play, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
+
+ expect(flash[:notice]).to start_with 'Successfully scheduled a pipeline to run'
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'prevents users from scheduling the same pipeline repeatedly' do
+ 2.times do
+ post :play, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
+ end
+
+ expect(flash.to_a.size).to eq(2)
+ expect(flash[:alert]).to eq 'You cannot play this scheduled pipeline at the moment. Please wait a minute.'
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when a developer attempts to schedule a protected ref' do
+ it 'does not allow pipeline to be executed' do
+ create(:protected_branch, project: project, name: ref)
+ protected_schedule = create(:ci_pipeline_schedule, project: project, ref: ref)
+
+ expect(RunPipelineScheduleWorker).not_to receive(:perform_async)
+
+ post :play, namespace_id: project.namespace.to_param, project_id: project, id: protected_schedule.id
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
describe 'DELETE #destroy' do
set(:user) { create(:user) }
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 1604a2da485..35ac999cc65 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -17,13 +17,10 @@ describe Projects::PipelinesController do
describe 'GET index.json' do
before do
- branch_head = project.commit
- parent = branch_head.parent
-
- create(:ci_empty_pipeline, status: 'pending', project: project, sha: branch_head.id)
- create(:ci_empty_pipeline, status: 'running', project: project, sha: branch_head.id)
- create(:ci_empty_pipeline, status: 'created', project: project, sha: parent.id)
- create(:ci_empty_pipeline, status: 'success', project: project, sha: parent.id)
+ %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)
+ end
end
subject do
@@ -46,7 +43,7 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do
- expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8)
+ expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(3)
end
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index a34dc27a5ed..46b08a03b19 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -21,7 +21,7 @@ describe Projects::ProjectMembersController do
context 'when user does not have enough rights' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'returns 404' do
@@ -37,7 +37,7 @@ describe Projects::ProjectMembersController do
context 'when user has enough rights' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'adds user to members' do
@@ -66,6 +66,26 @@ describe Projects::ProjectMembersController do
end
end
+ describe 'PUT update' do
+ let(:requester) { create(:project_member, :access_request, project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ Gitlab::Access.options.each do |label, value|
+ it "can change the access level to #{label}" do
+ xhr :put, :update, project_member: { access_level: value },
+ namespace_id: project.namespace,
+ project_id: project,
+ id: requester
+
+ expect(requester.reload.human_access).to eq(label)
+ end
+ end
+ end
+
describe 'DELETE destroy' do
let(:member) { create(:project_member, :developer, project: project) }
@@ -86,7 +106,7 @@ describe Projects::ProjectMembersController do
context 'when member is found' do
context 'when user does not have enough rights' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'returns 404' do
@@ -101,7 +121,7 @@ describe Projects::ProjectMembersController do
context 'when user has enough rights' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it '[HTML] removes user from members' do
@@ -144,7 +164,7 @@ describe Projects::ProjectMembersController do
context 'when member is found' do
context 'and is not an owner' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'removes user from members' do
@@ -161,7 +181,7 @@ describe Projects::ProjectMembersController do
let(:project) { create(:project, namespace: user.namespace) }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'cannot remove himself from the project' do
@@ -228,7 +248,7 @@ describe Projects::ProjectMembersController do
context 'when member is found' do
context 'when user does not have enough rights' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'returns 404' do
@@ -243,7 +263,7 @@ describe Projects::ProjectMembersController do
context 'when user has enough rights' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'adds user to members' do
@@ -265,8 +285,8 @@ describe Projects::ProjectMembersController do
let(:member) { create(:user) }
before do
- project.team << [user, :master]
- another_project.team << [member, :guest]
+ project.add_master(user)
+ another_project.add_guest(member)
sign_in(user)
end
@@ -280,7 +300,7 @@ describe Projects::ProjectMembersController do
context 'when user can access source project members' do
before do
- another_project.team << [user, :guest]
+ another_project.add_guest(user)
end
include_context 'import applied'
@@ -312,7 +332,7 @@ describe Projects::ProjectMembersController do
context 'when creating owner' do
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -328,7 +348,7 @@ describe Projects::ProjectMembersController do
context 'when create master' do
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 748ae040928..ceaffd92623 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::RefsController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe 'GET #logs_tree' do
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index 358f26dfb02..fc1619acec6 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::ReleasesController do
let!(:tag) { release.tag }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 8b777eb68ca..04d16e98913 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -17,7 +17,7 @@ describe Projects::RepositoriesController do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index a907da2b60f..847ac6f2be0 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.team << [user, :master]
+ project.add_master(user)
end
describe '#test' do
@@ -114,5 +114,41 @@ describe Projects::ServicesController do
expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.'
end
end
+
+ context 'with a deprecated service' do
+ let(:service) { create(:kubernetes_service, project: project) }
+
+ before do
+ put :update,
+ namespace_id: project.namespace, project_id: project, id: service.to_param, service: { namespace: 'updated_namespace' }
+ end
+
+ it 'should not update the service' do
+ service.reload
+ expect(service.namespace).not_to eq('updated_namespace')
+ end
+ end
+ end
+
+ describe "GET #edit" do
+ before do
+ get :edit, namespace_id: project.namespace, project_id: project, id: service_id
+ end
+
+ context 'with approved services' do
+ let(:service_id) { 'jira' }
+
+ it 'should render edit page' do
+ expect(response).to be_success
+ end
+ end
+
+ context 'with a deprecated service' do
+ let(:service_id) { 'kubernetes' }
+
+ it 'should render edit page' do
+ expect(response).to be_success
+ end
+ 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 b8fe0f46f57..0202149f335 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::Settings::CiCdController do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -17,4 +17,51 @@ describe Projects::Settings::CiCdController do
expect(response).to render_template(:show)
end
end
+
+ describe '#reset_cache' do
+ before do
+ sign_in(user)
+
+ project.add_master(user)
+
+ allow(ResetProjectCacheService).to receive_message_chain(:new, :execute).and_return(true)
+ end
+
+ subject { post :reset_cache, namespace_id: project.namespace, project_id: project }
+
+ it 'calls reset project cache service' do
+ expect(ResetProjectCacheService).to receive_message_chain(:new, :execute)
+
+ subject
+ end
+
+ it 'redirects to project pipelines path' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to(project_pipelines_path(project))
+ end
+
+ context 'when service returns successfully' do
+ it 'sets the flash notice variable' do
+ subject
+
+ expect(controller).to set_flash[:notice]
+ expect(controller).not_to set_flash[:error]
+ end
+ end
+
+ context 'when service does not return successfully' do
+ before do
+ allow(ResetProjectCacheService).to receive_message_chain(:new, :execute).and_return(false)
+ end
+
+ it 'sets the flash error variable' do
+ subject
+
+ expect(controller).not_to set_flash[:notice]
+ expect(controller).to set_flash[:error]
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index 3068837f394..77df9a6f567 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 70e7f9ca96e..8fcfa3c9ecd 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::TemplatesController do
let(:body) { JSON.parse(response.body) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index 4622e27e60f..e2524be7724 100644
--- a/spec/controllers/projects/todos_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -20,7 +20,7 @@ describe Projects::TodosController do
context 'when authorized' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'creates todo for issue' do
@@ -88,7 +88,7 @@ describe Projects::TodosController do
context 'when authorized' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'creates todo for merge request' do
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 65b821c9486..d3188f054cf 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.team << [user, :master]
+ project.add_master(user)
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index d065cd00d00..9fde6544215 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.team << [user, :master]
+ project.add_master(user)
end
describe 'POST #create' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e61187fb518..5202ffdd8bb 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -102,7 +102,7 @@ describe ProjectsController do
render_views
before do
- project.team << [user, :developer]
+ project.add_developer(user)
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
end
@@ -437,7 +437,7 @@ describe ProjectsController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
end
@@ -465,7 +465,7 @@ describe ProjectsController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 7e42e43345c..b1f601a19e5 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -265,13 +265,13 @@ describe UploadsController do
context "when the user has access to the project" do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
context "when the user is blocked" do
before do
user.block
- project.team << [user, :master]
+ project.add_master(user)
end
it "redirects to the sign in page" do
@@ -465,13 +465,13 @@ describe UploadsController do
context "when the user has access to the project" do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
context "when the user is blocked" do
before do
user.block
- project.team << [user, :master]
+ project.add_master(user)
end
it "redirects to the sign in page" do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 01ab59aa363..2898c4b119e 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -91,7 +91,7 @@ describe UsersController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
push_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@@ -117,7 +117,7 @@ describe UsersController do
allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id])
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'assigns @calendar_date' do
diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb
index 8f6422a7825..021971ac421 100644
--- a/spec/factories/abuse_reports.rb
+++ b/spec/factories/abuse_reports.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :abuse_report do
reporter factory: :user
user
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
index 860973024c9..5f9c57c0c8d 100644
--- a/spec/factories/appearances.rb
+++ b/spec/factories/appearances.rb
@@ -1,6 +1,6 @@
-# Read about factories at https://github.com/thoughtbot/factory_girl
+# Read about factories at https://github.com/thoughtbot/factory_bot
-FactoryGirl.define do
+FactoryBot.define do
factory :appearance do
title "MepMep"
description "This is my Community Edition instance"
diff --git a/spec/factories/application_settings.rb b/spec/factories/application_settings.rb
index aef65e724c2..3ecc90b6573 100644
--- a/spec/factories/application_settings.rb
+++ b/spec/factories/application_settings.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :application_setting do
end
end
diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb
index 4b858df52c9..a0abbbce686 100644
--- a/spec/factories/award_emoji.rb
+++ b/spec/factories/award_emoji.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :award_emoji do
name "thumbsup"
user
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index 1ec042a6ab4..1e125237ae8 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :board do
project
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index c2fdf89213a..9a65e7f8a3f 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :broadcast_message do
message "MyText"
starts_at 1.day.ago
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
index 9a0be1a4598..56993e5da18 100644
--- a/spec/factories/chat_names.rb
+++ b/spec/factories/chat_names.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :chat_name, class: ChatName do
user factory: :user
service factory: :service
diff --git a/spec/factories/chat_teams.rb b/spec/factories/chat_teams.rb
index ffedf69a69b..d048c46d6b6 100644
--- a/spec/factories/chat_teams.rb
+++ b/spec/factories/chat_teams.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :chat_team, class: ChatTeam do
sequence(:team_id) { |n| "abcdefghijklm#{n}" }
namespace factory: :group
diff --git a/spec/factories/ci/build_trace_section_names.rb b/spec/factories/ci/build_trace_section_names.rb
index 1c16225f0e5..ce07e716dde 100644
--- a/spec/factories/ci/build_trace_section_names.rb
+++ b/spec/factories/ci/build_trace_section_names.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_build_trace_section_name, class: Ci::BuildTraceSectionName do
sequence(:name) { |n| "section_#{n}" }
project factory: :project
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index c868525cbc0..6f66468570f 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -1,18 +1,16 @@
include ActionDispatch::TestProcess
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_build, class: Ci::Build do
name 'test'
stage 'test'
stage_idx 0
ref 'master'
tag false
- status 'pending'
- created_at 'Di 29. Okt 09:50:00 CET 2013'
- started_at 'Di 29. Okt 09:51:28 CET 2013'
- finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
protected false
+ created_at 'Di 29. Okt 09:50:00 CET 2013'
+ pending
options do
{
@@ -29,23 +27,37 @@ FactoryGirl.define do
pipeline factory: :ci_pipeline
+ trait :started do
+ started_at 'Di 29. Okt 09:51:28 CET 2013'
+ end
+
+ trait :finished do
+ started
+ finished_at 'Di 29. Okt 09:53:28 CET 2013'
+ end
+
trait :success do
+ finished
status 'success'
end
trait :failed do
+ finished
status 'failed'
end
trait :canceled do
+ finished
status 'canceled'
end
trait :skipped do
+ started
status 'skipped'
end
trait :running do
+ started
status 'running'
end
@@ -114,11 +126,6 @@ FactoryGirl.define do
build.project ||= build.pipeline.project
end
- factory :ci_not_started_build do
- started_at nil
- finished_at nil
- end
-
trait :tag do
tag true
end
diff --git a/spec/factories/ci/group_variables.rb b/spec/factories/ci/group_variables.rb
index 565ced9eb1a..64716842b12 100644
--- a/spec/factories/ci/group_variables.rb
+++ b/spec/factories/ci/group_variables.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_group_variable, class: Ci::GroupVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 39185249695..436735e7ed3 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -1,6 +1,6 @@
include ActionDispatch::TestProcess
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_job_artifact, class: Ci::JobArtifact do
job factory: :ci_build
file_type :archive
diff --git a/spec/factories/ci/pipeline_schedule.rb b/spec/factories/ci/pipeline_schedule.rb
index 564fef6833b..b2b79807429 100644
--- a/spec/factories/ci/pipeline_schedule.rb
+++ b/spec/factories/ci/pipeline_schedule.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_pipeline_schedule, class: Ci::PipelineSchedule do
cron '0 1 * * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
diff --git a/spec/factories/ci/pipeline_schedule_variables.rb b/spec/factories/ci/pipeline_schedule_variables.rb
index ca64d1aada0..8d29118e310 100644
--- a/spec/factories/ci/pipeline_schedule_variables.rb
+++ b/spec/factories/ci/pipeline_schedule_variables.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_pipeline_schedule_variable, class: Ci::PipelineScheduleVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
diff --git a/spec/factories/ci/pipeline_variables.rb b/spec/factories/ci/pipeline_variables.rb
index 7c1a7faec08..b18055d7b3a 100644
--- a/spec/factories/ci/pipeline_variables.rb
+++ b/spec/factories/ci/pipeline_variables.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_pipeline_variable, class: Ci::PipelineVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index f994c2df821..51a767e5b93 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_empty_pipeline, class: Ci::Pipeline do
source :push
ref 'master'
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index fa76d0ecd8c..f605e90ceed 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner factory: :ci_runner
project
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 88bb755d068..34b8b246d0f 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_runner, class: Ci::Runner do
sequence(:description) { |n| "My runner#{n}" }
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index b2ded945738..25309033571 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_stage, class: Ci::LegacyStage do
skip_create
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
index 40b8848920e..0e9fc3d0014 100644
--- a/spec/factories/ci/trigger_requests.rb
+++ b/spec/factories/ci/trigger_requests.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_trigger_request, class: Ci::TriggerRequest do
trigger factory: :ci_trigger
end
diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb
index 3734c7040c0..742d9efba2d 100644
--- a/spec/factories/ci/triggers.rb
+++ b/spec/factories/ci/triggers.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_trigger_without_token, class: Ci::Trigger do
owner
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index d8fd513ffcf..3d014b9b54f 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index fab37195113..775fbb3d27b 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -1,5 +1,5 @@
-FactoryGirl.define do
- factory :cluster_applications_helm, class: Clusters::Applications::Helm do
+FactoryBot.define do
+ factory :clusters_applications_helm, class: Clusters::Applications::Helm do
cluster factory: %i(cluster provided_by_gcp)
trait :not_installable do
@@ -31,5 +31,8 @@ FactoryGirl.define do
installing
updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
end
+
+ factory :clusters_applications_ingress, class: Clusters::Applications::Ingress
+ factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus
end
end
diff --git a/spec/factories/clusters/applications/ingress.rb b/spec/factories/clusters/applications/ingress.rb
deleted file mode 100644
index b103a980655..00000000000
--- a/spec/factories/clusters/applications/ingress.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-FactoryGirl.define do
- factory :cluster_applications_ingress, class: Clusters::Applications::Ingress do
- cluster factory: %i(cluster provided_by_gcp)
-
- trait :not_installable do
- status(-2)
- end
-
- trait :installable do
- status 0
- end
-
- trait :scheduled do
- status 1
- end
-
- trait :installing do
- status 2
- end
-
- trait :installed do
- status 3
- end
-
- trait :errored do
- status(-1)
- status_reason 'something went wrong'
- end
-
- trait :timeouted do
- installing
- updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
- end
- end
-end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 9e73a19e856..20d5580f0c2 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :cluster, class: Clusters::Cluster do
user
name 'test-cluster'
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
index 8b3e6ff35fa..89f6ddebf6a 100644
--- a/spec/factories/clusters/platforms/kubernetes.rb
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :cluster_platform_kubernetes, class: Clusters::Platforms::Kubernetes do
cluster
namespace nil
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
index a815410512a..a002ab28519 100644
--- a/spec/factories/clusters/providers/gcp.rb
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :cluster_provider_gcp, class: Clusters::Providers::Gcp do
cluster
gcp_project_id 'test-gcp-project'
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index abbe37df90e..ce5fbc343ee 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :commit_status, class: CommitStatus do
name 'default'
stage 'test'
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index f4f12a095fc..84a8bc56640 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -1,16 +1,29 @@
require_relative '../support/repo_helpers'
-FactoryGirl.define do
+FactoryBot.define do
factory :commit do
- git_commit RepoHelpers.sample_commit
+ transient do
+ author nil
+ end
+
+ git_commit do
+ commit = RepoHelpers.sample_commit
+
+ if author
+ commit.author_email = author.email
+ commit.author_name = author.name
+ end
+
+ commit
+ end
project
initialize_with do
new(git_commit, project)
end
- after(:build) do |commit|
- allow(commit).to receive(:author).and_return build(:author)
+ after(:build) do |commit, evaluator|
+ allow(commit).to receive(:author).and_return(evaluator.author || build(:author))
end
trait :without_author do
diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb
index 3fcad9fd4b3..62a89a12ef5 100644
--- a/spec/factories/container_repositories.rb
+++ b/spec/factories/container_repositories.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :container_repository do
name 'test_container_image'
project
diff --git a/spec/factories/conversational_development_index_metrics.rb b/spec/factories/conversational_development_index_metrics.rb
index 3806c43ba15..abf37fb861e 100644
--- a/spec/factories/conversational_development_index_metrics.rb
+++ b/spec/factories/conversational_development_index_metrics.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :conversational_development_index_metric, class: ConversationalDevelopmentIndex::Metric do
leader_issues 9.256
instance_issues 1.234
diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb
index 27cece487bd..30a6d468ed3 100644
--- a/spec/factories/deploy_keys_projects.rb
+++ b/spec/factories/deploy_keys_projects.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :deploy_keys_project do
deploy_key
project
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 0dd1238d6e2..9d7d5e56611 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :deployment, class: Deployment do
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb
index c9ab87a15aa..4dc7961060a 100644
--- a/spec/factories/emails.rb
+++ b/spec/factories/emails.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :email do
user
email { generate(:email_alias) }
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 9034476d094..b5db57d5148 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :environment, class: Environment do
sequence(:name) { |n| "environment#{n}" }
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index ad9f7e2caef..ed275243ac9 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :event do
project
author factory: :user
diff --git a/spec/factories/file_uploaders.rb b/spec/factories/file_uploaders.rb
index 622571390d2..8404985bfea 100644
--- a/spec/factories/file_uploaders.rb
+++ b/spec/factories/file_uploaders.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :file_uploader do
skip_create
diff --git a/spec/factories/fork_network_members.rb b/spec/factories/fork_network_members.rb
index 509c4e1fa1c..850e3f158f1 100644
--- a/spec/factories/fork_network_members.rb
+++ b/spec/factories/fork_network_members.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :fork_network_member do
association :project
association :fork_network
diff --git a/spec/factories/fork_networks.rb b/spec/factories/fork_networks.rb
index f42d36f3d19..813b1943eb2 100644
--- a/spec/factories/fork_networks.rb
+++ b/spec/factories/fork_networks.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :fork_network do
association :root_project, factory: :project
end
diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb
index 9b34651a4ae..bc59fea81ec 100644
--- a/spec/factories/forked_project_links.rb
+++ b/spec/factories/forked_project_links.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :forked_project_link do
association :forked_to_project, factory: [:project, :repository]
association :forked_from_project, factory: [:project, :repository]
diff --git a/spec/factories/gitaly/commit.rb b/spec/factories/gitaly/commit.rb
index e7966cee77b..5034c3d0e48 100644
--- a/spec/factories/gitaly/commit.rb
+++ b/spec/factories/gitaly/commit.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
sequence(:gitaly_commit_id) { Digest::SHA1.hexdigest(Time.now.to_f.to_s) }
factory :gitaly_commit, class: Gitaly::GitCommit do
diff --git a/spec/factories/gitaly/commit_author.rb b/spec/factories/gitaly/commit_author.rb
index 341873a2002..aaf634ce08b 100644
--- a/spec/factories/gitaly/commit_author.rb
+++ b/spec/factories/gitaly/commit_author.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :gitaly_commit_author, class: Gitaly::CommitAuthor do
skip_create
diff --git a/spec/factories/gpg_key_subkeys.rb b/spec/factories/gpg_key_subkeys.rb
index 66ecb44d84b..57eaaee345f 100644
--- a/spec/factories/gpg_key_subkeys.rb
+++ b/spec/factories/gpg_key_subkeys.rb
@@ -1,6 +1,6 @@
require_relative '../support/gpg_helpers'
-FactoryGirl.define do
+FactoryBot.define do
factory :gpg_key_subkey do
gpg_key
diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb
index 93218e5763e..b8aabf74221 100644
--- a/spec/factories/gpg_keys.rb
+++ b/spec/factories/gpg_keys.rb
@@ -1,6 +1,6 @@
require_relative '../support/gpg_helpers'
-FactoryGirl.define do
+FactoryBot.define do
factory :gpg_key do
key GpgHelpers::User1.public_key
user
diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb
index e9798ff6a41..4620caff823 100644
--- a/spec/factories/gpg_signature.rb
+++ b/spec/factories/gpg_signature.rb
@@ -1,6 +1,6 @@
require_relative '../support/gpg_helpers'
-FactoryGirl.define do
+FactoryBot.define do
factory :gpg_signature do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
project
diff --git a/spec/factories/group_custom_attributes.rb b/spec/factories/group_custom_attributes.rb
index 7ff5f376e8b..d2f45d5d3ce 100644
--- a/spec/factories/group_custom_attributes.rb
+++ b/spec/factories/group_custom_attributes.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :group_custom_attribute do
group
sequence(:key) { |n| "key#{n}" }
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index 32cbfe28a60..1c2214e9481 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :group_member do
access_level { GroupMember::OWNER }
group
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 52f76b094a3..1512f5a0e58 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :group, class: Group, parent: :namespace do
sequence(:name) { |n| "group#{n}" }
path { name.downcase.gsub(/\s/, '_') }
diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb
index 26ef6f18698..122d0d42938 100644
--- a/spec/factories/identities.rb
+++ b/spec/factories/identities.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :identity do
provider 'ldapmain'
extern_uid 'my-ldap-id'
diff --git a/spec/factories/instance_configuration.rb b/spec/factories/instance_configuration.rb
index 406c7c3caf1..31866a9c221 100644
--- a/spec/factories/instance_configuration.rb
+++ b/spec/factories/instance_configuration.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :instance_configuration do
skip_create
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 7c3b80198f9..71dc169c6a2 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :issue do
title { generate(:title) }
author
@@ -14,6 +14,7 @@ FactoryGirl.define do
trait :closed do
state :closed
+ closed_at { Time.now }
end
factory :closed_issue, traits: [:closed]
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index 3f7c794b14a..552b4b7e06e 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -1,6 +1,6 @@
require_relative '../support/helpers/key_generator_helper'
-FactoryGirl.define do
+FactoryBot.define do
factory :key do
title
key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' }
@@ -21,12 +21,14 @@ FactoryGirl.define do
factory :rsa_key_2048 do
key do
- 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9' \
- '6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5' \
- '/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7' \
- 'M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC' \
- 'rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0' \
- '5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com'
+ <<~KEY.delete("\n")
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
+ 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
+ /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
+ M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
+ rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
+ 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
+ KEY
end
factory :rsa_deploy_key_2048, class: 'DeployKey'
@@ -34,37 +36,44 @@ FactoryGirl.define do
factory :dsa_key_2048 do
key do
- 'ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G' \
- 'Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp' \
- 'YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ' \
- '/pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz' \
- 'OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv' \
- '5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB' \
- 'AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t' \
- 'poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1' \
- 'M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH' \
- 'MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H' \
- 'nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A' \
- '1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb' \
- 'aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI' \
- 'zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex' \
- 'PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z' \
- 'wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS' \
- 'Taja+Cf9kMo== dummy@gitlab.com'
+ <<~KEY.delete("\n")
+ ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G
+ Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp
+ YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ
+ /pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz
+ OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv
+ 5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB
+ AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t
+ poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1
+ M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH
+ MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H
+ nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A
+ 1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb
+ aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI
+ zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex
+ PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z
+ wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS
+ Taja+Cf9kMo== dummy@gitlab.com
+ KEY
end
end
factory :ecdsa_key_256 do
key do
- 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA' \
- 'AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO' \
- 'Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com'
+ <<~KEY.delete("\n")
+ ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA
+ AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO
+ Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com
+ KEY
end
end
factory :ed25519_key_256 do
key do
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJOhDRvf59ohL6 dummy@gitlab.com'
+ <<~KEY.delete("\n")
+ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJ
+ OhDRvf59ohL6 dummy@gitlab.com
+ KEY
end
end
end
diff --git a/spec/factories/label_links.rb b/spec/factories/label_links.rb
index 3580174e873..007847d9cf4 100644
--- a/spec/factories/label_links.rb
+++ b/spec/factories/label_links.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :label_link do
label
target factory: :issue
diff --git a/spec/factories/label_priorities.rb b/spec/factories/label_priorities.rb
index 7430466fc57..c4824faad53 100644
--- a/spec/factories/label_priorities.rb
+++ b/spec/factories/label_priorities.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :label_priority do
project
label
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index 416317d677b..f759b6d499d 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
trait :base_label do
title { generate(:label_title) }
color "#990000"
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index 477fab9e964..8eb709022ce 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -1,6 +1,6 @@
include ActionDispatch::TestProcess
-FactoryGirl.define do
+FactoryBot.define do
factory :lfs_object do
sequence(:oid) { |n| "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a%05x" % n }
size 499013
diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb
index 1ed0355c8e4..c225387a5de 100644
--- a/spec/factories/lfs_objects_projects.rb
+++ b/spec/factories/lfs_objects_projects.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :lfs_objects_project do
lfs_object
project
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
index 48142d3c49b..210c58b21e9 100644
--- a/spec/factories/lists.rb
+++ b/spec/factories/lists.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :list do
board
label
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index cc6cef63b47..40558c88d15 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :merge_request do
title { generate(:title) }
author
diff --git a/spec/factories/merge_requests_closing_issues.rb b/spec/factories/merge_requests_closing_issues.rb
index fdbdc00cad7..ee0606a72b6 100644
--- a/spec/factories/merge_requests_closing_issues.rb
+++ b/spec/factories/merge_requests_closing_issues.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :merge_requests_closing_issues do
issue
merge_request
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index b5298b2f969..f95632e7187 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :milestone do
title
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb
index 1b1fc4ce80d..f94b09cff15 100644
--- a/spec/factories/namespaces.rb
+++ b/spec/factories/namespaces.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :namespace do
sequence(:name) { |n| "namespace#{n}" }
path { name.downcase.gsub(/\s/, '_') }
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 471bfb3213a..707ecbd6be5 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -2,7 +2,7 @@ require_relative '../support/repo_helpers'
include ActionDispatch::TestProcess
-FactoryGirl.define do
+FactoryBot.define do
factory :note do
project
note { generate(:title) }
diff --git a/spec/factories/notification_settings.rb b/spec/factories/notification_settings.rb
index e9171528d86..5116ef33f5d 100644
--- a/spec/factories/notification_settings.rb
+++ b/spec/factories/notification_settings.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :notification_setting do
source factory: :project
user
diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb
index 543b3e99274..9e6af24c4eb 100644
--- a/spec/factories/oauth_access_grants.rb
+++ b/spec/factories/oauth_access_grants.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :oauth_access_grant do
resource_owner_id { create(:user).id }
application
diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb
index a46bc1d8ce8..eabfd6cd830 100644
--- a/spec/factories/oauth_access_tokens.rb
+++ b/spec/factories/oauth_access_tokens.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :oauth_access_token do
resource_owner
application
diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb
index c7ede40f240..4427da1d6c7 100644
--- a/spec/factories/oauth_applications.rb
+++ b/spec/factories/oauth_applications.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
sequence(:name) { |n| "OAuth App #{n}" }
uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index 6d2e45f41ba..61b04708da2 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :pages_domain, class: 'PagesDomain' do
domain 'my.domain.com'
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index 06acaff6cd0..1b12f84d7b8 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :personal_access_token do
user
token { SecureRandom.hex(50) }
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
index 8d124dc2381..5ce1988c76f 100644
--- a/spec/factories/project_auto_devops.rb
+++ b/spec/factories/project_auto_devops.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :project_auto_devops do
project
enabled true
diff --git a/spec/factories/project_custom_attributes.rb b/spec/factories/project_custom_attributes.rb
index 5eedeb86304..099d2d7ff19 100644
--- a/spec/factories/project_custom_attributes.rb
+++ b/spec/factories/project_custom_attributes.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :project_custom_attribute do
project
sequence(:key) { |n| "key#{n}" }
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
index e73cc05f9d7..d5ace9425a0 100644
--- a/spec/factories/project_group_links.rb
+++ b/spec/factories/project_group_links.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :project_group_link do
project
group
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index accae636a3a..493b7bc021c 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :project_hook do
url { generate(:url) }
enable_ssl_verification false
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index 9cf3a1e8e8a..4260f52498d 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :project_member do
user
project
diff --git a/spec/factories/project_statistics.rb b/spec/factories/project_statistics.rb
index 6c2ed7c6581..2d0f698475d 100644
--- a/spec/factories/project_statistics.rb
+++ b/spec/factories/project_statistics.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :project_statistics do
project
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
index 38fcab7466d..89d8248f9f4 100644
--- a/spec/factories/project_wikis.rb
+++ b/spec/factories/project_wikis.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :project_wiki do
skip_create
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 4034e7905ad..d0f3911f730 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -1,6 +1,6 @@
require_relative '../support/test_env'
-FactoryGirl.define do
+FactoryBot.define do
# Project without repository
#
# Project does not have bare repository.
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index fe0cbfc4444..39460834d06 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :protected_branch do
name
project
diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb
index 225588b23cc..df9c8b3cb63 100644
--- a/spec/factories/protected_tags.rb
+++ b/spec/factories/protected_tags.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :protected_tag do
name
project
diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb
index 74497dc82c0..d80c65cf8bb 100644
--- a/spec/factories/releases.rb
+++ b/spec/factories/releases.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :release do
tag "v1.1.0"
description "Awesome release"
diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb
index c2febf5b438..80872067233 100644
--- a/spec/factories/sent_notifications.rb
+++ b/spec/factories/sent_notifications.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :sent_notification do
project
recipient factory: :user
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index c0232ba5bf6..f2b6e7a11f9 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
sequence(:username) { |n| "user#{n}" }
sequence(:name) { |n| "John Doe#{n}" }
sequence(:email) { |n| "user#{n}@example.org" }
diff --git a/spec/factories/service_hooks.rb b/spec/factories/service_hooks.rb
index e3f88ab8fcc..c907862b4f6 100644
--- a/spec/factories/service_hooks.rb
+++ b/spec/factories/service_hooks.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :service_hook do
url { generate(:url) }
service
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index ccf63f3ffa4..110ef33c6f7 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :service do
project
type 'Service'
@@ -18,6 +18,7 @@ FactoryGirl.define do
factory :kubernetes_service do
project
+ type 'KubernetesService'
active true
properties({
api_url: 'https://kubernetes.example.com',
diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb
index 075bccd7f94..2ab9a56d255 100644
--- a/spec/factories/snippets.rb
+++ b/spec/factories/snippets.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :snippet do
author
title { generate(:title) }
diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb
index e369f9f13e9..a467f850a80 100644
--- a/spec/factories/spam_logs.rb
+++ b/spec/factories/spam_logs.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :spam_log do
user
sequence(:source_ip) { |n| "42.42.42.#{n % 255}" }
diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb
index 1ae7fc9f384..a4bc4e87b0a 100644
--- a/spec/factories/subscriptions.rb
+++ b/spec/factories/subscriptions.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :subscription do
user
project
diff --git a/spec/factories/system_hooks.rb b/spec/factories/system_hooks.rb
index 841e1e293e8..9e00eeb6ef1 100644
--- a/spec/factories/system_hooks.rb
+++ b/spec/factories/system_hooks.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :system_hook do
url { generate(:url) }
end
diff --git a/spec/factories/system_note_metadata.rb b/spec/factories/system_note_metadata.rb
index f487a2d7e4a..e913068da40 100644
--- a/spec/factories/system_note_metadata.rb
+++ b/spec/factories/system_note_metadata.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :system_note_metadata do
note
action 'merge'
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
index 6f1545418eb..af34b0681e2 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -1,6 +1,6 @@
-# Read about factories at https://github.com/thoughtbot/factory_girl
+# Read about factories at https://github.com/thoughtbot/factory_bot
-FactoryGirl.define do
+FactoryBot.define do
factory :timelog do
time_spent 3600
user
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 4975befbfe3..6a6de665dd1 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :todo do
project
author
diff --git a/spec/factories/trending_project.rb b/spec/factories/trending_project.rb
index 246176611dc..f7c634fd21f 100644
--- a/spec/factories/trending_project.rb
+++ b/spec/factories/trending_project.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
# TrendingProject
factory :trending_project, class: 'TrendingProject' do
project
diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb
index df92b079581..26090b08966 100644
--- a/spec/factories/u2f_registrations.rb
+++ b/spec/factories/u2f_registrations.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :u2f_registration do
certificate { FFaker::BaconIpsum.characters(728) }
key_handle { FFaker::BaconIpsum.characters(86) }
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index e18f1a6bd4a..c39500faea1 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :upload do
model { build(:project) }
path { "uploads/-/system/project/avatar/avatar.jpg" }
diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb
index 9763cc0cf15..7183a8e1140 100644
--- a/spec/factories/user_agent_details.rb
+++ b/spec/factories/user_agent_details.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :user_agent_detail do
ip_address '127.0.0.1'
user_agent 'AppleWebKit/537.36'
diff --git a/spec/factories/user_custom_attributes.rb b/spec/factories/user_custom_attributes.rb
index 278cf290d4f..a184a2e0f17 100644
--- a/spec/factories/user_custom_attributes.rb
+++ b/spec/factories/user_custom_attributes.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :user_custom_attribute do
user
sequence(:key) { |n| "key#{n}" }
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 4000cd085b7..e62e0b263ca 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :user, aliases: [:author, :assignee, :recipient, :owner, :resource_owner] do
email { generate(:email) }
name { generate(:name) }
@@ -58,6 +58,10 @@ FactoryGirl.define do
end
end
+ trait :readme do
+ project_view :readme
+ end
+
factory :omniauth_user do
transient do
extern_uid '123456'
diff --git a/spec/factories/web_hook_log.rb b/spec/factories/web_hook_log.rb
index 230b3f6b26e..17837260a4b 100644
--- a/spec/factories/web_hook_log.rb
+++ b/spec/factories/web_hook_log.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :web_hook_log do
web_hook factory: :project_hook
trigger 'push_hooks'
diff --git a/spec/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb
index 3b4cfc380b8..b105c82b19d 100644
--- a/spec/factories/wiki_directories.rb
+++ b/spec/factories/wiki_directories.rb
@@ -1,4 +1,4 @@
-FactoryGirl.define do
+FactoryBot.define do
factory :wiki_directory do
skip_create
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
index 4105f59e289..2335b5118dd 100644
--- a/spec/factories/wiki_pages.rb
+++ b/spec/factories/wiki_pages.rb
@@ -1,6 +1,6 @@
require 'ostruct'
-FactoryGirl.define do
+FactoryBot.define do
factory :wiki_page do
transient do
attrs do
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 09b3c0b0994..66b71d0f556 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'factories' do
- FactoryGirl.factories.each do |factory|
+ FactoryBot.factories.each do |factory|
describe "#{factory.name} factory" do
it 'does not raise error when built' do
expect { build(factory.name) }.not_to raise_error
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 94b12105088..d02ac6c2e2a 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -88,7 +88,7 @@ describe "Admin::Projects" do
describe 'add admin himself to a project' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'adds admin a to a project as developer', :js do
@@ -110,8 +110,8 @@ describe "Admin::Projects" do
describe 'admin remove himself from a project' do
before do
- project.team << [user, :master]
- project.team << [current_user, :developer]
+ project.add_master(user)
+ project.add_developer(current_user)
end
it 'removes admin from the project' do
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index e3bb16af38a..c1c54177167 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -13,8 +13,8 @@ describe "Admin Runners" do
context "when there are runners" do
before do
- runner = FactoryGirl.create(:ci_runner, contacted_at: Time.now)
- FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id)
+ runner = FactoryBot.create(:ci_runner, contacted_at: Time.now)
+ FactoryBot.create(:ci_build, pipeline: pipeline, runner_id: runner.id)
visit admin_runners_path
end
@@ -25,8 +25,8 @@ describe "Admin Runners" do
describe 'search' do
before do
- FactoryGirl.create :ci_runner, description: 'runner-foo'
- FactoryGirl.create :ci_runner, description: 'runner-bar'
+ FactoryBot.create :ci_runner, description: 'runner-foo'
+ FactoryBot.create :ci_runner, description: 'runner-bar'
end
it 'shows correct runner when description matches' do
@@ -62,11 +62,11 @@ describe "Admin Runners" do
end
describe "Runner show page" do
- let(:runner) { FactoryGirl.create :ci_runner }
+ let(:runner) { FactoryBot.create :ci_runner }
before do
- @project1 = FactoryGirl.create(:project)
- @project2 = FactoryGirl.create(:project)
+ @project1 = FactoryBot.create(:project)
+ @project2 = FactoryBot.create(:project)
visit admin_runner_path(runner)
end
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index 1fd1cda694a..5a989319d5b 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -18,8 +18,8 @@ describe 'Admin System Info' do
it 'shows system info page' do
expect(page).to have_content 'CPU 2 cores'
- expect(page).to have_content 'Memory 4 GB / 16 GB'
- expect(page).to have_content 'Disks'
+ expect(page).to have_content 'Memory Usage 4 GB / 16 GB'
+ expect(page).to have_content 'Disk Usage'
expect(page).to have_content 'Uptime'
end
end
@@ -33,8 +33,8 @@ describe 'Admin System Info' do
it 'shows system info page with no CPU info' do
expect(page).to have_content 'CPU Unable to collect CPU info'
- expect(page).to have_content 'Memory 4 GB / 16 GB'
- expect(page).to have_content 'Disks'
+ expect(page).to have_content 'Memory Usage 4 GB / 16 GB'
+ expect(page).to have_content 'Disk Usage'
expect(page).to have_content 'Uptime'
end
end
@@ -48,8 +48,8 @@ describe 'Admin System Info' do
it 'shows system info page with no CPU info' do
expect(page).to have_content 'CPU 2 cores'
- expect(page).to have_content 'Memory Unable to collect memory info'
- expect(page).to have_content 'Disks'
+ expect(page).to have_content 'Memory Usage Unable to collect memory info'
+ expect(page).to have_content 'Disk Usage'
expect(page).to have_content 'Uptime'
end
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 89c9d377003..d673bac4995 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.team << [user, :master]
- project2.team << [user, :master]
+ project1.add_master(user)
+ project2.add_master(user)
end
describe "atom feed" do
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index 2c0c331b6db..c6683bb3bc9 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -26,7 +26,7 @@ describe "Dashboard Feed" do
let(:note) { create(:note, noteable: issue, author: user, note: 'Bug confirmed', project: project) }
before do
- project.team << [user, :master]
+ project.add_master(user)
issue_event(issue, user)
note_event(note, user)
visit dashboard_projects_path(:atom, rss_token: user.rss_token)
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 4102ac0588a..525ce23aa56 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -9,7 +9,7 @@ describe 'Issues Feed' do
let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
group.add_developer(user)
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 2b934d81674..782f42aab04 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -47,7 +47,7 @@ describe "User Feed" do
let!(:push_event_payload) { create(:push_event_payload, event: push_event) }
before do
- project.team << [user, :master]
+ project.add_master(user)
issue_event(issue, user)
note_event(note, user)
merge_request_event(merge_request, user)
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index 7a395f62511..9aef68b7156 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -52,7 +52,7 @@ describe 'Auto deploy' do
context 'when user configured kubernetes from Integration > Kubernetes' do
before do
create :kubernetes_service, project: project
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
@@ -65,7 +65,7 @@ describe 'Auto deploy' do
context 'when user configured kubernetes from CI/CD > Clusters' do
before do
create(:cluster, :provided_by_gcp, projects: [project])
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index e4cfcea45a5..18901a954cc 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index e8d779f5772..3876d1c76d7 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.team << [user, :master]
- project.team << [user2, :master]
+ project.add_master(user)
+ project.add_master(user2)
set_cookie('sidebar_collapsed', 'true')
@@ -551,7 +551,7 @@ describe 'Issue Boards', :js do
let(:user_guest) { create(:user) }
before do
- project.team << [user_guest, :guest]
+ project.add_guest(user_guest)
sign_out(:user)
sign_in(user_guest)
visit project_board_path(project, board)
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 4cbb48e2e6e..5abd02dbb48 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
index 435de3861cf..d820a59aa16 100644
--- a/spec/features/boards/keyboard_shortcut_spec.rb
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -1,20 +1,38 @@
require 'rails_helper'
describe 'Issue Boards shortcut', :js do
- let(:project) { create(:project) }
+ context 'issues are enabled' do
+ let(:project) { create(:project) }
- before do
- create(:board, project: project)
+ before do
+ create(:board, project: project)
- sign_in(create(:admin))
+ sign_in(create(:admin))
- visit project_path(project)
+ visit project_path(project)
+ end
+
+ it 'takes user to issue board index' do
+ find('body').native.send_keys('gb')
+ expect(page).to have_selector('.boards-list')
+
+ wait_for_requests
+ end
end
- it 'takes user to issue board index' do
- find('body').native.send_keys('gb')
- expect(page).to have_selector('.boards-list')
+ context 'issues are not enabled' do
+ let(:project) { create(:project, :issues_disabled) }
+
+ before do
+ sign_in(create(:admin))
+
+ visit project_path(project)
+ end
+
+ it 'does not take user to the issue board index' do
+ find('body').native.send_keys('gb')
- wait_for_requests
+ expect(page).to have_selector("body[data-page='projects:show']")
+ end
end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 422d96175f7..5907bb0840f 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -76,7 +76,7 @@ describe 'Issue Boards add issue modal filtering', :js do
let!(:issue) { create(:issue, project: project, author: user2) }
before do
- project.team << [user2, :developer]
+ project.add_developer(user2)
visit_board
end
@@ -99,7 +99,7 @@ describe 'Issue Boards add issue modal filtering', :js do
let!(:issue) { create(:issue, project: project, assignees: [user2]) }
before do
- project.team << [user2, :developer]
+ project.add_developer(user2)
visit_board
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 5ac4d87e90b..6769acb7c9c 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index a9530becb65..70faf28e09d 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -12,7 +12,7 @@ feature 'Contributions Calendar', :js do
issue_params = { title: issue_title }
def get_cell_color_selector(contributions)
- activity_colors = %w[#ededed #acd5f2 #7fa8c9 #527ba0 #254e77]
+ activity_colors = ["#ededed", "rgb(172, 213, 242)", "rgb(127, 168, 201)", "rgb(82, 123, 160)", "rgb(37, 78, 119)"]
# We currently don't actually test the cases with contributions >= 20
activity_colors_index =
if contributions > 0 && contributions < 10
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index c870910c8ea..a28b8905b65 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -26,7 +26,7 @@ describe 'Commits' do
let!(:status) { create(:generic_commit_status, pipeline: pipeline) }
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
describe 'Commit builds' do
@@ -51,7 +51,7 @@ describe 'Commits' do
context 'when logged as developer' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe 'Project commits' do
@@ -145,7 +145,7 @@ describe 'Commits' do
context "when logged as reporter" do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
build.update_attributes(legacy_artifacts_file: artifacts_file)
visit pipeline_path(pipeline)
end
@@ -188,7 +188,7 @@ describe 'Commits' do
let(:branch_name) { 'master' }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_commits_path(project, branch_name)
end
@@ -197,7 +197,7 @@ describe 'Commits' do
commits = project.repository.commits(branch_name)
commits.each do |commit|
- expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}")
+ expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}")
end
end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 1fcb8d5bc67..d8f1a919522 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -285,6 +285,102 @@ describe 'Copy as GFM', :js do
end
verify(
+ 'MermaidFilter: mermaid as converted from GFM to HTML',
+
+ <<-GFM.strip_heredoc
+ ```mermaid
+ graph TD;
+ A-->B;
+ ```
+ GFM
+ )
+
+ aggregate_failures('MermaidFilter: mermaid as transformed from HTML to SVG') do
+ gfm = <<-GFM.strip_heredoc
+ ```mermaid
+ graph TD;
+ A-->B;
+ ```
+ GFM
+
+ html = <<-HTML.strip_heredoc
+ <svg id="mermaidChart1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 87.234375 174" style="max-width:87.234375px;" class="mermaid">
+ <style>
+ .mermaid {
+ /* Flowchart variables */
+ /* Sequence Diagram variables */
+ /* Gantt chart variables */
+ /** Section styling */
+ /* Grid and axis */
+ /* Today line */
+ /* Task styling */
+ /* Default task */
+ /* Specific task settings for the sections*/
+ /* Active task */
+ /* Completed task */
+ /* Tasks on the critical line */
+ }
+ </style>
+ <g>
+ <g class="output">
+ <g class="clusters"></g>
+ <g class="edgePaths">
+ <g class="edgePath" style="opacity: 1;">
+ <path class="path" d="M33.6171875,52L33.6171875,77L33.6171875,102" marker-end="url(#arrowhead65)" style="fill:none"></path>
+ <defs>
+ <marker id="arrowhead65" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto">
+ <path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path>
+ </marker>
+ </defs>
+ </g>
+ </g>
+ <g class="edgeLabels">
+ <g class="edgeLabel" style="opacity: 1;" transform="">
+ <g transform="translate(0,0)" class="label">
+ <foreignObject width="0" height="0">
+ <div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">
+ <span class="edgeLabel"></span>
+ </div>
+ </foreignObject>
+ </g>
+ </g>
+ </g>
+ <g class="nodes">
+ <g class="node" id="A" transform="translate(33.6171875,36)" style="opacity: 1;">
+ <rect rx="0" ry="0" x="-13.6171875" y="-16" width="27.234375" height="32"></rect>
+ <g class="label" transform="translate(0,0)">
+ <g transform="translate(-3.6171875,-6)">
+ <foreignObject width="7.234375" height="12">
+ <div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">A</div>
+ </foreignObject>
+ </g>
+ </g>
+ </g>
+ <g class="node" id="B" transform="translate(33.6171875,118)" style="opacity: 1;">
+ <rect rx="0" ry="0" x="-13.6171875" y="-16" width="27.234375" height="32">
+ </rect>
+ <g class="label" transform="translate(0,0)">
+ <g transform="translate(-3.6171875,-6)">
+ <foreignObject width="7.234375" height="12">
+ <div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">B</div>
+ </foreignObject>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ <text class="source" display="none">graph TD;
+ A--&gt;B;
+ </text>
+ </svg>
+ HTML
+
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+
+ verify(
'SanitizationFilter',
<<-GFM.strip_heredoc
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 177cd50dd72..d36954954b6 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -95,7 +95,7 @@ feature 'Cycle Analytics', :js do
before do
user.update_attribute(:preferred_language, 'es')
- project.team << [user, :master]
+ project.add_master(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 bd115785646..a74a8aac2b2 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -24,6 +24,7 @@ feature 'Dashboard > Activity' do
end
let(:note) { create(:note, project: project, noteable: merge_request) }
+ let(:milestone) { create(:milestone, :active, project: project, title: '1.0') }
let!(:push_event) do
event = create(:push_event, project: project, author: user)
@@ -54,6 +55,10 @@ feature 'Dashboard > Activity' do
create(:event, :commented, project: project, target: note, author: user)
end
+ let!(:milestone_event) do
+ create(:event, :closed, project: project, target: milestone, author: user)
+ end
+
before do
project.add_master(user)
@@ -68,6 +73,7 @@ feature 'Dashboard > Activity' do
expect(page).to have_content('accepted')
expect(page).to have_content('closed')
expect(page).to have_content('commented on')
+ expect(page).to have_content('closed milestone')
end
end
@@ -107,6 +113,7 @@ feature 'Dashboard > Activity' do
expect(page).not_to have_content('accepted')
expect(page).to have_content('closed')
expect(page).not_to have_content('commented on')
+ expect(page).to have_content('closed milestone')
end
end
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index e8d699ff5e0..b36231fd78b 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.team << [user, :master]
- archived_project.team << [user, :master]
+ project.add_master(user)
+ archived_project.add_master(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 349f9a47112..089c388636d 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -8,7 +8,7 @@ feature 'Tooltips on .timeago dates', :js do
context 'on the activity tab' do
before do
- project.team << [user, :master]
+ project.add_master(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.team << [user, :master]
+ project.add_master(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 d92c002b4e7..a71020002dc 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -94,22 +94,14 @@ feature 'Dashboard Groups page', :js do
end
it 'can toggle parent group' do
- # Collapsed by default
- expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
- expect(page).to have_selector("#group-#{group.id} .fa-caret-right")
-
# expand
click_group_caret(group)
- expect(page).to have_selector("#group-#{group.id} .fa-caret-down")
- expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right", count: 1)
expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}")
# collapse
click_group_caret(group)
- expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
- expect(page).to have_selector("#group-#{group.id} .fa-caret-right")
expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}")
end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 5b4c00b3c7e..54652e2d849 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.team << [current_user, :master] }
+ [project, project_with_issues_disabled].each { |project| project.add_master(current_user) }
sign_in(current_user)
visit issues_dashboard_path(assignee_id: current_user.id)
end
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 41d37376cfb..7787772a958 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -16,7 +16,7 @@ feature 'Dashboard > Milestones' do
let(:project) { create(:project, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
before do
- project.team << [user, :master]
+ project.add_master(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 8f96899fb4f..6c3093607b0 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -5,7 +5,7 @@ feature 'Project member activity', :js do
let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) }
before do
- project.team << [user, :master]
+ project.add_master(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 fbf8b5c0db6..586c7b48d0b 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -6,7 +6,7 @@ feature 'Dashboard Projects' do
let(:project2) { create(:project, :public, name: 'Community project') }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index ad0f132da8c..2fc34301d51 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -15,8 +15,8 @@ feature 'Dashboard > User filters todos', :js do
create(:todo, user: user_1, author: user_2, project: project_1, target: issue, action: 1)
create(:todo, user: user_1, author: user_1, project: project_2, target: merge_request, action: 2)
- project_1.team << [user_1, :developer]
- project_2.team << [user_1, :developer]
+ project_1.add_developer(user_1)
+ project_2.add_developer(user_1)
sign_in(user_1)
visit dashboard_todos_path
end
@@ -66,8 +66,8 @@ feature 'Dashboard > User filters todos', :js do
create(:todo, user: user_1, author: user_3, project: project_1, target: issue, action: 1, state: :done)
create(:todo, user: user_1, author: user_4, project: project_2, target: merge_request, action: 2, state: :done)
- project_1.team << [user_3, :developer]
- project_2.team << [user_4, :developer]
+ project_1.add_developer(user_3)
+ project_2.add_developer(user_4)
visit dashboard_todos_path(state: 'done')
diff --git a/spec/features/dashboard/todos/todos_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb
index b7d39a872b0..10e3ad843fd 100644
--- a/spec/features/dashboard/todos/todos_sorting_spec.rb
+++ b/spec/features/dashboard/todos/todos_sorting_spec.rb
@@ -9,7 +9,7 @@ feature 'Dashboard > User sorts todos' do
let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'sort options' do
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 6f916078b1a..94133c62b5c 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -13,7 +13,7 @@ feature 'Dashboard Todos' do
end
it 'shows "All done" message' do
- expect(page).to have_content 'Todos let you see what you should do next.'
+ expect(page).to have_content 'Todos let you see what you should do next'
end
end
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index c352b6ded14..92f4d4b854c 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -7,14 +7,14 @@ describe 'Dashboard > User filters projects' do
let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
describe 'filtering personal projects' do
before do
- project2.team << [user, :developer]
+ project2.add_developer(user)
visit dashboard_projects_path
end
diff --git a/spec/features/explore/groups_spec.rb b/spec/features/explore/groups_spec.rb
new file mode 100644
index 00000000000..e4ef47d88dd
--- /dev/null
+++ b/spec/features/explore/groups_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe 'Explore Groups', :js do
+ let(:user) { create :user }
+ let(:group) { create :group }
+ let!(:private_project) do
+ create :project, :private, namespace: group do |project|
+ create(:issue, project: internal_project)
+ create(:merge_request, source_project: project, target_project: project)
+ end
+ end
+
+ let!(:internal_project) do
+ create :project, :internal, namespace: group do |project|
+ create(:issue, project: project)
+ create(:merge_request, source_project: project, target_project: project)
+ end
+ end
+
+ let!(:public_project) do
+ create(:project, :public, namespace: group) do |project|
+ create(:issue, project: project)
+ create(:merge_request, source_project: project, target_project: project)
+ end
+ end
+
+ shared_examples 'renders public and internal projects' do
+ it do
+ visit_page
+ expect(page).to have_content(public_project.name)
+ expect(page).to have_content(internal_project.name)
+ expect(page).not_to have_content(private_project.name)
+ end
+ end
+
+ shared_examples 'renders only public project' do
+ it do
+ visit_page
+ expect(page).to have_content(public_project.name)
+ expect(page).not_to have_content(internal_project.name)
+ expect(page).not_to have_content(private_project.name)
+ end
+ end
+
+ shared_examples 'renders group in public groups area' do
+ it do
+ visit explore_groups_path
+ expect(page).to have_content(group.name)
+ end
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it_behaves_like 'renders public and internal projects' do
+ subject(:visit_page) { visit group_path(group) }
+ end
+
+ it_behaves_like 'renders public and internal projects' do
+ subject(:visit_page) { visit issues_group_path(group) }
+ end
+
+ it_behaves_like 'renders public and internal projects' do
+ subject(:visit_page) { visit merge_requests_group_path(group) }
+ end
+
+ it_behaves_like 'renders group in public groups area'
+ end
+
+ context 'when signed out' do
+ it_behaves_like 'renders only public project' do
+ subject(:visit_page) { visit group_path(group) }
+ end
+
+ it_behaves_like 'renders only public project' do
+ subject(:visit_page) { visit issues_group_path(group) }
+ end
+
+ it_behaves_like 'renders only public project' do
+ subject(:visit_page) { visit merge_requests_group_path(group) }
+ end
+
+ it_behaves_like 'renders group in public groups area'
+ end
+end
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index f04e13adba7..4f575613848 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -5,7 +5,7 @@ feature 'Global search' do
let(:project) { create(:project, namespace: user.namespace) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index d2d0be35f1c..e9b375f4c94 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -24,7 +24,7 @@ feature 'Group variables', :js do
expect(find(".variable-value")).to have_content('******')
expect(find(".variable-protected")).to have_content('Yes')
end
- click_on 'Reveal Values'
+ click_on 'Reveal value'
page.within('.variables-table') do
expect(find(".variable-value")).to have_content('AAA123')
end
diff --git a/spec/features/groups/labels/user_sees_links_to_issuables.rb b/spec/features/groups/labels/user_sees_links_to_issuables.rb
new file mode 100644
index 00000000000..5d6290d2109
--- /dev/null
+++ b/spec/features/groups/labels/user_sees_links_to_issuables.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+feature 'Groups > Labels > User sees links to issuables' do
+ set(:group) { create(:group, :public) }
+
+ before do
+ create(:group_label, group: group, title: 'bug')
+ visit group_labels_path(group)
+ end
+
+ scenario 'shows links to MRs and issues' do
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+end
diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb
index da1e17225db..21f7b4999ad 100644
--- a/spec/features/groups/members/manage_members.rb
+++ b/spec/features/groups/members/manage_members.rb
@@ -38,6 +38,27 @@ feature 'Groups > Members > Manage members' do
end
end
+ scenario 'do not disclose email addresses', :js do
+ group.add_owner(user1)
+ create(:user, email: 'undisclosed_email@gitlab.com', name: "Jane 'invisible' Doe")
+
+ visit group_group_members_path(group)
+
+ find('.select2-container').click
+ select_input = find('.select2-input')
+
+ select_input.send_keys('@gitlab.com')
+ wait_for_requests
+
+ expect(page).to have_content('No matches found')
+
+ select_input.native.clear
+ select_input.send_keys('undisclosed_email@gitlab.com')
+ wait_for_requests
+
+ expect(page).to have_content("Jane 'invisible' Doe")
+ end
+
scenario 'remove user from group', :js do
group.add_owner(user1)
group.add_developer(user2)
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 7fc2b383749..ceccc471405 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -55,4 +55,20 @@ feature 'Group show page' do
end
end
end
+
+ context 'group has a project with emoji in description', :js do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, description: ':smile:', namespace: group) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ visit path
+ end
+
+ it 'shows the project info' do
+ expect(page).to have_content(project.title)
+ expect(page).to have_selector('gl-emoji[data-name="smile"]')
+ end
+ end
end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 93be3b066ee..0d04ed612c2 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -32,12 +32,30 @@ describe 'Help Pages' do
it_behaves_like 'help page', prefix: '/gitlab'
end
+
+ context 'quick link shortcuts', :js do
+ before do
+ visit help_path
+ end
+
+ it 'focuses search bar' do
+ find('.js-trigger-search-bar').click
+
+ expect(page).to have_selector('#search:focus')
+ end
+
+ it 'opens shortcuts help dialog' do
+ find('.js-trigger-shortcut').click
+
+ expect(page).to have_selector('#modal-shortcuts')
+ end
+ end
end
context 'in a production environment with version check enabled', :js do
before do
allow(Rails.env).to receive(:production?) { true }
- allow_any_instance_of(ApplicationSetting).to receive(:version_check_enabled) { true }
+ stub_application_setting(version_check_enabled: true)
allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' }
sign_in(create(:user))
@@ -56,9 +74,9 @@ describe 'Help Pages' do
describe 'when help page is customized' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:help_page_hide_commercial_content?) { true }
- allow_any_instance_of(ApplicationSetting).to receive(:help_page_text) { "My Custom Text" }
- allow_any_instance_of(ApplicationSetting).to receive(:help_page_support_url) { "http://example.com/help" }
+ stub_application_setting(help_page_hide_commercial_content: true,
+ help_page_text: 'My Custom Text',
+ help_page_support_url: 'http://example.com/help')
sign_in(create(:user))
visit help_path
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
new file mode 100644
index 00000000000..e4be6193b8b
--- /dev/null
+++ b/spec/features/invites_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe 'Invites' do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user, name: 'John Doe') }
+ let(:group) { create(:group, name: 'Owned') }
+ let(:project) { create(:project, :repository, namespace: group) }
+ let(:invite) { group.group_members.invite.last }
+
+ before do
+ project.add_master(owner)
+ group.add_user(owner, Gitlab::Access::OWNER)
+ group.add_developer('user@example.com', owner)
+ invite.generate_invite_token!
+ end
+
+ context 'when signed out' do
+ before do
+ visit invite_path(invite.raw_invite_token)
+ end
+
+ it 'renders sign in page with sign in notice' do
+ expect(current_path).to eq(new_user_session_path)
+ expect(page).to have_content('To accept this invitation, sign in')
+ end
+
+ it 'sign in and redirects to invitation page' do
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: user.password
+ check 'user_remember_me'
+ click_button 'Sign in'
+
+ expect(current_path).to eq(invite_path(invite.raw_invite_token))
+ expect(page).to have_content(
+ 'You have been invited by John Doe to join group Owned as Developer.'
+ )
+ expect(page).to have_link('Accept invitation')
+ expect(page).to have_link('Decline')
+ end
+ end
+
+ context 'when signed in as an exists member' do
+ before do
+ sign_in(owner)
+ end
+
+ it 'shows message user already a member' do
+ visit invite_path(invite.raw_invite_token)
+ expect(page).to have_content('However, you are already a member of this group.')
+ end
+ end
+
+ describe 'accepting the invitation' do
+ before do
+ sign_in(user)
+ visit invite_path(invite.raw_invite_token)
+ end
+
+ it 'grants access and redirects to group page' do
+ page.click_link 'Accept invitation'
+ expect(current_path).to eq(group_path(group))
+ expect(page).to have_content(
+ 'You have been granted Developer access to group Owned.'
+ )
+ end
+ end
+
+ describe 'declining the application' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ visit invite_path(invite.raw_invite_token)
+ end
+
+ it 'declines application and redirects to dashboard' do
+ page.click_link 'Decline'
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(page).to have_content(
+ 'You have declined the invitation to join group Owned.'
+ )
+ end
+ end
+
+ context 'when signed out' do
+ before do
+ visit decline_invite_path(invite.raw_invite_token)
+ end
+
+ it 'declines application and redirects to sign in page' do
+ expect(current_path).to eq(new_user_session_path)
+ expect(page).to have_content(
+ 'You have declined the invitation to join group Owned.'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 850b35c4467..1131e1711bf 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index fa4d3a55c62..587ece22ec7 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -11,7 +11,7 @@ feature 'Issues > Labels bulk assignment' do
context 'as an allowed user', :js do
before do
- project.team << [user, :master]
+ project.add_master(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 822ba48e005..e0466aaf422 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
@@ -8,7 +8,7 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d
describe 'as a user with access to the project' do
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_merge_request_path(project, merge_request)
end
@@ -81,7 +81,7 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d
describe 'as a reporter' do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
sign_in user
visit new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
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 f0bed85595c..34beb282bad 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
@@ -8,7 +8,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
describe 'As a user with access to the project' do
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_merge_request_path(project, merge_request)
end
@@ -65,7 +65,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
describe 'as a reporter' do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
sign_in user
visit new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid,
discussion_to_resolve: discussion.id)
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 2e4a25ee15d..cbd0949c192 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.team << [user, :master]
- project.team << [user_john, :master]
- project.team << [user_jacob, :master]
+ project.add_master(user)
+ project.add_master(user_john)
+ project.add_master(user_jacob)
sign_in(user)
create(:issue, project: project)
@@ -222,7 +222,7 @@ describe 'Dropdown assignee', :js do
expect(initial_size).to be > 0
new_user = create(:user)
- project.team << [new_user, :master]
+ project.add_master(new_user)
find('.filtered-search-box .clear-search').click
filtered_search.set('assignee')
filtered_search.send_keys(':')
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 2fb5e7cdba4..70b4f11410d 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.team << [user, :master]
- project.team << [user_john, :master]
- project.team << [user_jacob, :master]
+ project.add_master(user)
+ project.add_master(user_john)
+ project.add_master(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.team << [new_user, :master]
+ project.add_master(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 8db435634fd..436625a6f7b 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.team << [user, :master]
+ project.add_master(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 0183495a1db..ef40dddfd3a 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.team << [user, :master]
+ project.add_master(user)
create(:issue, project: project)
end
@@ -176,6 +176,7 @@ describe 'Dropdown hint', :js do
it 'reuses existing author text' do
filtered_search.send_keys('author:')
filtered_search.send_keys(:backspace)
+ filtered_search.send_keys(:backspace)
click_hint('author')
expect_tokens([{ name: 'author' }])
@@ -185,6 +186,7 @@ describe 'Dropdown hint', :js do
it 'reuses existing assignee text' do
filtered_search.send_keys('assignee:')
filtered_search.send_keys(:backspace)
+ filtered_search.send_keys(:backspace)
click_hint('assignee')
expect_tokens([{ name: 'assignee' }])
@@ -194,6 +196,7 @@ describe 'Dropdown hint', :js do
it 'reuses existing milestone text' do
filtered_search.send_keys('milestone:')
filtered_search.send_keys(:backspace)
+ filtered_search.send_keys(:backspace)
click_hint('milestone')
expect_tokens([{ name: 'milestone' }])
@@ -203,6 +206,7 @@ describe 'Dropdown hint', :js do
it 'reuses existing label text' do
filtered_search.send_keys('label:')
filtered_search.send_keys(:backspace)
+ filtered_search.send_keys(:backspace)
click_hint('label')
expect_tokens([{ name: 'label' }])
@@ -212,6 +216,7 @@ describe 'Dropdown hint', :js do
it 'reuses existing emoji text' do
filtered_search.send_keys('my-reaction:')
filtered_search.send_keys(:backspace)
+ filtered_search.send_keys(:backspace)
click_hint('my-reaction')
expect_tokens([{ name: 'my-reaction' }])
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 031eb06723a..94710c2f71f 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
create(:issue, project: project)
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 88688422dc7..268590da599 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
create(:issue, project: project)
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 2db6f9a2982..faf14be4818 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.team << [user, :master]
- project.team << [user2, :master]
+ project.add_master(user)
+ project.add_master(user2)
sign_in(user)
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index c31b636d67f..f2624f55c86 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -7,7 +7,7 @@ feature 'GFM autocomplete', :js do
let(:issue) { create(:issue, project: project) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_issue_path(project, issue)
@@ -15,7 +15,7 @@ feature 'GFM autocomplete', :js do
end
it 'updates issue descripton with GFM reference' do
- find('.issuable-edit').click
+ find('.js-issuable-edit').click
simulate_input('#issue-description', "@#{user.name[0...3]}")
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 4224a8fe5d4..babb0285590 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -24,7 +24,7 @@ feature 'Issue Detail', :js do
visit project_issue_path(project, issue)
wait_for_requests
- click_link 'Edit'
+ page.find('.js-issuable-edit').click
fill_in 'issuable-title', with: 'issue title'
click_button 'Save'
wait_for_requests
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index a9de52bd8d5..a5c9d0bde5d 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -18,7 +18,7 @@ feature 'Issue Sidebar' do
let(:issue2) { create(:issue, project: project, author: user2) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
visit_issue(project, issue2)
find('.block.assignee .edit-link').click
@@ -78,7 +78,7 @@ feature 'Issue Sidebar' do
context 'as a allowed user' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
visit_issue(project, issue)
end
@@ -156,7 +156,7 @@ feature 'Issue Sidebar' do
context 'as a guest' do
before do
- project.team << [user, :guest]
+ project.add_guest(user)
visit_issue(project, issue)
end
diff --git a/spec/features/issues/keyboard_shortcut_spec.rb b/spec/features/issues/keyboard_shortcut_spec.rb
new file mode 100644
index 00000000000..961de9d3d25
--- /dev/null
+++ b/spec/features/issues/keyboard_shortcut_spec.rb
@@ -0,0 +1,36 @@
+require 'rails_helper'
+
+describe 'Issues shortcut', :js do
+ context 'New Issue shortcut' do
+ context 'issues are enabled' do
+ let(:project) { create(:project) }
+
+ before do
+ sign_in(create(:admin))
+
+ visit project_path(project)
+ end
+
+ it 'takes user to the new issue page' do
+ find('body').native.send_keys('i')
+ expect(page).to have_selector('#new_issue')
+ end
+ end
+
+ context 'issues are not enabled' do
+ let(:project) { create(:project, :issues_disabled) }
+
+ before do
+ sign_in(create(:admin))
+
+ visit project_path(project)
+ end
+
+ it 'does not take user to the new issue page' do
+ find('body').native.send_keys('i')
+
+ expect(page).to have_selector("body[data-page='projects:show']")
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 17035b5501c..076a02150a4 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -13,7 +13,7 @@ feature 'issue move to another project' do
context 'user does not have permission to move issue' do
background do
- old_project.team << [user, :guest]
+ old_project.add_guest(user)
visit issue_path(issue)
end
@@ -31,8 +31,8 @@ feature 'issue move to another project' do
let(:cross_reference) { old_project.to_reference(new_project) }
background do
- old_project.team << [user, :reporter]
- new_project.team << [user, :reporter]
+ old_project.add_reporter(user)
+ new_project.add_reporter(user)
visit issue_path(issue)
end
@@ -50,7 +50,7 @@ feature 'issue move to another project' do
end
scenario 'searching project dropdown', :js do
- new_project_search.team << [user, :reporter]
+ new_project_search.add_reporter(user)
find('.js-move-issue').click
wait_for_requests
@@ -66,7 +66,7 @@ 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.team << [user, :guest] }
+ background { another_project.add_guest(user) }
scenario 'browsing projects in projects select' do
find('.js-move-issue').click
diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb
index 05c93a19253..f08c73f947c 100644
--- a/spec/features/issues/notes_on_issues_spec.rb
+++ b/spec/features/issues/notes_on_issues_spec.rb
@@ -8,7 +8,7 @@ describe 'Create notes on issues', :js do
let(:note_text) { "Check #{mention.to_reference}" }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit project_issue_path(project, issue)
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index d25231d624c..53706ef84bc 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 29a2d38ae18..8e6493bbd93 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -6,7 +6,7 @@ feature 'Manually create a todo item from issue', :js do
let!(:user) { create(:user)}
before do
- project.team << [user, :master]
+ project.add_master(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 bcc6e9bab0f..7d6edc171f8 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -6,7 +6,7 @@ feature 'Multiple issue updating from issues#index', :js do
let!(:user) { create(:user)}
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index c4c06ed514b..e711a191db2 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -12,7 +12,7 @@ feature 'Issues > User uses quick actions', :js do
let(:project) { create(:project, :public) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_issue_path(project, issue)
end
@@ -50,7 +50,7 @@ feature 'Issues > User uses quick actions', :js do
context 'when the current user cannot update the due date' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
@@ -90,7 +90,7 @@ feature 'Issues > User uses quick actions', :js do
context 'when the current user cannot update the due date' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
@@ -138,7 +138,7 @@ feature 'Issues > User uses quick actions', :js do
context 'when the current user cannot update the issue' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
@@ -163,7 +163,7 @@ feature 'Issues > User uses quick actions', :js do
let(:target_project) { create(:project, :public) }
before do
- target_project.team << [user, :master]
+ target_project.add_master(user)
sign_in(user)
visit project_issue_path(project, issue)
end
@@ -220,7 +220,7 @@ feature 'Issues > User uses quick actions', :js do
let(:wontfix_target) { create(:label, project: target_project, title: 'wontfix') }
before do
- target_project.team << [user, :master]
+ target_project.add_master(user)
sign_in(user)
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 852d9e368aa..314bd19f586 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -8,729 +8,754 @@ describe 'Issues' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
- before do
- sign_in(user)
- user2 = create(:user)
-
- project.team << [[user, user2], :developer]
- end
+ describe 'while user is signed out' do
+ describe 'empty state' do
+ it 'user sees empty state' do
+ visit project_issues_path(project)
- describe 'Edit issue' do
- let!(:issue) do
- create(:issue,
- author: user,
- assignees: [user],
- project: project)
+ expect(page).to have_content('Register / Sign In')
+ expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project.')
+ expect(page).to have_content('You can register or sign in to create issues for this project.')
+ end
end
+ end
+ describe 'while user is signed in' do
before do
- visit edit_project_issue_path(project, issue)
- find('.js-zen-enter').click
- end
-
- it 'opens new issue popup' do
- expect(page).to have_content("Issue ##{issue.iid}")
- end
- end
+ sign_in(user)
+ user2 = create(:user)
- describe 'Editing issue assignee' do
- let!(:issue) do
- create(:issue,
- author: user,
- assignees: [user],
- project: project)
+ project.add_developer(user)
+ project.add_developer(user2)
end
- it 'allows user to select unassigned', :js do
- visit edit_project_issue_path(project, issue)
-
- expect(page).to have_content "Assignee #{user.name}"
+ describe 'empty state' do
+ it 'user sees empty state' do
+ visit project_issues_path(project)
- first('.js-user-search').click
- click_link 'Unassigned'
+ expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project')
+ expect(page).to have_content('Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.')
+ expect(page).to have_content('New issue')
+ end
+ end
- click_button 'Save changes'
+ describe 'Edit issue' do
+ let!(:issue) do
+ create(:issue,
+ author: user,
+ assignees: [user],
+ project: project)
+ end
- page.within('.assignee') do
- expect(page).to have_content 'No assignee - assign yourself'
+ before do
+ visit edit_project_issue_path(project, issue)
+ find('.js-zen-enter').click
end
- expect(issue.reload.assignees).to be_empty
+ it 'opens new issue popup' do
+ expect(page).to have_content("Issue ##{issue.iid}")
+ end
end
- end
- describe 'due date', :js do
- context 'on new form' do
- before do
- visit new_project_issue_path(project)
+ describe 'Editing issue assignee' do
+ let!(:issue) do
+ create(:issue,
+ author: user,
+ assignees: [user],
+ project: project)
end
- it 'saves with due date' do
- date = Date.today.at_beginning_of_month
-
- fill_in 'issue_title', with: 'bug 345'
- fill_in 'issue_description', with: 'bug description'
- find('#issuable-due-date').click
+ it 'allows user to select unassigned', :js do
+ visit edit_project_issue_path(project, issue)
- page.within '.pika-single' do
- click_button date.day
- end
+ expect(page).to have_content "Assignee #{user.name}"
- expect(find('#issuable-due-date').value).to eq date.to_s
+ first('.js-user-search').click
+ click_link 'Unassigned'
- click_button 'Submit issue'
+ click_button 'Save changes'
- page.within '.issuable-sidebar' do
- expect(page).to have_content date.to_s(:medium)
+ page.within('.assignee') do
+ expect(page).to have_content 'No assignee - assign yourself'
end
+
+ expect(issue.reload.assignees).to be_empty
end
end
- context 'on edit form' do
- let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
+ describe 'due date', :js do
+ context 'on new form' do
+ before do
+ visit new_project_issue_path(project)
+ end
- before do
- visit edit_project_issue_path(project, issue)
- end
+ it 'saves with due date' do
+ date = Date.today.at_beginning_of_month
- it 'saves with due date' do
- date = Date.today.at_beginning_of_month
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
- expect(find('#issuable-due-date').value).to eq date.to_s
+ page.within '.pika-single' do
+ click_button date.day
+ end
- date = date.tomorrow
+ expect(find('#issuable-due-date').value).to eq date.to_s
- fill_in 'issue_title', with: 'bug 345'
- fill_in 'issue_description', with: 'bug description'
- find('#issuable-due-date').click
+ click_button 'Submit issue'
- page.within '.pika-single' do
- click_button date.day
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
end
+ end
- expect(find('#issuable-due-date').value).to eq date.to_s
+ context 'on edit form' do
+ let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
- click_button 'Save changes'
-
- page.within '.issuable-sidebar' do
- expect(page).to have_content date.to_s(:medium)
+ before do
+ visit edit_project_issue_path(project, issue)
end
- end
- it 'warns about version conflict' do
- issue.update(title: "New title")
+ it 'saves with due date' do
+ date = Date.today.at_beginning_of_month
- fill_in 'issue_title', with: 'bug 345'
- fill_in 'issue_description', with: 'bug description'
+ expect(find('#issuable-due-date').value).to eq date.to_s
- click_button 'Save changes'
+ date = date.tomorrow
- expect(page).to have_content 'Someone edited the issue the same time you did'
- end
- end
- end
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.pika-single' do
+ click_button date.day
+ end
- describe 'Issue info' do
- it 'links to current issue in breadcrubs' do
- issue = create(:issue, project: project)
+ expect(find('#issuable-due-date').value).to eq date.to_s
- visit project_issue_path(project, issue)
+ click_button 'Save changes'
- expect(find('.breadcrumbs-sub-title a')[:href]).to end_with(issue_path(issue))
- end
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
- it 'excludes award_emoji from comment count' do
- issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar')
- create(:award_emoji, awardable: issue)
+ it 'warns about version conflict' do
+ issue.update(title: "New title")
- visit project_issues_path(project, assignee_id: user.id)
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
- expect(page).to have_content 'foobar'
- expect(page.all('.no-comments').first.text).to eq "0"
- end
- end
+ click_button 'Save changes'
- describe 'Filter issue' do
- before do
- %w(foobar barbaz gitlab).each do |title|
- create(:issue,
- author: user,
- assignees: [user],
- project: project,
- title: title)
+ expect(page).to have_content 'Someone edited the issue the same time you did'
+ end
end
-
- @issue = Issue.find_by(title: 'foobar')
- @issue.milestone = create(:milestone, project: project)
- @issue.assignees = []
- @issue.save
end
- let(:issue) { @issue }
+ describe 'Issue info' do
+ it 'links to current issue in breadcrubs' do
+ issue = create(:issue, project: project)
- it 'allows filtering by issues with no specified assignee' do
- visit project_issues_path(project, assignee_id: IssuableFinder::NONE)
+ visit project_issue_path(project, issue)
- expect(page).to have_content 'foobar'
- expect(page).not_to have_content 'barbaz'
- expect(page).not_to have_content 'gitlab'
- end
+ expect(find('.breadcrumbs-sub-title a')[:href]).to end_with(issue_path(issue))
+ end
- it 'allows filtering by a specified assignee' do
- visit project_issues_path(project, assignee_id: user.id)
+ it 'excludes award_emoji from comment count' do
+ issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar')
+ create(:award_emoji, awardable: issue)
- expect(page).not_to have_content 'foobar'
- expect(page).to have_content 'barbaz'
- expect(page).to have_content 'gitlab'
- end
- end
+ visit project_issues_path(project, assignee_id: user.id)
- describe 'filter issue' do
- titles = %w[foo bar baz]
- titles.each_with_index do |title, index|
- let!(title.to_sym) do
- create(:issue, title: title,
- project: project,
- created_at: Time.now - (index * 60))
+ expect(page).to have_content 'foobar'
+ expect(page.all('.no-comments').first.text).to eq "0"
end
end
- let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') }
- let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') }
- it 'sorts by newest' do
- visit project_issues_path(project, sort: sort_value_created_date)
+ describe 'Filter issue' do
+ before do
+ %w(foobar barbaz gitlab).each do |title|
+ create(:issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ title: title)
+ end
- expect(first_issue).to include('foo')
- expect(last_issue).to include('baz')
- end
+ @issue = Issue.find_by(title: 'foobar')
+ @issue.milestone = create(:milestone, project: project)
+ @issue.assignees = []
+ @issue.save
+ end
- it 'sorts by most recently updated' do
- baz.updated_at = Time.now + 100
- baz.save
- visit project_issues_path(project, sort: sort_value_recently_updated)
+ let(:issue) { @issue }
- expect(first_issue).to include('baz')
- end
+ it 'allows filtering by issues with no specified assignee' do
+ visit project_issues_path(project, assignee_id: IssuableFinder::NONE)
- describe 'sorting by due date' do
- before do
- foo.update(due_date: 1.day.from_now)
- bar.update(due_date: 6.days.from_now)
+ expect(page).to have_content 'foobar'
+ expect(page).not_to have_content 'barbaz'
+ expect(page).not_to have_content 'gitlab'
end
- it 'sorts by due date' do
- visit project_issues_path(project, sort: sort_value_due_date)
+ it 'allows filtering by a specified assignee' do
+ visit project_issues_path(project, assignee_id: user.id)
- expect(first_issue).to include('foo')
+ expect(page).not_to have_content 'foobar'
+ expect(page).to have_content 'barbaz'
+ expect(page).to have_content 'gitlab'
end
+ end
- it 'sorts by due date by excluding nil due dates' do
- bar.update(due_date: nil)
+ describe 'filter issue' do
+ titles = %w[foo bar baz]
+ titles.each_with_index do |title, index|
+ let!(title.to_sym) do
+ create(:issue, title: title,
+ project: project,
+ created_at: Time.now - (index * 60))
+ end
+ end
+ let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') }
+ let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') }
- visit project_issues_path(project, sort: sort_value_due_date)
+ it 'sorts by newest' do
+ visit project_issues_path(project, sort: sort_value_created_date)
expect(first_issue).to include('foo')
+ expect(last_issue).to include('baz')
end
- context 'with a filter on labels' do
- let(:label) { create(:label, project: project) }
+ it 'sorts by most recently updated' do
+ baz.updated_at = Time.now + 100
+ baz.save
+ visit project_issues_path(project, sort: sort_value_recently_updated)
+ expect(first_issue).to include('baz')
+ end
+
+ describe 'sorting by due date' do
before do
- create(:label_link, label: label, target: foo)
+ foo.update(due_date: 1.day.from_now)
+ bar.update(due_date: 6.days.from_now)
+ end
+
+ it 'sorts by due date' do
+ visit project_issues_path(project, sort: sort_value_due_date)
+
+ expect(first_issue).to include('foo')
end
- it 'sorts by least recently due date by excluding nil due dates' do
+ it 'sorts by due date by excluding nil due dates' do
bar.update(due_date: nil)
- visit project_issues_path(project, label_names: [label.name], sort: sort_value_due_date_later)
+ visit project_issues_path(project, sort: sort_value_due_date)
expect(first_issue).to include('foo')
end
- end
- end
- describe 'filtering by due date' do
- before do
- foo.update(due_date: 1.day.from_now)
- bar.update(due_date: 6.days.from_now)
- end
+ context 'with a filter on labels' do
+ let(:label) { create(:label, project: project) }
+
+ before do
+ create(:label_link, label: label, target: foo)
+ end
+
+ it 'sorts by least recently due date by excluding nil due dates' do
+ bar.update(due_date: nil)
- it 'filters by none' do
- visit project_issues_path(project, due_date: Issue::NoDueDate.name)
+ visit project_issues_path(project, label_names: [label.name], sort: sort_value_due_date_later)
- 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')
+ expect(first_issue).to include('foo')
+ end
end
end
- it 'filters by any' do
- visit project_issues_path(project, due_date: Issue::AnyDueDate.name)
+ describe 'filtering by due date' do
+ before do
+ foo.update(due_date: 1.day.from_now)
+ bar.update(due_date: 6.days.from_now)
+ end
+
+ it 'filters by none' do
+ visit project_issues_path(project, due_date: Issue::NoDueDate.name)
- page.within '.issues-holder' do
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+ end
+
+ it 'filters by any' do
+ visit project_issues_path(project, due_date: Issue::AnyDueDate.name)
+
+ page.within '.issues-holder' do
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).to have_content('baz')
+ end
end
- end
- it 'filters by due this week' do
- foo.update(due_date: Date.today.beginning_of_week + 2.days)
- bar.update(due_date: Date.today.end_of_week)
- baz.update(due_date: Date.today - 8.days)
+ it 'filters by due this week' do
+ foo.update(due_date: Date.today.beginning_of_week + 2.days)
+ bar.update(due_date: Date.today.end_of_week)
+ baz.update(due_date: Date.today - 8.days)
- visit project_issues_path(project, due_date: Issue::DueThisWeek.name)
+ visit project_issues_path(project, due_date: Issue::DueThisWeek.name)
- page.within '.issues-holder' do
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).not_to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
end
- end
- it 'filters by due this month' do
- foo.update(due_date: Date.today.beginning_of_month + 2.days)
- bar.update(due_date: Date.today.end_of_month)
- baz.update(due_date: Date.today - 50.days)
+ it 'filters by due this month' do
+ foo.update(due_date: Date.today.beginning_of_month + 2.days)
+ bar.update(due_date: Date.today.end_of_month)
+ baz.update(due_date: Date.today - 50.days)
- visit project_issues_path(project, due_date: Issue::DueThisMonth.name)
+ visit project_issues_path(project, due_date: Issue::DueThisMonth.name)
- page.within '.issues-holder' do
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).not_to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
end
- end
- it 'filters by overdue' do
- foo.update(due_date: Date.today + 2.days)
- bar.update(due_date: Date.today + 20.days)
- baz.update(due_date: Date.yesterday)
+ it 'filters by overdue' do
+ foo.update(due_date: Date.today + 2.days)
+ bar.update(due_date: Date.today + 20.days)
+ baz.update(due_date: Date.yesterday)
- visit project_issues_path(project, due_date: Issue::Overdue.name)
+ visit project_issues_path(project, due_date: Issue::Overdue.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')
+ 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
- end
- describe 'sorting by milestone' do
- before do
- foo.milestone = newer_due_milestone
- foo.save
- bar.milestone = later_due_milestone
- bar.save
- end
+ describe 'sorting by milestone' do
+ before do
+ foo.milestone = newer_due_milestone
+ foo.save
+ bar.milestone = later_due_milestone
+ bar.save
+ end
- it 'sorts by milestone' do
- visit project_issues_path(project, sort: sort_value_milestone)
+ it 'sorts by milestone' do
+ visit project_issues_path(project, sort: sort_value_milestone)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('baz')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('baz')
+ end
end
- end
- describe 'combine filter and sort' do
- let(:user2) { create(:user) }
+ describe 'combine filter and sort' do
+ let(:user2) { create(:user) }
- before do
- foo.assignees << user2
- foo.save
- bar.assignees << user2
- bar.save
- end
+ before do
+ foo.assignees << user2
+ foo.save
+ bar.assignees << user2
+ bar.save
+ end
- it 'sorts with a filter applied' do
- visit project_issues_path(project, sort: sort_value_created_date, assignee_id: user2.id)
+ it 'sorts with a filter applied' do
+ visit project_issues_path(project, sort: sort_value_created_date, assignee_id: user2.id)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('bar')
- expect(page).not_to have_content('baz')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('bar')
+ expect(page).not_to have_content('baz')
+ end
end
end
- end
- describe 'when I want to reset my incoming email token' do
- let(:project1) { create(:project, namespace: user.namespace) }
- let!(:issue) { create(:issue, project: project1) }
+ describe 'when I want to reset my incoming email token' do
+ let(:project1) { create(:project, namespace: user.namespace) }
+ let!(:issue) { create(:issue, project: project1) }
- before do
- stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
- project1.team << [user, :master]
- visit namespace_project_issues_path(user.namespace, project1)
- end
+ before do
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
+ project1.add_master(user)
+ visit namespace_project_issues_path(user.namespace, project1)
+ end
- it 'changes incoming email address token', :js do
- find('.issuable-email-modal-btn').click
- previous_token = find('input#issuable_email').value
- find('.incoming-email-token-reset').click
+ it 'changes incoming email address token', :js do
+ find('.issuable-email-modal-btn').click
+ previous_token = find('input#issuable_email').value
+ find('.incoming-email-token-reset').click
- wait_for_requests
+ wait_for_requests
- expect(page).to have_no_field('issuable_email', with: previous_token)
- new_token = project1.new_issuable_address(user.reload, 'issue')
- expect(page).to have_field(
- 'issuable_email',
- with: new_token
- )
+ expect(page).to have_no_field('issuable_email', with: previous_token)
+ new_token = project1.new_issuable_address(user.reload, 'issue')
+ expect(page).to have_field(
+ 'issuable_email',
+ with: new_token
+ )
+ end
end
- end
- describe 'update labels from issue#show', :js do
- let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
- let!(:label) { create(:label, project: project) }
+ describe 'update labels from issue#show', :js do
+ let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
+ let!(:label) { create(:label, project: project) }
- before do
- visit project_issue_path(project, issue)
- end
+ before do
+ visit project_issue_path(project, issue)
+ end
- it 'will not send ajax request when no data is changed' do
- page.within '.labels' do
- click_link 'Edit'
+ it 'will not send ajax request when no data is changed' do
+ page.within '.labels' do
+ click_link 'Edit'
- find('.dropdown-menu-close', match: :first).click
+ find('.dropdown-menu-close', match: :first).click
- expect(page).not_to have_selector('.block-loading')
+ expect(page).not_to have_selector('.block-loading')
+ end
end
end
- end
- describe 'update assignee from issue#show' do
- let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
+ describe 'update assignee from issue#show' do
+ let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
- context 'by authorized user' do
- it 'allows user to select unassigned', :js do
- visit project_issue_path(project, issue)
+ context 'by authorized user' do
+ it 'allows user to select unassigned', :js do
+ visit project_issue_path(project, issue)
- page.within('.assignee') do
- expect(page).to have_content "#{user.name}"
+ page.within('.assignee') do
+ expect(page).to have_content "#{user.name}"
- click_link 'Edit'
- click_link 'Unassigned'
- first('.title').click
- expect(page).to have_content 'No assignee'
- end
+ click_link 'Edit'
+ click_link 'Unassigned'
+ first('.title').click
+ expect(page).to have_content 'No assignee'
+ end
- # wait_for_requests does not work with vue-resource at the moment
- sleep 1
+ # wait_for_requests does not work with vue-resource at the moment
+ sleep 1
- expect(issue.reload.assignees).to be_empty
- end
+ expect(issue.reload.assignees).to be_empty
+ end
- it 'allows user to select an assignee', :js do
- issue2 = create(:issue, project: project, author: user)
- visit project_issue_path(project, issue2)
+ it 'allows user to select an assignee', :js do
+ issue2 = create(:issue, project: project, author: user)
+ visit project_issue_path(project, issue2)
- page.within('.assignee') do
- expect(page).to have_content "No assignee"
- end
+ page.within('.assignee') do
+ expect(page).to have_content "No assignee"
+ end
- page.within '.assignee' do
- click_link 'Edit'
- end
+ page.within '.assignee' do
+ click_link 'Edit'
+ end
- page.within '.dropdown-menu-user' do
- click_link user.name
- end
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
- page.within('.assignee') do
- expect(page).to have_content user.name
+ page.within('.assignee') do
+ expect(page).to have_content user.name
+ end
end
- end
- it 'allows user to unselect themselves', :js do
- issue2 = create(:issue, project: project, author: user)
- visit project_issue_path(project, issue2)
+ it 'allows user to unselect themselves', :js do
+ issue2 = create(:issue, project: project, author: user)
+ visit project_issue_path(project, issue2)
- page.within '.assignee' do
- click_link 'Edit'
- click_link user.name
+ page.within '.assignee' do
+ click_link 'Edit'
+ click_link user.name
- page.within '.value .author' do
- expect(page).to have_content user.name
- end
+ page.within '.value .author' do
+ expect(page).to have_content user.name
+ end
- click_link 'Edit'
- click_link user.name
+ click_link 'Edit'
+ click_link user.name
- page.within '.value .assign-yourself' do
- expect(page).to have_content "No assignee"
+ page.within '.value .assign-yourself' do
+ expect(page).to have_content "No assignee"
+ end
end
end
end
- end
- context 'by unauthorized user' do
- let(:guest) { create(:user) }
+ context 'by unauthorized user' do
+ let(:guest) { create(:user) }
- before do
- project.team << [[guest], :guest]
- end
+ before do
+ project.add_guest(guest)
+ end
- it 'shows assignee text', :js do
- sign_out(:user)
- sign_in(guest)
+ it 'shows assignee text', :js do
+ sign_out(:user)
+ sign_in(guest)
- visit project_issue_path(project, issue)
- expect(page).to have_content issue.assignees.first.name
+ visit project_issue_path(project, issue)
+ expect(page).to have_content issue.assignees.first.name
+ end
end
end
- end
- describe 'update milestone from issue#show' do
- let!(:issue) { create(:issue, project: project, author: user) }
- let!(:milestone) { create(:milestone, project: project) }
+ describe 'update milestone from issue#show' do
+ let!(:issue) { create(:issue, project: project, author: user) }
+ let!(:milestone) { create(:milestone, project: project) }
- context 'by authorized user' do
- it 'allows user to select unassigned', :js do
- visit project_issue_path(project, issue)
+ context 'by authorized user' do
+ it 'allows user to select unassigned', :js do
+ visit project_issue_path(project, issue)
- page.within('.milestone') do
- expect(page).to have_content "None"
- end
+ page.within('.milestone') do
+ expect(page).to have_content "None"
+ end
- find('.block.milestone .edit-link').click
- sleep 2 # wait for ajax stuff to complete
- first('.dropdown-content li').click
- sleep 2
- page.within('.milestone') do
- expect(page).to have_content 'None'
+ find('.block.milestone .edit-link').click
+ sleep 2 # wait for ajax stuff to complete
+ first('.dropdown-content li').click
+ sleep 2
+ page.within('.milestone') do
+ expect(page).to have_content 'None'
+ end
+
+ expect(issue.reload.milestone).to be_nil
end
- expect(issue.reload.milestone).to be_nil
- end
+ it 'allows user to de-select milestone', :js do
+ visit project_issue_path(project, issue)
- it 'allows user to de-select milestone', :js do
- visit project_issue_path(project, issue)
+ page.within('.milestone') do
+ click_link 'Edit'
+ click_link milestone.title
- page.within('.milestone') do
- click_link 'Edit'
- click_link milestone.title
-
- page.within '.value' do
- expect(page).to have_content milestone.title
- end
+ page.within '.value' do
+ expect(page).to have_content milestone.title
+ end
- click_link 'Edit'
- click_link milestone.title
+ click_link 'Edit'
+ click_link milestone.title
- page.within '.value' do
- expect(page).to have_content 'None'
+ page.within '.value' do
+ expect(page).to have_content 'None'
+ end
end
end
end
- end
- context 'by unauthorized user' do
- let(:guest) { create(:user) }
+ context 'by unauthorized user' do
+ let(:guest) { create(:user) }
- before do
- project.team << [guest, :guest]
- issue.milestone = milestone
- issue.save
- end
+ before do
+ project.add_guest(guest)
+ issue.milestone = milestone
+ issue.save
+ end
- it 'shows milestone text', :js do
- sign_out(:user)
- sign_in(guest)
+ it 'shows milestone text', :js do
+ sign_out(:user)
+ sign_in(guest)
- visit project_issue_path(project, issue)
- expect(page).to have_content milestone.title
+ visit project_issue_path(project, issue)
+ expect(page).to have_content milestone.title
+ end
end
end
- end
- describe 'new issue' do
- let!(:issue) { create(:issue, project: project) }
+ describe 'new issue' do
+ let!(:issue) { create(:issue, project: project) }
- context 'by unauthenticated user' do
- before do
- sign_out(:user)
- end
+ context 'by unauthenticated user' do
+ before do
+ sign_out(:user)
+ end
- it 'redirects to signin then back to new issue after signin' do
- visit project_issues_path(project)
+ it 'redirects to signin then back to new issue after signin' do
+ visit project_issues_path(project)
- page.within '.nav-controls' do
- click_link 'New issue'
- end
+ page.within '.nav-controls' do
+ click_link 'New issue'
+ end
- expect(current_path).to eq new_user_session_path
+ expect(current_path).to eq new_user_session_path
- gitlab_sign_in(create(:user))
+ gitlab_sign_in(create(:user))
- expect(current_path).to eq new_project_issue_path(project)
+ expect(current_path).to eq new_project_issue_path(project)
+ end
end
- end
- context 'dropzone upload file', :js do
- before do
- visit new_project_issue_path(project)
- end
+ context 'dropzone upload file', :js do
+ before do
+ visit new_project_issue_path(project)
+ end
- it 'uploads file when dragging into textarea' do
- dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
+ it 'uploads file when dragging into textarea' do
+ dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
- expect(page.find_field("issue_description").value).to have_content 'banana_sample'
- end
+ expect(page.find_field("issue_description").value).to have_content 'banana_sample'
+ end
- it "doesn't add double newline to end of a single attachment markdown" do
- dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
+ it "doesn't add double newline to end of a single attachment markdown" do
+ dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
- expect(page.find_field("issue_description").value).not_to match /\n\n$/
- end
+ expect(page.find_field("issue_description").value).not_to match /\n\n$/
+ end
- it "cancels a file upload correctly" do
- slow_requests do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ it "cancels a file upload correctly" do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- click_button 'Cancel'
- end
+ click_button 'Cancel'
+ end
- expect(page).to have_button('Attach a file')
- expect(page).not_to have_button('Cancel')
- expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_button('Cancel')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
end
- end
- context 'form filled by URL parameters' do
- let(:project) { create(:project, :public, :repository) }
+ context 'form filled by URL parameters' do
+ let(:project) { create(:project, :public, :repository) }
- before do
- project.repository.create_file(
- user,
- '.gitlab/issue_templates/bug.md',
- 'this is a test "bug" template',
- message: 'added issue template',
- branch_name: 'master')
-
- visit new_project_issue_path(project, issuable_template: 'bug')
- end
+ before do
+ project.repository.create_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ 'this is a test "bug" template',
+ message: 'added issue template',
+ branch_name: 'master')
+
+ visit new_project_issue_path(project, issuable_template: 'bug')
+ end
- it 'fills in template' do
- expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug')
+ it 'fills in template' do
+ expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug')
+ end
end
end
- end
- describe 'new issue by email' do
- shared_examples 'show the email in the modal' do
- let(:issue) { create(:issue, project: project) }
+ describe 'new issue by email' do
+ shared_examples 'show the email in the modal' do
+ let(:issue) { create(:issue, project: project) }
- before do
- project.issues << issue
- stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
+ before do
+ project.issues << issue
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
- visit project_issues_path(project)
- click_button('Email a new issue')
- end
+ visit project_issues_path(project)
+ click_button('Email a new issue')
+ end
- it 'click the button to show modal for the new email' do
- page.within '#issuable-email-modal' do
- email = project.new_issuable_address(user, 'issue')
+ it 'click the button to show modal for the new email' do
+ page.within '#issuable-email-modal' do
+ email = project.new_issuable_address(user, 'issue')
- expect(page).to have_selector("input[value='#{email}']")
+ expect(page).to have_selector("input[value='#{email}']")
+ end
end
end
- end
- context 'with existing issues' do
- let!(:issue) { create(:issue, project: project, author: user) }
+ context 'with existing issues' do
+ let!(:issue) { create(:issue, project: project, author: user) }
- it_behaves_like 'show the email in the modal'
- end
+ it_behaves_like 'show the email in the modal'
+ end
- context 'without existing issues' do
- it_behaves_like 'show the email in the modal'
+ context 'without existing issues' do
+ it_behaves_like 'show the email in the modal'
+ end
end
- end
- describe 'due date' do
- context 'update due on issue#show', :js do
- let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
+ describe 'due date' do
+ context 'update due on issue#show', :js do
+ let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
- before do
- visit project_issue_path(project, issue)
- end
+ before do
+ visit project_issue_path(project, issue)
+ end
- it 'adds due date to issue' do
- date = Date.today.at_beginning_of_month + 2.days
+ it 'adds due date to issue' do
+ date = Date.today.at_beginning_of_month + 2.days
- page.within '.due_date' do
- click_link 'Edit'
+ page.within '.due_date' do
+ click_link 'Edit'
- page.within '.pika-single' do
- click_button date.day
- end
+ page.within '.pika-single' do
+ click_button date.day
+ end
- wait_for_requests
+ wait_for_requests
- expect(find('.value').text).to have_content date.strftime('%b %-d, %Y')
+ expect(find('.value').text).to have_content date.strftime('%b %-d, %Y')
+ end
end
- end
- it 'removes due date from issue' do
- date = Date.today.at_beginning_of_month + 2.days
+ it 'removes due date from issue' do
+ date = Date.today.at_beginning_of_month + 2.days
- page.within '.due_date' do
- click_link 'Edit'
+ page.within '.due_date' do
+ click_link 'Edit'
- page.within '.pika-single' do
- click_button date.day
- end
+ page.within '.pika-single' do
+ click_button date.day
+ end
- wait_for_requests
+ wait_for_requests
- expect(page).to have_no_content 'No due date'
+ expect(page).to have_no_content 'No due date'
- click_link 'remove due date'
- expect(page).to have_content 'No due date'
+ click_link 'remove due date'
+ expect(page).to have_content 'No due date'
+ end
end
end
end
- end
- describe 'title issue#show', :js do
- it 'updates the title', :js do
- issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title')
+ describe 'title issue#show', :js do
+ it 'updates the title', :js do
+ issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title')
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, issue)
- expect(page).to have_text("new title")
+ expect(page).to have_text("new title")
- issue.update(title: "updated title")
+ issue.update(title: "updated title")
- wait_for_requests
- expect(page).to have_text("updated title")
+ wait_for_requests
+ expect(page).to have_text("updated title")
+ end
end
- end
- describe 'confidential issue#show', :js do
- it 'shows confidential sibebar information as confidential and can be turned off' do
- issue = create(:issue, :confidential, project: project)
+ describe 'confidential issue#show', :js do
+ it 'shows confidential sibebar information as confidential and can be turned off' do
+ issue = create(:issue, :confidential, project: project)
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, issue)
- expect(page).to have_css('.issuable-note-warning')
- expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
- expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
+ expect(page).to have_css('.issuable-note-warning')
+ expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
+ expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
- find('.confidential-edit').click
- expect(page).to have_css('.sidebar-item-warning-message')
+ find('.confidential-edit').click
+ expect(page).to have_css('.sidebar-item-warning-message')
- within('.sidebar-item-warning-message') do
- find('.btn-close').click
- end
+ within('.sidebar-item-warning-message') do
+ find('.btn-close').click
+ end
- wait_for_requests
+ wait_for_requests
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, issue)
- expect(page).not_to have_css('.is-active')
+ expect(page).not_to have_css('.is-active')
+ end
end
end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index e285befc66f..a2b78a5e021 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -71,7 +71,7 @@ describe 'GitLab Markdown' do
it 'parses mermaid code block' do
aggregate_failures do
- expect(doc).to have_selector('pre.code.js-render-mermaid')
+ expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid')
end
end
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
index d49d145f254..b2d64a62b4f 100644
--- a/spec/features/merge_requests/assign_issues_spec.rb
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -9,7 +9,7 @@ feature 'Merge request issue assignment', :js do
let(:service) { MergeRequests::AssignIssuesService.new(merge_request, user, user, project) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
def visit_merge_request(current_user = nil)
diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
index fbbfe7942be..892c32c8806 100644
--- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
+++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
@@ -7,7 +7,7 @@ feature 'Check if mergeable with unresolved discussions', :js do
before do
sign_in user
- project.team << [user, :master]
+ project.add_master(user)
end
context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
index 48f370c3ad4..205e38337d1 100644
--- a/spec/features/merge_requests/cherry_pick_spec.rb
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -8,7 +8,7 @@ describe 'Cherry-pick Merge Requests', :js do
before do
sign_in user
- project.team << [user, :master]
+ project.add_master(user)
end
context "Viewing a merged merge request" do
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index 4dd4e40f52c..55de9a01ed5 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -18,7 +18,7 @@ feature 'Merge Request closing issues message', :js do
let(:merge_request_title) { 'Merge Request Title' }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 4e2963c116d..05d99a2dff2 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -88,7 +88,7 @@ feature 'Merge request conflict resolution', :js do
context 'can be resolved in the UI' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
@@ -175,7 +175,7 @@ feature 'Merge request conflict resolution', :js do
let(:merge_request) { create_merge_request(source_branch) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index db5ce2d11a8..486555ed5cd 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -5,7 +5,7 @@ feature 'Create New Merge Request', :js do
let(:project) { create(:project, :public, :repository) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index ca2225318cd..53b62caf743 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -14,7 +14,7 @@ feature 'Merge request created from fork' do
end
background do
- forked_project.team << [user, :master]
+ forked_project.add_master(user)
sign_in user
end
diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
index 7f69e82af4c..56aa0b2ede2 100644
--- a/spec/features/merge_requests/deleted_source_branch_spec.rb
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -9,7 +9,7 @@ describe 'Deleted source branch', :js do
before do
sign_in user
- merge_request.project.team << [user, :master]
+ merge_request.project.add_master(user)
merge_request.update!(source_branch: 'this-branch-does-not-exist')
visit project_merge_request_path(merge_request.project, merge_request)
end
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 9e816cf041b..ef8f314cc03 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -19,7 +19,7 @@ feature 'Diff note avatars', :js do
let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
set_cookie('sidebar_collapsed', 'true')
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 15d380b1bf4..9d4194d8ca0 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -18,7 +18,7 @@ feature 'Diff notes resolve', :js do
context 'no discussions' do
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
note.destroy
visit_merge_request
@@ -32,7 +32,7 @@ feature 'Diff notes resolve', :js do
context 'as authorized user' do
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit_merge_request
end
@@ -429,7 +429,7 @@ feature 'Diff notes resolve', :js do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
sign_in guest
end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 4362f8b3fcc..79be2fbf945 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -6,7 +6,7 @@ feature 'Edit Merge Request' do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 8b9ff9be943..8db94352f73 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -14,7 +14,7 @@ feature 'Merge Request filtering by Milestone' do
end
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 1dcc1e139a0..1ebf762a006 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -12,8 +12,8 @@ describe 'New/edit merge request', :js do
let!(:label2) { create(:label, project: project) }
before do
- project.team << [user, :master]
- project.team << [user2, :master]
+ project.add_master(user)
+ project.add_master(user2)
end
context 'owned projects' do
@@ -172,7 +172,7 @@ describe 'New/edit merge request', :js do
context 'forked project' do
before do
- forked_project.team << [user, :master]
+ forked_project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes_spec.rb
index 3c53b51e330..d0f8da4e6cd 100644
--- a/spec/features/merge_requests/image_diff_notes.rb
+++ b/spec/features/merge_requests/image_diff_notes_spec.rb
@@ -7,14 +7,13 @@ feature 'image diff notes', :js do
let(:project) { create(:project, :public, :repository) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
- page.driver.set_cookie('sidebar_collapsed', 'true')
-
# Stub helper to return any blob file as image from public app folder.
# This is necessary to run this specs since we don't display repo images in capybara.
- allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_path).and_return('/apple-touch-icon.png')
+ 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')
end
context 'create commit diff notes' do
@@ -141,13 +140,13 @@ feature 'image diff notes', :js do
end
it 'allows expanding/collapsing the discussion notes' do
- page.all('.js-diff-notes-toggle')[0].trigger('click')
- page.all('.js-diff-notes-toggle')[1].trigger('click')
+ page.all('.js-diff-notes-toggle')[0].click
+ page.all('.js-diff-notes-toggle')[1].click
expect(page).not_to have_content('image diff test comment')
- page.all('.js-diff-notes-toggle')[0].trigger('click')
- page.all('.js-diff-notes-toggle')[1].trigger('click')
+ page.all('.js-diff-notes-toggle')[0].click
+ page.all('.js-diff-notes-toggle')[1].click
expect(page).to have_content('image diff test comment')
end
@@ -185,12 +184,42 @@ feature 'image diff notes', :js do
expect(page).to have_content(diff_note.note)
end
end
-end
-def create_image_diff_note
- find('.js-add-image-diff-note-button', match: :first).click
- page.all('.js-add-image-diff-note-button')[0].trigger('click')
- find('.diff-content .note-textarea').native.send_keys('image diff test comment')
- click_button 'Comment'
- wait_for_requests
+ describe 'image view modes' do
+ before do
+ visit project_commit_path(project, '2f63565e7aac07bcdadb654e253078b727143ec4')
+ end
+
+ it 'resizes image in onion skin view mode' do
+ find('.view-modes-menu .onion-skin').click
+
+ expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
+ end
+
+ it 'resets onion skin view mode opacity when toggling between view modes' do
+ find('.view-modes-menu .onion-skin').click
+
+ # Simulate dragging onion-skin slider
+ drag_and_drop_by(find('.dragger'), -30, 0)
+
+ expect(find('.onion-skin-frame .frame.added', visible: false)['style']).not_to match('opacity: 1;')
+
+ find('.view-modes-menu .swipe').click
+ find('.view-modes-menu .onion-skin').click
+
+ expect(find('.onion-skin-frame .frame.added', visible: false)['style']).to match('opacity: 1;')
+ end
+ end
+
+ def drag_and_drop_by(element, right_by, down_by)
+ page.driver.browser.action.drag_and_drop_by(element.native, right_by, down_by).perform
+ end
+
+ def create_image_diff_note
+ find('.js-add-image-diff-note-button', match: :first).click
+ page.all('.js-add-image-diff-note-button')[0].click
+ find('.diff-content .note-textarea').native.send_keys('image diff test comment')
+ click_button 'Comment'
+ wait_for_requests
+ end
end
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
index 82b2b56ef80..ddd034e1376 100644
--- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -32,7 +32,7 @@ feature 'Clicking toggle commit message link', :js do
end
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
index 0b5a595acce..e1317b33ad1 100644
--- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -19,7 +19,7 @@ feature 'Merge immediately', :js do
end
before do
- project.team << [user, :master]
+ project.add_master(user)
end
context 'when there is active pipeline for merge request' do
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index 93c5e945453..a7e7c0eeff6 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -15,8 +15,8 @@ feature 'Mini Pipeline Graph', :js do
visit_merge_request
end
- def visit_merge_request(format = :html)
- visit project_merge_request_path(project, merge_request, format: format)
+ def visit_merge_request(format: :html, serializer: nil)
+ visit project_merge_request_path(project, merge_request, format: format, serializer: serializer)
end
it 'should display a mini pipeline graph' do
@@ -33,12 +33,12 @@ feature 'Mini Pipeline Graph', :js do
end
it 'avoids repeated database queries' do
- before = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) }
+ before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
create(:ci_build, pipeline: pipeline, when: 'manual')
- after = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) }
+ after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
expect(before.count).to eq(after.count)
expect(before.cached_count).to eq(after.cached_count)
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 91f207bd339..7d9282b932b 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -7,7 +7,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', :js d
before do
sign_in merge_request.author
- project.team << [merge_request.author, :master]
+ project.add_master(merge_request.author)
end
context 'project does not have CI enabled', :js do
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index 307c860eac4..04e3f4bdcf1 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -7,7 +7,7 @@ feature 'Pipelines for Merge Requests', :js do
given(:project) { merge_request.target_project }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index eed95816bdf..daca4422bf1 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -17,7 +17,7 @@ feature 'Merge requests filter clear button', :js do
before do
mr2.labels << bug
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when a milestone filter has been applied' do
diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb
index bce36e05e57..d9f7a056dea 100644
--- a/spec/features/merge_requests/target_branch_spec.rb
+++ b/spec/features/merge_requests/target_branch_spec.rb
@@ -11,7 +11,7 @@ describe 'Target branch', :js do
before do
sign_in user
- project.team << [user, :master]
+ project.add_master(user)
end
context 'when branch was deleted' do
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index c5498563b39..a96404b86ed 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -6,7 +6,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index f4c75a2f265..e17e9c2ccf5 100644
--- a/spec/features/merge_requests/user_posts_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -66,6 +66,21 @@ describe 'Merge requests > User posts notes', :js do
end
end
+ describe 'when previewing a note' do
+ it 'shows the toolbar buttons when editing a note' do
+ page.within('.js-main-target-form') do
+ expect(page).to have_css('.md-header-toolbar.active')
+ end
+ end
+
+ it 'hides the toolbar buttons when previewing a note' do
+ find('.js-md-preview-button').click
+ page.within('.js-main-target-form') do
+ expect(page).not_to have_css('.md-header-toolbar.active')
+ end
+ end
+ 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)
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index ee0766f1192..5874bf5e187 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -15,7 +15,7 @@ feature 'Merge Requests > User uses quick actions', :js do
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
@@ -58,7 +58,7 @@ feature 'Merge Requests > User uses quick actions', :js do
context 'when the current user cannot toggle the WIP prefix' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
@@ -104,7 +104,7 @@ feature 'Merge Requests > User uses quick actions', :js do
context 'when the current user cannot merge the MR' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
@@ -134,7 +134,7 @@ feature 'Merge Requests > User uses quick actions', :js do
before do
sign_out(:user)
- another_project.team << [user, :master]
+ another_project.add_master(user)
sign_in(user)
end
@@ -188,7 +188,7 @@ feature 'Merge Requests > User uses quick actions', :js do
context 'when current user can not change target branch' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index 72a52c979b3..ec2da72ddff 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -13,7 +13,7 @@ feature 'Widget Deployments Header', :js do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 3ee094c216e..8970586a160 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -273,7 +273,7 @@ describe 'Merge request', :js do
let(:user2) { create(:user) }
before do
- project.team << [user2, :master]
+ project.add_master(user2)
sign_out(:user)
sign_in(user2)
merge_request.update(target_project: fork_project)
diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_requests/wip_message_spec.rb
index b422c76249d..2617e735c25 100644
--- a/spec/features/merge_requests/wip_message_spec.rb
+++ b/spec/features/merge_requests/wip_message_spec.rb
@@ -5,7 +5,7 @@ feature 'Work In Progress help message' do
let!(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 27efc32c95b..b02d2d4261c 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -7,7 +7,7 @@ feature 'Milestone' do
before do
create(:group_member, group: group, user: user)
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -82,9 +82,9 @@ feature 'Milestone' do
milestone = create(:milestone, project: project, title: 8.7)
issue1 = create(:issue, project: project, milestone: milestone)
issue2 = create(:issue, project: project, milestone: milestone)
- issue1.spend_time(duration: 3600, user: user)
+ issue1.spend_time(duration: 3600, user_id: user.id)
issue1.save!
- issue2.spend_time(duration: 7200, user: user)
+ issue2.spend_time(duration: 7200, user_id: user.id)
issue2.save!
visit project_milestone_path(project, milestone)
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index b45972b7f6b..73a526c3d8a 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -33,6 +33,25 @@ feature 'Password reset' do
end
end
+ describe 'Changing password while logged in' do
+ it 'updates the password' do
+ user = create(:user)
+ token = user.send_reset_password_instructions
+
+ sign_in(user)
+
+ visit(edit_user_password_path(reset_password_token: token))
+
+ fill_in 'New password', with: 'hello1234'
+ fill_in 'Confirm new password', with: 'hello1234'
+
+ click_button 'Change your password'
+
+ expect(page).to have_content(I18n.t('devise.passwords.updated_not_active'))
+ expect(current_path).to eq new_user_session_path
+ end
+ end
+
def forgot_password(user)
visit root_path
click_on 'Forgot your password?'
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index c60883911f7..0848857ed1e 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -25,7 +25,7 @@ describe 'Profile account page', :js do
fill_in 'password', with: '12345678'
- page.within '.popup-dialog' do
+ page.within '.modal' do
click_button 'Delete account'
end
@@ -38,7 +38,7 @@ describe 'Profile account page', :js do
fill_in 'password', with: 'testing123'
- page.within '.popup-dialog' do
+ page.within '.modal' do
click_button 'Delete account'
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index 7d5ba3a7328..b04a5422fed 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -27,6 +27,7 @@ feature 'Profile > SSH Keys' do
expect(page).to have_content("Title: #{attrs[:title]}")
expect(page).to have_content(attrs[:key])
+ expect(find('.breadcrumbs-sub-title')).to have_link(attrs[:title])
end
context 'when only DSA and ECDSA keys are allowed' do
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index d1edeef8da4..7d204f89fba 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -2,12 +2,20 @@ require 'spec_helper'
describe 'Profile > Applications' do
let(:user) { create(:user) }
+ let(:application) { create(:oauth_application, owner: user) }
before do
sign_in(user)
end
describe 'User manages applications', :js do
+ it 'views an application' do
+ visit oauth_application_path(application)
+
+ expect(page).to have_content("Application: #{application.name}")
+ expect(find('.breadcrumbs-sub-title')).to have_link(application.name)
+ end
+
it 'deletes an application' do
create(:oauth_application, owner: user)
visit oauth_applications_path
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index df89918f17a..1952fdae798 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -5,7 +5,7 @@ feature 'User visits the notifications tab', :js do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit(profile_notifications_path)
end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 90d6841af0e..266af8f4e3d 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -32,6 +32,18 @@ describe 'User visits the profile preferences page' do
end
end
+ describe 'User changes their multi file editor preferences', :js do
+ it 'set the new_repo cookie when the option is ON' do
+ choose 'user_multi_file_on'
+ expect(get_cookie('new_repo')).not_to be_nil
+ end
+
+ it 'deletes the new_repo cookie when the option is OFF' do
+ choose 'user_multi_file_off'
+ expect(get_cookie('new_repo')).to be_nil
+ end
+ end
+
describe 'User changes their default dashboard', :js do
it 'creates a flash message' do
select 'Starred Projects', from: 'user_dashboard'
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
index 84c2faa2015..2693e539268 100644
--- a/spec/features/projects/activity/rss_spec.rb
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -11,7 +11,7 @@ feature 'Project Activity RSS' do
context 'when signed in' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit path
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index c68e10a2563..821ce88a402 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -6,7 +6,7 @@ feature 'test coverage badge' do
context 'when user has access to view badge' do
background do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 68c4a647958..c705e479690 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -4,7 +4,7 @@ feature 'list of badges' do
background do
user = create(:user)
project = create(:project, :repository)
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_pipelines_settings_path(project)
end
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 965028a6f90..69e4c9f04a1 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -13,7 +13,7 @@ feature 'Editing file blob', :js do
let(:role) { :developer }
before do
- project.team << [user, role]
+ project.add_role(user, role)
sign_in(user)
end
@@ -55,7 +55,7 @@ feature 'Editing file blob', :js do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
visit project_edit_blob_path(project, tree_join(branch, file_path))
end
@@ -90,7 +90,7 @@ feature 'Editing file blob', :js do
let(:protected_branch) { 'protected-branch' }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
project.repository.add_branch(user, protected_branch, 'master')
create(:protected_branch, project: project, name: protected_branch)
sign_in(user)
@@ -122,7 +122,7 @@ feature 'Editing file blob', :js do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_edit_blob_path(project, tree_join(branch, file_path))
end
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 2f407b13c2f..39bcea013e7 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -23,7 +23,7 @@ feature 'Download buttons in branches page' do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
end
describe 'when checking branches' do
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 7a77df83034..2fddd274078 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -8,7 +8,7 @@ describe 'Branches' do
context 'logged in as developer' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe 'Initial branches page' do
@@ -78,7 +78,7 @@ describe 'Branches' do
context 'logged in as master' do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(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 b34cd061ec6..9c4abec115f 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -73,7 +73,7 @@ feature 'Clusters Applications', :js do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
- create(:cluster_applications_helm, :installed, cluster: cluster)
+ create(:clusters_applications_helm, :installed, cluster: cluster)
page.within('.js-cluster-application-row-ingress') do
page.find(:css, '.js-cluster-application-install-button').click
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 67b8901f8fb..523cc08496b 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -20,105 +20,126 @@ feature 'Gcp Cluster', :js do
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
end
- context 'when user does not have a cluster and visits cluster index page' do
+ context 'when user has a GCP project with billing enabled' do
before do
- visit project_clusters_path(project)
-
- click_link 'Add cluster'
- click_link 'Create on GKE'
+ allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
+ stub_google_project_billing_status
end
- context 'when user filled form with valid parameters' do
+ context 'when user does not have a cluster and visits cluster index page' 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'
- )
+ visit project_clusters_path(project)
+
+ click_link 'Add cluster'
+ click_link 'Create on GKE'
+ 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 cluster'
end
- allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+ it 'user sees a cluster details page and creation status' do
+ expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create cluster'
- end
+ Clusters::Cluster.last.provider.make_created!
- it 'user sees a cluster details page and creation status' do
- expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
+ expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine')
+ end
- Clusters::Cluster.last.provider.make_created!
+ it 'user sees a error if something worng during creation' do
+ expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
- expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine')
- end
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
- it 'user sees a error if something worng during creation' do
- expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
+ expect(page).to have_content('Something wrong!')
+ end
+ end
- Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Create cluster'
+ end
- expect(page).to have_content('Something wrong!')
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
end
end
- context 'when user filled form with invalid parameters' do
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
before do
- click_button 'Create cluster'
+ visit project_cluster_path(project, cluster)
end
- it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ 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
- end
- context 'when user does have a cluster and visits cluster page' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ page.within('#cluster-integration') { click_button 'Save changes' }
+ end
- before do
- visit project_cluster_path(project, cluster)
- end
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
- it 'user sees a cluster details page' do
- expect(page).to have_button('Save')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
- end
+ 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
- context 'when user disables the cluster' do
- before do
- page.find(:css, '.js-toggle-cluster').click
- click_button 'Save'
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
+ end
end
- it 'user sees the successful message' do
- expect(page).to have_content('Cluster was successfully updated.')
+ 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('Cluster integration was successfully removed.')
+ expect(page).to have_link('Add cluster')
+ end
end
end
+ end
- context 'when user changes cluster parameters' do
- before do
- fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
- click_button 'Save changes'
- end
+ context 'when user does not have a GCP project with billing enabled' do
+ before do
+ visit project_clusters_path(project)
- it 'user sees the successful message' do
- expect(page).to have_content('Cluster was successfully updated.')
- expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
- end
+ click_link 'Add cluster'
+ click_link 'Create on GKE'
end
- 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('Cluster integration was successfully removed.')
- expect(page).to have_link('Add cluster')
- end
+ it 'user sees a check page' do
+ pending 'the frontend still has not been implemented'
+ expect(page).to have_link('Continue')
end
end
end
diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb
index 01f9526608f..3ddb35c755c 100644
--- a/spec/features/projects/clusters/interchangeability_spec.rb
+++ b/spec/features/projects/clusters/interchangeability_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'Interchangeability between KubernetesService and Platform::Kubernetes' do
- EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url=].freeze
+ EXCEPT_METHODS = %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
it 'Clusters::Platform::Kubernetes covers core interfaces in KubernetesService' do
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 414f4acba86..a519b9f9c7e 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -29,7 +29,7 @@ feature 'User Cluster', :js do
end
it 'user sees a cluster details page' do
- expect(page).to have_content('Enable cluster integration')
+ expect(page).to have_content('Cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com')
@@ -57,14 +57,14 @@ feature 'User Cluster', :js do
end
it 'user sees a cluster details page' do
- expect(page).to have_button('Save')
+ expect(page).to have_button('Save changes')
end
context 'when user disables the cluster' do
before do
page.find(:css, '.js-toggle-cluster').click
fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Save'
+ page.within('#cluster-integration') { click_button 'Save changes' }
end
it 'user sees the successful message' do
@@ -76,7 +76,7 @@ feature 'User Cluster', :js do
before do
fill_in 'cluster_name', with: 'my-dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
- click_button 'Save changes'
+ page.within('#js-cluster-details') { click_button 'Save changes' }
end
it 'user sees the successful message' do
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 008bdf2044b..eae2910a8f6 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -35,17 +35,6 @@ feature 'Clusters', :js do
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end
- it 'user sees navigation tabs' do
- expect(page.find('.js-active-tab').text).to include('Active')
- expect(page.find('.js-active-tab .badge').text).to include('1')
-
- expect(page.find('.js-inactive-tab').text).to include('Inactive')
- expect(page.find('.js-inactive-tab .badge').text).to include('0')
-
- expect(page.find('.js-all-tab').text).to include('All')
- expect(page.find('.js-all-tab .badge').text).to include('1')
- end
-
context 'inline update of cluster' do
it 'user can update cluster' do
expect(page).to have_selector('.js-toggle-cluster-list')
@@ -88,4 +77,18 @@ feature 'Clusters', :js do
end
end
end
+
+ context 'when user has not signed in Google' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add cluster'
+ click_link 'Create on GKE'
+ end
+
+ it 'user sees a login page' do
+ expect(page).to have_css('.signin-with-google')
+ expect(page).to have_link('Google account')
+ end
+ end
end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 79e84a4f0a6..36a746ac83d 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -5,7 +5,7 @@ feature 'project commit pipelines', :js do
background do
user = create(:user)
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index c11a95732b2..c4c399e3058 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.team << [user, :master]
+ project.add_master(user)
visit project_commit_path(project, master_pickable_commit.id)
end
diff --git a/spec/features/projects/commits/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb
index db958346f06..0d9c7355ddd 100644
--- a/spec/features/projects/commits/rss_spec.rb
+++ b/spec/features/projects/commits/rss_spec.rb
@@ -7,7 +7,7 @@ feature 'Project Commits RSS' do
context 'when signed in' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit path
end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 87ffc2a0b90..1fb22fd0e4c 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.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_compare_index_path(project, from: "master", to: "master")
end
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index e445758cb5e..886c56e7163 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index 36809240f76..bf55917bf4c 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -5,7 +5,7 @@ feature 'Developer views empty project instructions' do
let(:developer) { create(:user) }
background do
- project.team << [developer, :developer]
+ project.add_developer(developer)
sign_in(developer)
end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index 7a372757523..1d4b4d0fdca 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -7,7 +7,7 @@ feature 'Project edit', :js do
context 'feature visibility' do
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit edit_project_path(project)
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index dfcf97ad495..64e600144e0 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -7,7 +7,7 @@ feature 'Environment' do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
end
feature 'environment details page' do
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 4a05313c14a..5248a783db4 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -6,7 +6,7 @@ feature 'Environments page', :js do
given(:role) { :developer }
background do
- project.team << [user, role]
+ project.add_role(user, role)
sign_in(user)
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 033c45a60bf..b0eb7c5b42a 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.team << [member, :master]
+ project.add_master(member)
sign_in(member)
end
@@ -165,7 +165,7 @@ describe 'Edit Project Settings' do
describe 'repository visibility', :js do
before do
- project.team << [member, :master]
+ project.add_master(member)
sign_in(member)
visit edit_project_path(project)
end
@@ -261,7 +261,7 @@ describe 'Edit Project Settings' do
let!(:project) { create(:project, :private) }
before do
- project.team << [member, :guest]
+ project.add_guest(member)
sign_in(member)
visit project_path(project)
end
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index 84197e45dcb..2c38c380d9d 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -5,7 +5,7 @@ feature 'user browses project', :js do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_tree_path(project, project.default_branch)
end
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
index e1852a6e544..8d982636525 100644
--- a/spec/features/projects/files/creating_a_file_spec.rb
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -5,7 +5,7 @@ feature 'User wants to create a file' do
let(:user) { create(:user) }
background do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_new_blob_path(project, project.default_branch)
end
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 3c3a5326538..f4a39e331fd 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -5,7 +5,7 @@ feature 'User wants to add a Dockerfile file' do
before do
user = create(:user)
project = create(:project, :repository)
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index d2382d55c0b..2101627f324 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -23,7 +23,7 @@ feature 'Download buttons in files tree' do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
end
describe 'when files tree' do
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index 3ab43b3c656..8d32ada5795 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -4,7 +4,7 @@ feature 'User uses soft wrap whilst editing file', :js do
before do
user = create(:user)
project = create(:project, :repository)
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
page.within('.file-editor.code') do
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index 20be968e89f..d874cdbff8d 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -16,7 +16,7 @@ feature 'User wants to edit a file' do
end
background do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_edit_blob_path(project,
File.join(project.default_branch, '.gitignore'))
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
index 702b99de733..ead9f7e9168 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -5,7 +5,7 @@ feature 'User views files page' do
let(:project) { create(:forked_project_with_submodules) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_tree_path(project, project.repository.root_ref)
end
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 618725ee781..e9ff06c72d8 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -5,7 +5,7 @@ feature 'Find file keyboard shortcuts', :js do
let(:project) { create(:project, :repository) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_find_file_path(project, project.repository.root_ref)
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index 81d68c3d67c..79f3fd09b48 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -4,7 +4,7 @@ feature 'User wants to add a .gitignore file' do
before do
user = create(:user)
project = create(:project, :repository)
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_new_blob_path(project, 'master', file_name: '.gitignore')
end
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index 8e58fa7bd56..db6c67b802e 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -4,7 +4,7 @@ feature 'User wants to add a .gitlab-ci.yml file' do
before do
user = create(:user)
project = create(:project, :repository)
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_new_blob_path(project, 'master', file_name: '.gitlab-ci.yml')
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 6c5b1086ec1..07599600876 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
@@ -6,7 +6,7 @@ feature 'project owner creates a license file', :js do
background do
project.repository.delete_file(project_master, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
- project.team << [project_master, :master]
+ project.add_master(project_master)
sign_in(project_master)
visit project_path(project)
end
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 6c616bf0456..8ac9821b879 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,15 +2,15 @@ require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', :js do
let(:project_master) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project_empty_repo) }
+
background do
- project.team << [project_master, :master]
+ project.add_master(project_master)
sign_in(project_master)
end
scenario 'project master creates a license file from a template' do
visit project_path(project)
- click_link 'Create empty bare repository'
click_on 'LICENSE'
expect(page).to have_content('New file')
@@ -26,8 +26,6 @@ feature 'project owner sees a link to create a license file in empty project', :
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
- # Remove pre-receive hook so we can push without auth
- FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
click_button 'Commit changes'
expect(current_path).to eq(
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index f95a60e5194..97408a9c41e 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -5,7 +5,7 @@ feature 'Template type dropdown selector', :js do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 64fe350f3dc..fbf35fb4e1c 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -5,7 +5,7 @@ feature 'Template Undo Button', :js do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
index 98c7ef57a51..199682b943c 100644
--- a/spec/features/projects/guest_navigation_menu_spec.rb
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -5,7 +5,7 @@ describe 'Guest navigation menu' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
sign_in(guest)
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 461aa39d0ad..6732cf61767 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
# Integration test that exports a file using the Import/Export feature
# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
-# we''l have to either include it adding the model that includes it to the +safe_list+
+# 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
include Select2Helper
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 0c354298433..0cc68aff494 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 0257cd157c9..e26caf1f456 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -8,7 +8,7 @@ feature 'issuable templates', :js do
let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
end
@@ -32,9 +32,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.js-issuable-actions') do
- click_on 'Edit'
- end
+ page.find('.js-issuable-edit').click
fill_in :'issuable-title', with: 'test issue title'
end
@@ -77,9 +75,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.js-issuable-actions') do
- click_on 'Edit'
- end
+ page.find('.js-issuable-edit').click
fill_in :'issuable-title', with: 'test issue title'
fill_in :'issue-description', with: prior_description
end
@@ -124,7 +120,7 @@ feature 'issuable templates', :js do
background do
sign_out(:user)
- project.team << [fork_user, :developer]
+ project.add_developer(fork_user)
sign_in(fork_user)
diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb
index 58eeef8c258..ff91aabc311 100644
--- a/spec/features/projects/issues/rss_spec.rb
+++ b/spec/features/projects/issues/rss_spec.rb
@@ -12,7 +12,7 @@ feature 'Project Issues RSS' do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit path
end
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 5d9208ebadd..4c49cff30d4 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'User browses a job', :js do
- let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) }
+ let!(:build) { create(:ci_build, :running, :coverage, pipeline: pipeline) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 0b0d5a2dce8..9a6b27c00f8 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -15,7 +15,7 @@ feature 'Jobs' do
end
before do
- project.team << [user, user_access_level]
+ project.add_role(user, user_access_level)
sign_in(user)
end
@@ -369,6 +369,34 @@ feature 'Jobs' do
end
end
end
+
+ context 'Playable manual action' do
+ let(:job) { create(:ci_build, :playable, pipeline: pipeline) }
+
+ before do
+ project.add_developer(user)
+ visit project_job_path(project, job)
+ end
+
+ it 'shows manual action empty state' do
+ expect(page).to have_content('This job requires a manual action')
+ expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.')
+ expect(page).to have_link('Trigger this manual action')
+ end
+ end
+
+ context 'Non triggered job' do
+ let(:job) { create(:ci_build, :created, pipeline: pipeline) }
+
+ before do
+ visit project_job_path(project, job)
+ end
+
+ it 'shows manual action empty state' do
+ expect(page).to have_content('This job has not been triggered yet')
+ expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered.')
+ end
+ end
end
describe "POST /:project/jobs/:id/cancel", :js do
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
index e8c70dec854..70e8d436dcb 100644
--- a/spec/features/projects/labels/subscription_spec.rb
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -9,7 +9,7 @@ feature 'Labels subscription' do
context 'when signed in' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in user
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index d063f5c27b5..85bd776932b 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -12,7 +12,7 @@ feature 'Prioritize labels' do
context 'when user belongs to project team' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in user
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
new file mode 100644
index 00000000000..aa56fd7f74e
--- /dev/null
+++ b/spec/features/projects/labels/user_sees_links_to_issuables.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+feature 'Projects > Labels > User sees links to issuables' do
+ set(:user) { create(:user) }
+
+ before do
+ label # creates the label
+ project.add_developer(user)
+ sign_in user
+ visit project_labels_path(project)
+ end
+
+ context 'with a project label' do
+ let(:label) { create(:label, project: project, title: 'bug') }
+
+ 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
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+
+ 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
+ expect(page).to have_link('view merge requests')
+ expect(page).not_to have_link('view open issues')
+ end
+ end
+
+ 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
+ expect(page).not_to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+ end
+
+ context 'with a group label' do
+ set(:group) { create(:group) }
+ let(:label) { create(:group_label, group: group, title: 'bug') }
+
+ 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
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+
+ 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
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+
+ 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
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
index 3f2579bb01a..81f08e44cf3 100644
--- a/spec/features/projects/main/download_buttons_spec.rb
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -23,7 +23,7 @@ feature 'Download buttons in project main page' do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
end
describe 'when checking project main page' do
diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb
index 7914180b951..3c98c11b490 100644
--- a/spec/features/projects/main/rss_spec.rb
+++ b/spec/features/projects/main/rss_spec.rb
@@ -7,7 +7,7 @@ feature 'Project RSS' do
context 'when signed in' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit path
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 bf0990d675d..e2a48bfd1d4 100644
--- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb
+++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
@@ -6,7 +6,7 @@ feature 'Projects > Members > Anonymous user sees members' do
let(:project) { create(:project, :public) }
background do
- project.team << [user, :master]
+ project.add_master(user)
create(:project_group_link, project: project, group: group)
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index c140fece41d..e22b6fa6c43 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -11,7 +11,7 @@ feature 'Projects members' do
let(:group_requester) { create(:user) }
background do
- project.team << [developer, :developer]
+ project.add_developer(developer)
group.add_owner(user)
sign_in(user)
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 7f067aadec6..e6d0c6e00f8 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -6,7 +6,7 @@ feature 'Projects > Members > Groups with access list', :js do
let(:project) { create(:project, :public) }
background do
- project.team << [user, :master]
+ project.add_master(user)
@group_link = create(:project_group_link, project: project, group: group)
sign_in(user)
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 0f88f4cb1e8..8fe340d3bae 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
@@ -9,7 +9,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do
let!(:new_member) { create(:user) }
background do
- project.team << [master, :master]
+ project.add_master(master)
sign_in(master)
end
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 eb3c8034873..d575596937d 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -7,7 +7,7 @@ feature 'Projects > Members > Master manages access requests' do
background do
project.request_access(user)
- project.team << [master, :master]
+ project.add_master(master)
sign_in(master)
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 04806f8fd9e..47911c32a72 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
@@ -5,7 +5,7 @@ feature 'Projects > Members > Member cannot request access to his project' do
let(:project) { create(:project) }
background do
- project.team << [member, :developer]
+ project.add_developer(member)
sign_in(member)
visit project_path(project)
end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index 1bcf827d33c..e54c2c76975 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -5,7 +5,7 @@ feature 'Projects > Members > Member leaves project' do
let(:project) { create(:project, :repository) }
background do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit project_path(project)
end
diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb
index a879efef4b5..b34b13db381 100644
--- a/spec/features/projects/merge_requests/list_spec.rb
+++ b/spec/features/projects/merge_requests/list_spec.rb
@@ -5,7 +5,7 @@ feature 'Merge Requests List' do
let(:project) { create(:project, :repository) }
background do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index 013ed6f2e58..2e334caa98f 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -8,7 +8,7 @@ feature 'Pages' do
background do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
- project.team << [user, role]
+ project.add_role(user, role)
sign_in(user)
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 888e290292b..266ef693d0b 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -6,7 +6,7 @@ describe 'Pipeline', :js do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
shared_context 'pipeline builds' do
@@ -152,7 +152,7 @@ describe 'Pipeline', :js do
end
it 'shows counter in Jobs tab' do
- expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
end
it 'shows Pipeline tab as active' do
@@ -248,7 +248,7 @@ describe 'Pipeline', :js do
end
it 'shows counter in Jobs tab' do
- expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
end
it 'shows Jobs tab as active' do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index b87b47d0e1a..592c99fc64a 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -8,7 +8,7 @@ describe 'Pipelines', :js do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe 'GET /:project/pipelines' do
@@ -545,6 +545,40 @@ describe 'Pipelines', :js do
end
end
end
+
+ describe 'Reset runner caches' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master')
+ project.add_master(user)
+ visit project_pipelines_path(project)
+ end
+
+ it 'has a clear caches button' do
+ expect(page).to have_link 'Clear runner caches'
+ end
+
+ describe 'user clicks the button' do
+ context 'when project already has jobs_cache_index' do
+ before do
+ project.update_attributes(jobs_cache_index: 1)
+ end
+
+ it 'increments jobs_cache_index' do
+ click_link 'Clear runner caches'
+ expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
+ end
+ end
+
+ context 'when project does not have jobs_cache_index' do
+ it 'sets jobs_cache_index to 1' do
+ click_link 'Clear runner caches'
+ expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
+ end
+ end
+ end
+ end
end
context 'when user is not logged in' do
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
deleted file mode 100644
index 33ccbc1a29f..00000000000
--- a/spec/features/projects/ref_switcher_spec.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-require 'rails_helper'
-
-feature 'Ref switcher', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
-
- before do
- project.team << [user, :master]
- set_cookie('new_repo', 'true')
- sign_in(user)
- visit project_tree_path(project, 'master')
- end
-
- it 'allow user to change ref by enter key' do
- click_button 'master'
- wait_for_requests
-
- page.within '.project-refs-form' do
- input = find('input[type="search"]')
- input.set 'binary'
- wait_for_requests
-
- expect(find('.dropdown-content ul')).to have_selector('li', count: 7)
-
- page.within '.dropdown-content ul' do
- input.native.send_keys :enter
- end
- end
-
- expect(page).to have_title 'add-pdf-text-binary'
- end
-
- it "user selects ref with special characters" do
- click_button 'master'
- wait_for_requests
-
- page.within '.project-refs-form' do
- page.fill_in 'Search branches and tags', with: "'test'"
- click_link "'test'"
- end
-
- expect(page).to have_title "'test'"
- end
-
- context "create branch" do
- let(:input) { find('.js-new-branch-name') }
-
- before do
- click_button 'master'
- wait_for_requests
-
- page.within '.project-refs-form' do
- find(".dropdown-footer-list a").click
- end
- end
-
- it "shows error message for the invalid branch name" do
- input.set 'foo bar'
- click_button('Create')
- wait_for_requests
- expect(page).to have_content 'Branch name is invalid'
- end
-
- it "should create new branch properly" do
- input.set 'new-branch-name'
- click_button('Create')
- wait_for_requests
- expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name'
- end
-
- it "should create new branch by Enter key" do
- input.set 'new-branch-name-2'
- input.native.send_keys :enter
- wait_for_requests
- expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name-2'
- end
- end
-end
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb
index ac78b1dfb1c..028669eeaf2 100644
--- a/spec/features/projects/services/user_activates_jira_spec.rb
+++ b/spec/features/projects/services/user_activates_jira_spec.rb
@@ -18,7 +18,7 @@ describe 'User activates Jira', :js do
end
before do
- project.team << [user, :master]
+ project.add_master(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 6f057137867..b2906e315f7 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
@@ -8,7 +8,7 @@ feature 'Setup Mattermost slash commands', :js do
before do
stub_mattermost_setting(enabled: mattermost_enabled)
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit edit_project_service_path(project, service)
end
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 a8baf126269..4a88654210c 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
@@ -6,7 +6,7 @@ feature 'Slack slash commands' do
given(:service) { project.create_slack_slash_commands_service }
background do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit edit_project_service_path(project, service)
end
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
index cbdb7973ac8..f6a1a46df11 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -8,7 +8,7 @@ feature 'Integration settings' do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
end
context 'for developer' do
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index ac76c30cc7c..015db603d33 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -5,7 +5,7 @@ feature 'Project settings > Merge Requests', :js do
let(:user) { create(:user) }
background do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 561f08cba00..d0720855564 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -7,7 +7,7 @@ feature "Pipelines settings" do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
end
context 'for developer' do
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index e2a5619c22b..81b282502fc 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -6,7 +6,7 @@ feature 'Repository settings' do
let(:role) { :developer }
background do
- project.team << [user, role]
+ project.add_role(user, role)
sign_in(user)
end
@@ -66,7 +66,7 @@ feature 'Repository settings' do
scenario 'edit a deploy key from projects user has access to' do
project2 = create(:project_empty_repo)
- project2.team << [user, role]
+ project2.add_role(user, role)
project2.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 1c3b84d0114..06f6702670b 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -31,7 +31,7 @@ feature 'Visibility settings', :js do
let(:master_user) { create(:user) }
before do
- project.team << [master_user, :master]
+ project.add_master(master_user)
sign_in(master_user)
visit edit_project_path(project)
end
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index e4215291f99..3466a3dfb77 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -16,7 +16,7 @@ feature 'Create Snippet', :js do
context 'when a user is authenticated' do
before do
- project.team << [user, :master]
+ project.add_master(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 08dc7cf6c5b..216f2af7c88 100644
--- a/spec/features/projects/snippets/show_spec.rb
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -6,7 +6,7 @@ feature 'Project snippet', :js do
let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) }
before do
- project.team << [user, :master]
+ project.add_master(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 d38a5b1324b..b62498194c4 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -24,7 +24,7 @@ feature 'Download buttons in tags page' do
background do
sign_in(user)
- project.team << [user, role]
+ project.add_role(user, role)
end
describe 'when checking tags' do
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 156293289dd..3f6d16c8acf 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -13,6 +13,14 @@ feature 'Multi-file editor new directory', :js do
visit project_tree_path(project, :master)
wait_for_requests
+
+ click_link('Multi Edit')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
end
it 'creates directory in current directory' do
@@ -20,18 +28,30 @@ feature 'Multi-file editor new directory', :js do
click_link('New directory')
- page.within('.popup-dialog') do
- find('.form-control').set('foldername')
+ page.within('.modal') do
+ find('.form-control').set('folder name')
click_button('Create directory')
end
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal-dialog') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
find('.multi-file-commit-panel-collapse-btn').click
- fill_in('commit-message', with: 'commit message')
+ fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
- expect(page).to have_selector('td', text: 'commit message')
+ expect(page).to have_content('folder name')
end
end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 8fb8476e631..ba71eef07f4 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -13,6 +13,14 @@ feature 'Multi-file editor new file', :js do
visit project_tree_path(project, :master)
wait_for_requests
+
+ click_link('Multi Edit')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
end
it 'creates file in current directory' do
@@ -20,18 +28,20 @@ feature 'Multi-file editor new file', :js do
click_link('New file')
- page.within('.popup-dialog') do
- find('.form-control').set('filename')
+ page.within('.modal') do
+ find('.form-control').set('file name')
click_button('Create file')
end
+ wait_for_requests
+
find('.multi-file-commit-panel-collapse-btn').click
- fill_in('commit-message', with: 'commit message')
+ fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
- expect(page).to have_selector('td', text: 'commit message')
+ expect(page).to have_content('file name')
end
end
diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb
index 4f2e0a76a65..6407370ac0d 100644
--- a/spec/features/projects/tree/rss_spec.rb
+++ b/spec/features/projects/tree/rss_spec.rb
@@ -7,7 +7,7 @@ feature 'Project Tree RSS' do
context 'when signed in' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit path
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
new file mode 100644
index 00000000000..c8a17871508
--- /dev/null
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'Projects tree' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, 'master')
+ end
+
+ it 'renders tree table' do
+ expect(page).to have_selector('.tree-item')
+ expect(page).not_to have_selector('.label-lfs', text: 'LFS')
+ end
+
+ context 'LFS' do
+ before do
+ visit project_tree_path(project, File.join('master', 'files/lfs'))
+ end
+
+ it 'renders LFS badge on blob item' do
+ expect(page).to have_selector('.label-lfs', text: 'LFS')
+ end
+ end
+end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index d4e57d1ecfa..9fbb1dbd0e8 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -15,6 +15,14 @@ feature 'Multi-file editor upload file', :js do
visit project_tree_path(project, :master)
wait_for_requests
+
+ click_link('Multi Edit')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
end
it 'uploads text file' do
@@ -41,6 +49,5 @@ feature 'Multi-file editor upload file', :js do
expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
expect(page).not_to have_selector('.monaco-editor')
- expect(page).to have_content('The source could not be displayed for this temporary file.')
end
end
diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb
index f5e4d7f5130..62e6419cc42 100644
--- a/spec/features/projects/user_browses_files_spec.rb
+++ b/spec/features/projects/user_browses_files_spec.rb
@@ -15,7 +15,7 @@ describe 'User browses files' do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb
index 052cb3188c5..00e48f6fabd 100644
--- a/spec/features/projects/user_creates_directory_spec.rb
+++ b/spec/features/projects/user_creates_directory_spec.rb
@@ -11,7 +11,7 @@ feature 'User creates a directory', :js do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
visit project_tree_path(project, 'master')
end
@@ -63,7 +63,7 @@ feature 'User creates a directory', :js do
context 'when an user does not have write access' do
before do
- project2.team << [user, :reporter]
+ project2.add_reporter(user)
visit(project2_tree_path_root_ref)
end
diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb
index d84b91ddc32..7a935dd2477 100644
--- a/spec/features/projects/user_creates_files_spec.rb
+++ b/spec/features/projects/user_creates_files_spec.rb
@@ -12,7 +12,7 @@ describe 'User creates files' do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -33,7 +33,7 @@ describe 'User creates files' do
context 'when an user does not have write access' do
before do
- project2.team << [user, :reporter]
+ project2.add_reporter(user)
visit(project2_tree_path_root_ref)
end
@@ -131,7 +131,7 @@ describe 'User creates files' do
context 'when an user does not have write access' do
before do
- project2.team << [user, :reporter]
+ project2.add_reporter(user)
visit(project2_tree_path_root_ref)
end
diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb
index 9e4e92ec076..9d55197e719 100644
--- a/spec/features/projects/user_deletes_files_spec.rb
+++ b/spec/features/projects/user_deletes_files_spec.rb
@@ -17,7 +17,7 @@ describe 'User deletes files' do
context 'when an user has write access' do
before do
- project.team << [user, :master]
+ project.add_master(user)
visit(project_tree_path_root_ref)
end
@@ -37,7 +37,7 @@ describe 'User deletes files' do
context 'when an user does not have write access' do
before do
- project2.team << [user, :reporter]
+ project2.add_reporter(user)
visit(project2_tree_path_root_ref)
end
diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb
index d26ee653415..05c2be473da 100644
--- a/spec/features/projects/user_edits_files_spec.rb
+++ b/spec/features/projects/user_edits_files_spec.rb
@@ -14,7 +14,7 @@ describe 'User edits files' do
context 'when an user has write access' do
before do
- project.team << [user, :master]
+ project.add_master(user)
visit(project_tree_path_root_ref)
end
@@ -33,7 +33,9 @@ describe 'User edits files' do
binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png')
visit(project_blob_path(project, binary_file))
- expect(page).not_to have_link('edit')
+ page.within '.content' do
+ expect(page).not_to have_link('edit')
+ end
end
it 'commits an edited file', :js do
@@ -87,7 +89,7 @@ describe 'User edits files' do
context 'when an user does not have write access' do
before do
- project2.team << [user, :reporter]
+ project2.add_reporter(user)
visit(project2_tree_path_root_ref)
end
diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb
index 245b6aa285b..74872403b35 100644
--- a/spec/features/projects/user_replaces_files_spec.rb
+++ b/spec/features/projects/user_replaces_files_spec.rb
@@ -19,7 +19,7 @@ describe 'User replaces files' do
context 'when an user has write access' do
before do
- project.team << [user, :master]
+ project.add_master(user)
visit(project_tree_path_root_ref)
end
@@ -45,7 +45,7 @@ describe 'User replaces files' do
context 'when an user does not have write access' do
before do
- project2.team << [user, :reporter]
+ project2.add_reporter(user)
visit(project2_tree_path_root_ref)
end
diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb
index ae51901adc6..75898afcda9 100644
--- a/spec/features/projects/user_uploads_files_spec.rb
+++ b/spec/features/projects/user_uploads_files_spec.rb
@@ -14,7 +14,7 @@ describe 'User uploads files' do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
@@ -50,7 +50,7 @@ describe 'User uploads files' do
context 'when an user does not have write access' do
before do
- project2.team << [user, :reporter]
+ project2.add_reporter(user)
visit(project2_tree_path_root_ref)
end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 337baaf4dcd..006c15d60c5 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -13,7 +13,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
end
background do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index ebb3bd044c1..2682b62fa04 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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 63e6051b571..b66a7dea598 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -133,7 +133,7 @@ feature 'Project' do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
visit edit_project_path(project)
end
@@ -151,7 +151,7 @@ feature 'Project' do
let(:project) { create(:forked_project_with_submodules) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_path(project)
end
@@ -180,7 +180,7 @@ feature 'Project' do
let(:project) { create(:project, :repository) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in user
visit project_path(project)
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index c7f0e342809..aec9de6c7ca 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -33,6 +33,26 @@ feature 'Runners' do
expect(page).to have_content(specific_runner.platform)
end
+ scenario 'user can pause and resume the specific runner' do
+ visit runners_path(project)
+
+ within '.activated-specific-runners' do
+ expect(page).to have_content('Pause')
+ end
+
+ click_on 'Pause'
+
+ within '.activated-specific-runners' do
+ expect(page).to have_content('Resume')
+ end
+
+ click_on 'Resume'
+
+ within '.activated-specific-runners' do
+ expect(page).to have_content('Pause')
+ end
+ end
+
scenario 'user removes an activated specific runner if this is last project for that runners' do
visit runners_path(project)
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 8efa5b58141..6088b831c14 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.team << [user, :master]
+ project.add_master(user)
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
@@ -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.team << [user, :master]
+ project.add_master(user)
sign_in(user)
@@ -86,7 +86,7 @@ describe 'GPG signed commits', :js do
before do
user = create :user
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 1f8bd8d681e..8a8f6933fa5 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -5,7 +5,7 @@ feature 'Master creates tag' do
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index dfda664d673..c0b4fa52526 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -5,7 +5,7 @@ feature 'Master deletes tag' do
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_tags_path(project)
end
diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb
index b93ad44dfd3..1c370a99b13 100644
--- a/spec/features/tags/master_updates_tag_spec.rb
+++ b/spec/features/tags/master_updates_tag_spec.rb
@@ -5,7 +5,7 @@ feature 'Master updates tag' do
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_tags_path(project)
end
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index 9edc7ced163..4662367d843 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -4,18 +4,17 @@ feature 'Master views tags' do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
context 'when project has no tags' do
let(:project) { create(:project_empty_repo) }
+
before do
visit project_path(project)
click_on 'README'
fill_in :commit_message, with: 'Add a README file', visible: true
- # Remove pre-receive hook so we can push without auth
- FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
click_button 'Commit changes'
visit project_tags_path(project)
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index bc472e74997..19784120108 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -10,9 +10,9 @@ feature 'Triggers', :js do
sign_in(user)
@project = create(:project)
- @project.team << [user, :master]
- @project.team << [user2, :master]
- @project.team << [guest_user, :guest]
+ @project.add_master(user)
+ @project.add_master(user2)
+ @project.add_guest(guest_user)
visit project_settings_ci_cd_path(@project)
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index c9afef2a8de..50ee1656e10 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -264,7 +264,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
end
it "deletes u2f registrations" do
- visit profile_account_path
+ visit profile_two_factor_auth_path
expect do
accept_confirm { click_on "Disable" }
end.to change { U2fRegistration.count }.by(-1)
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index c78f7d0d9be..79ca2b4bb4a 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -7,7 +7,7 @@ describe 'Project variables', :js do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
project.variables << variable
visit project_settings_ci_cd_path(project)
@@ -65,14 +65,14 @@ describe 'Project variables', :js do
expect(page).to have_content('******')
end
- click_button('Reveal Values')
+ click_button('Reveal values')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('key value')
end
- click_button('Hide Values')
+ click_button('Hide values')
page.within('.variables-table') do
expect(page).to have_content('key')
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
index 0789d3a9b44..650f7229647 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.team << [user, :master]
+ project.add_master(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.team << [user, :master]
+ project.add_master(user)
group.add_owner(user)
end
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 074914420a1..ae050f36b4a 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -73,6 +73,41 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(matching_project)
end
end
+
+ context 'sorting by name' do
+ let!(:project1) { create(:project, namespace: group, name: 'a', path: 'project-a') }
+ let!(:project2) { create(:project, namespace: group, name: 'z', path: 'project-z') }
+ let(:params) do
+ {
+ sort: 'name_asc'
+ }
+ end
+
+ it 'sorts elements by name' do
+ expect(subject.execute).to eq(
+ [
+ project1,
+ project2
+ ]
+ )
+ end
+
+ context 'with nested groups', :nested_groups do
+ let!(:subgroup1) { create(:group, parent: group, name: 'a', path: 'sub-a') }
+ let!(:subgroup2) { create(:group, parent: group, name: 'z', path: 'sub-z') }
+
+ it 'sorts elements by name' do
+ expect(subject.execute).to eq(
+ [
+ subgroup1,
+ subgroup2,
+ project1,
+ project2
+ ]
+ )
+ end
+ end
+ end
end
context 'with nested groups', :nested_groups do
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index c6d257bc479..27a09d7c6f5 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -45,7 +45,7 @@ describe GroupProjectsFinder do
describe 'without group member current_user' do
before do
- shared_project_2.team << [current_user, Gitlab::Access::MASTER]
+ shared_project_2.add_master(current_user)
current_user.reload
end
@@ -70,7 +70,7 @@ describe GroupProjectsFinder do
context "without external user" do
before do
- private_project.team << [current_user, Gitlab::Access::MASTER]
+ private_project.add_master(current_user)
end
it { is_expected.to match_array([private_project, public_project]) }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 47b173dea0a..47fd98234f9 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -22,9 +22,9 @@ describe IssuesFinder do
let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
before(:context) do
- project1.team << [user, :master]
- project2.team << [user, :developer]
- project2.team << [user2, :developer]
+ project1.add_master(user)
+ project2.add_developer(user)
+ project2.add_developer(user2)
issue1
issue2
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index afa2a40ed2a..06031aee217 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -27,7 +27,7 @@ describe LabelsFinder do
create(:label, project: project_3, title: 'Label 3')
create(:group_label, group: group_3, title: 'Group Label 4')
- project_1.team << [user, :developer]
+ project_1.add_developer(user)
end
context 'with no filter' do
@@ -56,6 +56,16 @@ describe LabelsFinder do
expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5]
end
+
+ context 'when only_group_labels is true' do
+ it 'returns only group labels' do
+ group_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: group_1.id, only_group_labels: true)
+
+ expect(finder.execute).to eq [group_label_2, group_label_1]
+ end
+ end
end
context 'filtering by project_id' do
@@ -73,7 +83,7 @@ describe LabelsFinder do
# project_3 has a label associated to it, which we don't want coming
# back when we ask for the isolated project's labels
- project_3.team << [admin, :reporter]
+ project_3.add_reporter(admin)
finder = described_class.new(admin, project_id: isolated_project.id)
expect(finder.execute).to be_empty
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 883bdf3746a..687ffaec7cc 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -20,10 +20,10 @@ describe MergeRequestsFinder do
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
before do
- project1.team << [user, :master]
- project2.team << [user, :developer]
- project3.team << [user, :developer]
- project2.team << [user2, :developer]
+ project1.add_master(user)
+ project2.add_developer(user)
+ project3.add_developer(user)
+ project2.add_developer(user2)
end
describe "#execute" do
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index e577083a2d0..74639d4147f 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -15,39 +15,39 @@ describe MoveToProjectFinder do
describe '#execute' do
context 'filter' do
it 'does not return projects under Gitlab::Access::REPORTER' do
- guest_project.team << [user, :guest]
+ guest_project.add_guest(user)
expect(subject.execute(project)).to be_empty
end
it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do
- reporter_project.team << [user, :reporter]
- developer_project.team << [user, :developer]
- master_project.team << [user, :master]
+ reporter_project.add_reporter(user)
+ developer_project.add_developer(user)
+ master_project.add_master(user)
expect(subject.execute(project).to_a).to eq([master_project, developer_project, reporter_project])
end
it 'does not include the source project' do
- project.team << [user, :reporter]
+ project.add_reporter(user)
expect(subject.execute(project).to_a).to be_empty
end
it 'does not return archived projects' do
- reporter_project.team << [user, :reporter]
+ reporter_project.add_reporter(user)
reporter_project.archive!
other_reporter_project = create(:project)
- other_reporter_project.team << [user, :reporter]
+ other_reporter_project.add_reporter(user)
expect(subject.execute(project).to_a).to eq([other_reporter_project])
end
it 'does not return projects for which issues are disabled' do
- reporter_project.team << [user, :reporter]
+ reporter_project.add_reporter(user)
reporter_project.update_attributes(issues_enabled: false)
other_reporter_project = create(:project)
- other_reporter_project.team << [user, :reporter]
+ other_reporter_project.add_reporter(user)
expect(subject.execute(project).to_a).to eq([other_reporter_project])
end
@@ -55,9 +55,9 @@ describe MoveToProjectFinder do
it 'returns a page of projects ordered by id in descending order' do
stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
- reporter_project.team << [user, :reporter]
- developer_project.team << [user, :developer]
- master_project.team << [user, :master]
+ reporter_project.add_reporter(user)
+ developer_project.add_developer(user)
+ master_project.add_master(user)
expect(subject.execute(project).to_a).to eq([master_project, developer_project])
end
@@ -65,9 +65,9 @@ describe MoveToProjectFinder do
it 'returns projects after the given offset id' do
stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
- reporter_project.team << [user, :reporter]
- developer_project.team << [user, :developer]
- master_project.team << [user, :master]
+ reporter_project.add_reporter(user)
+ developer_project.add_developer(user)
+ master_project.add_master(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: developer_project.id).to_a).to eq([reporter_project])
@@ -84,10 +84,10 @@ describe MoveToProjectFinder do
it 'returns projects matching a search query' do
foo_project = create(:project)
- foo_project.team << [user, :master]
+ foo_project.add_master(user)
wadus_project = create(:project, name: 'wadus')
- wadus_project.team << [user, :master]
+ wadus_project.add_master(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 900fa2b12d1..7b43494eea2 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.team << [user, :master]
+ project.add_master(user)
end
describe '#execute' do
@@ -147,7 +147,7 @@ describe NotesFinder do
it 'raises an error for project members with guest role' do
user = create(:user)
- project.team << [user, :guest]
+ project.add_guest(user)
expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
end
@@ -189,7 +189,7 @@ describe NotesFinder do
it "does not return notes with matching content for project members with guest role" do
user = create(:user)
- project.team << [user, :guest]
+ project.add_guest(user)
expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty
end
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index d0113ba87df..5e52898e9c0 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -15,7 +15,7 @@ describe PersonalProjectsFinder do
end
before do
- private_project.team << [current_user, Gitlab::Access::DEVELOPER]
+ private_project.add_developer(current_user)
end
describe 'without a current user' do
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 7ae7b7d2140..0a018d2b417 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -188,7 +188,7 @@ describe SnippetsFinder do
end
it "returns all snippets for project members" do
- project1.team << [user, :developer]
+ project1.add_developer(user)
snippets = described_class.new(user, project: project1).execute
@@ -196,7 +196,7 @@ describe SnippetsFinder do
end
it "returns private snippets for project members" do
- project1.team << [user, :developer]
+ project1.add_developer(user)
snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 884ce22091e..90eb0fe21e4 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -7,7 +7,7 @@ describe TodosFinder do
let(:finder) { described_class }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe '#sort' do
diff --git a/spec/fixtures/api/schemas/contributor.json b/spec/fixtures/api/schemas/contributor.json
new file mode 100644
index 00000000000..e88470a2363
--- /dev/null
+++ b/spec/fixtures/api/schemas/contributor.json
@@ -0,0 +1,18 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "email",
+ "commits",
+ "additions",
+ "deletions"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "email": { "type": "string" },
+ "commits": { "type": "integer" },
+ "additions": { "type": "integer" },
+ "deletions": { "type": "integer" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/contributors.json b/spec/fixtures/api/schemas/contributors.json
new file mode 100644
index 00000000000..a9f1d1ea64f
--- /dev/null
+++ b/spec/fixtures/api/schemas/contributors.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "contributor.json" }
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 995f13381ad..f1199468d53 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -9,6 +9,7 @@
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
+ "rebase_in_progress": { "type": "boolean" },
"assignee_id": { "type": ["integer", "null"] },
"subscribed": { "type": ["boolean", "null"] },
"participants": { "type": "array" }
diff --git a/spec/fixtures/api/schemas/entities/merge_request_metrics.json b/spec/fixtures/api/schemas/entities/merge_request_metrics.json
new file mode 100644
index 00000000000..3fa767f85df
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/merge_request_metrics.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "required": ["closed_at", "merged_at", "closed_by", "merged_by"],
+ "properties" : {
+ "closed_at": { "type": ["datetime", "null"] },
+ "merged_at": { "type": ["datetime", "null"] },
+ "closed_by": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "user.json" }
+ ]
+ },
+ "merged_by": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "user.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index ba094ba1657..7f662098216 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -31,8 +31,12 @@
"source_project_id": { "type": "integer" },
"target_branch": { "type": "string" },
"target_project_id": { "type": "integer" },
- "merge_event": { "type": ["object", "null"] },
- "closed_event": { "type": ["object", "null"] },
+ "metrics": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "merge_request_metrics.json" }
+ ]
+ },
"author": { "type": ["object", "null"] },
"merge_user": { "type": ["object", "null"] },
"diff_head_sha": { "type": ["string", "null"] },
@@ -80,15 +84,15 @@
"target_branch_tree_path": { "type": "string" },
"source_branch_path": { "type": "string" },
"conflict_resolution_path": { "type": ["string", "null"] },
- "cancel_merge_when_pipeline_succeeds_path": { "type": "string" },
- "create_issue_to_resolve_discussions_path": { "type": "string" },
- "merge_path": { "type": "string" },
+ "cancel_merge_when_pipeline_succeeds_path": { "type": ["string", "null"] },
+ "create_issue_to_resolve_discussions_path": { "type": ["string", "null"] },
+ "merge_path": { "type": ["string", "null"] },
"cherry_pick_in_fork_path": { "type": ["string", "null"] },
"revert_in_fork_path": { "type": ["string", "null"] },
"email_patches_path": { "type": "string" },
"plain_diff_path": { "type": "string" },
"status_path": { "type": "string" },
- "new_blob_path": { "type": "string" },
+ "new_blob_path": { "type": ["string", "null"] },
"merge_check_path": { "type": "string" },
"ci_environments_status_path": { "type": "string" },
"merge_commit_message_with_description": { "type": "string" },
@@ -99,7 +103,11 @@
"remove_source_branch": { "type": ["boolean", "null"] },
"merge_ongoing": { "type": "boolean" },
"ff_only_enabled": { "type": ["boolean", false] },
- "should_be_rebased": { "type": "boolean" }
+ "should_be_rebased": { "type": "boolean" },
+ "rebase_commit_sha": { "type": ["string", "null"] },
+ "rebase_in_progress": { "type": "boolean" },
+ "can_push_to_source_branch": { "type": "boolean" },
+ "rebase_path": { "type": ["string", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/entities/user.json b/spec/fixtures/api/schemas/entities/user.json
new file mode 100644
index 00000000000..6482e0eedd2
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/user.json
@@ -0,0 +1,17 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "state",
+ "avatar_url",
+ "web_url",
+ "path"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "string" },
+ "web_url": { "type": "string" },
+ "path": { "type": "string" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json
new file mode 100644
index 00000000000..d667f1d631c
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/board.json
@@ -0,0 +1,86 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "project",
+ "lists"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "project": {
+ "type": ["object", "null"],
+ "required": [
+ "id",
+ "avatar_url",
+ "description",
+ "default_branch",
+ "tag_list",
+ "ssh_url_to_repo",
+ "http_url_to_repo",
+ "web_url",
+ "name",
+ "name_with_namespace",
+ "path",
+ "path_with_namespace",
+ "star_count",
+ "forks_count",
+ "created_at",
+ "last_activity_at"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "avatar_url": { "type": ["string", "null"] },
+ "description": { "type": ["string", "null"] },
+ "default_branch": { "type": ["string", "null"] },
+ "tag_list": { "type": "array" },
+ "ssh_url_to_repo": { "type": "string" },
+ "http_url_to_repo": { "type": "string" },
+ "web_url": { "type": "string" },
+ "name": { "type": "string" },
+ "name_with_namespace": { "type": "string" },
+ "path": { "type": "string" },
+ "path_with_namespace": { "type": "string" },
+ "star_count": { "type": "integer" },
+ "forks_count": { "type": "integer" },
+ "created_at": { "type": "date" },
+ "last_activity_at": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "lists": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required" : [
+ "id",
+ "label",
+ "position"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "label": {
+ "type": ["object", "null"],
+ "required": [
+ "id",
+ "color",
+ "description",
+ "name"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "name": { "type": "string" }
+ }
+ },
+ "position": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/boards.json b/spec/fixtures/api/schemas/public_api/v4/boards.json
new file mode 100644
index 00000000000..117564ef77a
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/boards.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "board.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
index 4ba6422406c..e8c17298b43 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
@@ -3,6 +3,7 @@
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
+ "project_id": { "type": "integer" },
"certificate_expiration": {
"type": "object",
"properties": {
@@ -13,6 +14,6 @@
"additionalProperties": false
}
},
- "required": ["domain", "url"],
+ "required": ["domain", "url", "project_id"],
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
index 9f69d31971c..bf330d8278c 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
@@ -1,5 +1,5 @@
{
- "type": "object",
+ "type": ["object", "null"],
"required": [
"id",
"state",
diff --git a/spec/fixtures/emails/valid_new_merge_request.eml b/spec/fixtures/emails/valid_new_merge_request.eml
index 480675a6d7e..729df674604 100644
--- a/spec/fixtures/emails/valid_new_merge_request.eml
+++ b/spec/fixtures/emails/valid_new_merge_request.eml
@@ -16,3 +16,5 @@ X-Sieve: CMU Sieve 2.2
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
13 Jun 2013 14:03:48 -0700 (PDT)
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Merge request description
diff --git a/spec/fixtures/emails/valid_new_merge_request_no_description.eml b/spec/fixtures/emails/valid_new_merge_request_no_description.eml
new file mode 100644
index 00000000000..480675a6d7e
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_merge_request_no_description.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: feature
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 638cd8b07c8..71abb6da607 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -258,12 +258,23 @@ With inline diffs tags you can display {+ additions +} or [- deletions -].
The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
-However the wrapping tags can not be mixed as such -
+Examples:
+```
+- {+ additions +}
+- [+ additions +]
+- {- deletions -}
+- [- deletions -]
+```
+
+However the wrapping tags cannot be mixed as such:
+
+```
- {+ additions +]
- [+ additions +}
-- {- delletions -]
-- [- delletions -}
+- {- deletions -]
+- [- deletions -}
+```
### Videos
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 4ac4302adfd..0286d36952c 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -1,6 +1,69 @@
require 'spec_helper'
describe LabelsHelper do
+ describe '#show_label_issuables_link?' do
+ shared_examples 'a valid response to show_label_issuables_link?' do |issuables_type, when_enabled = true, when_disabled = false|
+ let(:context_project) { project }
+
+ context "when asking for a #{issuables_type} link" do
+ subject { show_label_issuables_link?(label, issuables_type, project: context_project) }
+
+ context "when #{issuables_type} are enabled for the project" do
+ let(:project) { create(:project, "#{issuables_type}_access_level": ProjectFeature::ENABLED) }
+
+ it { is_expected.to be(when_enabled) }
+ end
+
+ context "when #{issuables_type} are disabled for the project" do
+ let(:project) { create(:project, :public, "#{issuables_type}_access_level": ProjectFeature::DISABLED) }
+
+ it { is_expected.to be(when_disabled) }
+ end
+ end
+ end
+
+ context 'with a project label' do
+ let(:label) { create(:label, project: project, title: 'bug') }
+
+ context 'when asking for an issue link' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :issues
+ end
+
+ context 'when asking for a merge requests link' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests
+ end
+ end
+
+ context 'with a group label' do
+ set(:group) { create(:group) }
+ let(:label) { create(:group_label, group: group, title: 'bug') }
+
+ context 'when asking for an issue link' do
+ context 'in the context of a project' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true
+ end
+
+ context 'in the context of a group' do
+ let(:context_project) { nil }
+
+ it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true
+ end
+ end
+
+ context 'when asking for a merge requests link' do
+ context 'in the context of a project' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true
+ end
+
+ context 'in the context of a group' do
+ let(:context_project) { nil }
+
+ it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true
+ end
+ end
+ end
+ end
+
describe 'link_to_label' do
let(:project) { create(:project) }
let(:label) { create(:label, project: project) }
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index ba0039f3a11..c0dc9293397 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.team << [user, :master]
+ project.add_master(user)
# Helper expects a @project instance variable
helper.instance_variable_set(:@project, project)
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 33186cf50d5..45ffbeb27a4 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -1,14 +1,6 @@
require 'spec_helper'
describe MembersHelper do
- describe '#action_member_permission' do
- let(:project_member) { build(:project_member) }
- let(:group_member) { build(:group_member) }
-
- it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
- it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
- end
-
describe '#remove_member_message' do
let(:requester) { create(:user) }
let(:project) { create(:project, :public, :access_requestable) }
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index cd15e27b497..b992bdb4a5e 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -17,9 +17,9 @@ describe NotesHelper do
before do
group.add_owner(owner)
- project.team << [master, :master]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
end
describe "#notes_max_access_for_users" do
@@ -31,7 +31,7 @@ describe NotesHelper do
it 'handles access in different projects' do
second_project = create(:project)
- second_project.team << [master, :reporter]
+ second_project.add_reporter(master)
other_note = create(:note, author: master, project: second_project)
expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER)
@@ -41,6 +41,7 @@ describe NotesHelper do
describe '#discussion_path' do
let(:project) { create(:project, :repository) }
+ let(:anchor) { discussion.line_code }
context 'for a merge request discusion' do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
@@ -151,6 +152,15 @@ describe NotesHelper do
expect(helper.discussion_path(discussion)).to be_nil
end
end
+
+ context 'for a contextual commit discussion' do
+ let(:commit) { merge_request.commits.last }
+ let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, commit_id: commit.id).to_discussion }
+
+ it 'returns the merge request diff discussion scoped in the commit' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_project_merge_request_path(project, merge_request, commit_id: commit.id, anchor: anchor))
+ end
+ end
end
context 'for a commit discussion' do
@@ -160,7 +170,7 @@ describe NotesHelper do
let(:discussion) { create(:diff_note_on_commit, project: project).to_discussion }
it 'returns the commit path with the line code' do
- expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit, anchor: discussion.line_code))
+ expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit, anchor: anchor))
end
end
@@ -168,7 +178,7 @@ describe NotesHelper do
let(:discussion) { create(:legacy_diff_note_on_commit, project: project).to_discussion }
it 'returns the commit path with the line code' do
- expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit, anchor: discussion.line_code))
+ expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit, anchor: anchor))
end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 8b8080563d3..749aa25e632 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -77,15 +77,6 @@ describe PreferencesHelper do
end
end
- def stub_user(messages = {})
- if messages.empty?
- allow(helper).to receive(:current_user).and_return(nil)
- else
- allow(helper).to receive(:current_user)
- .and_return(double('user', messages))
- end
- end
-
describe '#default_project_view' do
context 'user not signed in' do
before do
@@ -125,5 +116,70 @@ describe PreferencesHelper do
end
end
end
+
+ context 'user signed in' do
+ let(:user) { create(:user, :readme) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'when the user is allowed to see the code' do
+ it 'returns the project view' do
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('readme')
+ end
+ end
+
+ context 'with wikis enabled and the right policy for the user' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, 0)
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ end
+
+ it 'returns wiki if the user has the right policy' do
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('wiki')
+ end
+
+ it 'returns customize_workflow if the user does not have the right policy' do
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('customize_workflow')
+ end
+ end
+
+ context 'with issues as a feature available' do
+ it 'return issues' do
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('projects/issues/issues')
+ end
+ end
+
+ context 'with no activity, no wikies and no issues' do
+ it 'returns customize_workflow as default' do
+ project.project_feature.update_attribute(:issues_access_level, 0)
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('customize_workflow')
+ end
+ end
+ end
+ end
+
+ def stub_user(messages = {})
+ if messages.empty?
+ allow(helper).to receive(:current_user).and_return(nil)
+ else
+ allow(helper).to receive(:current_user)
+ .and_return(double('user', messages))
+ end
end
end
diff --git a/spec/helpers/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb
index 35f91b7decf..a4a483e68a8 100644
--- a/spec/helpers/runners_helper_spec.rb
+++ b/spec/helpers/runners_helper_spec.rb
@@ -2,17 +2,17 @@ require 'spec_helper'
describe RunnersHelper do
it "returns - not contacted yet" do
- runner = FactoryGirl.build :ci_runner
+ runner = FactoryBot.build :ci_runner
expect(runner_status_icon(runner)).to include("not connected yet")
end
it "returns offline text" do
- runner = FactoryGirl.build(:ci_runner, contacted_at: 1.day.ago, active: true)
+ runner = FactoryBot.build(:ci_runner, contacted_at: 1.day.ago, active: true)
expect(runner_status_icon(runner)).to include("Runner is offline")
end
it "returns online text" do
- runner = FactoryGirl.build(:ci_runner, contacted_at: 1.second.ago, active: true)
+ runner = FactoryBot.build(:ci_runner, contacted_at: 1.second.ago, active: true)
expect(runner_status_icon(runner)).to include("Runner is online")
end
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c358ccae9c3..d3b1be599dd 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -9,6 +9,7 @@ describe TreeHelper do
before do
@id = sha
@project = project
+ @lfs_blob_ids = []
end
it 'displays all entries without a warning' do
diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js
index e8c5f721423..7a9c539e9d0 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/javascripts/activities_spec.js
@@ -1,8 +1,7 @@
/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */
import 'vendor/jquery.endless-scroll';
-import '~/pager';
-import '~/activities';
+import Activities from '~/activities';
(() => {
window.gon || (window.gon = {});
@@ -35,7 +34,7 @@ import '~/activities';
describe('Activities', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- new gl.Activities();
+ new Activities();
});
for (let i = 0; i < filters.length; i += 1) {
diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js
new file mode 100644
index 00000000000..9eeae474e7d
--- /dev/null
+++ b/spec/javascripts/behaviors/secret_values_spec.js
@@ -0,0 +1,146 @@
+import SecretValues from '~/behaviors/secret_values';
+
+function generateFixtureMarkup(secrets, isRevealed) {
+ return `
+ <div class="js-secret-container">
+ ${secrets.map(secret => `
+ <div class="js-secret-value-placeholder">
+ ***
+ </div>
+ <div class="hide js-secret-value">
+ ${secret}
+ </div>
+ `).join('')}
+ <button
+ class="js-secret-value-reveal-button"
+ data-secret-reveal-status="${isRevealed}"
+ >
+ ...
+ </button>
+ </div>
+ `;
+}
+
+function setupSecretFixture(secrets, isRevealed) {
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = generateFixtureMarkup(secrets, isRevealed);
+
+ const secretValues = new SecretValues(wrapper.querySelector('.js-secret-container'));
+ secretValues.init();
+
+ return wrapper;
+}
+
+describe('setupSecretValues', () => {
+ describe('with a single secret', () => {
+ const secrets = ['mysecret123'];
+
+ it('should have correct "Reveal" label when values are hidden', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+
+ expect(revealButton.textContent).toEqual('Reveal value');
+ });
+
+ it('should have correct "Hide" label when values are shown', () => {
+ const wrapper = setupSecretFixture(secrets, true);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+
+ expect(revealButton.textContent).toEqual('Hide value');
+ });
+
+ it('should value hidden initially', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ const values = wrapper.querySelectorAll('.js-secret-value');
+ const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
+
+ expect(values.length).toEqual(1);
+ expect(values[0].classList.contains('hide')).toEqual(true);
+ expect(placeholders.length).toEqual(1);
+ expect(placeholders[0].classList.contains('hide')).toEqual(false);
+ });
+
+ it('should toggle value and placeholder', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+ const values = wrapper.querySelectorAll('.js-secret-value');
+ const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
+
+ revealButton.click();
+
+ expect(values.length).toEqual(1);
+ expect(values[0].classList.contains('hide')).toEqual(false);
+ expect(placeholders.length).toEqual(1);
+ expect(placeholders[0].classList.contains('hide')).toEqual(true);
+
+ revealButton.click();
+
+ expect(values.length).toEqual(1);
+ expect(values[0].classList.contains('hide')).toEqual(true);
+ expect(placeholders.length).toEqual(1);
+ expect(placeholders[0].classList.contains('hide')).toEqual(false);
+ });
+ });
+
+ describe('with a multiple secrets', () => {
+ const secrets = ['mysecret123', 'happygoat456', 'tanuki789'];
+
+ it('should have correct "Reveal" label when values are hidden', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+
+ expect(revealButton.textContent).toEqual('Reveal values');
+ });
+
+ it('should have correct "Hide" label when values are shown', () => {
+ const wrapper = setupSecretFixture(secrets, true);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+
+ expect(revealButton.textContent).toEqual('Hide values');
+ });
+
+ it('should have all values hidden initially', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ const values = wrapper.querySelectorAll('.js-secret-value');
+ const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
+
+ expect(values.length).toEqual(3);
+ values.forEach((value) => {
+ expect(value.classList.contains('hide')).toEqual(true);
+ });
+ expect(placeholders.length).toEqual(3);
+ placeholders.forEach((placeholder) => {
+ expect(placeholder.classList.contains('hide')).toEqual(false);
+ });
+ });
+
+ it('should toggle values and placeholders', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+ const values = wrapper.querySelectorAll('.js-secret-value');
+ const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
+
+ revealButton.click();
+
+ expect(values.length).toEqual(3);
+ values.forEach((value) => {
+ expect(value.classList.contains('hide')).toEqual(false);
+ });
+ expect(placeholders.length).toEqual(3);
+ placeholders.forEach((placeholder) => {
+ expect(placeholder.classList.contains('hide')).toEqual(true);
+ });
+
+ revealButton.click();
+
+ expect(values.length).toEqual(3);
+ values.forEach((value) => {
+ expect(value.classList.contains('hide')).toEqual(true);
+ });
+ expect(placeholders.length).toEqual(3);
+ placeholders.forEach((placeholder) => {
+ expect(placeholder.classList.contains('hide')).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js
index c3e67550f05..df1b2c9960b 100644
--- a/spec/javascripts/blob/notebook/index_spec.js
+++ b/spec/javascripts/blob/notebook/index_spec.js
@@ -1,4 +1,5 @@
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import renderNotebook from '~/blob/notebook';
describe('iPython notebook renderer', () => {
@@ -17,8 +18,11 @@ describe('iPython notebook renderer', () => {
});
describe('successful response', () => {
- const response = (request, next) => {
- next(request.respondWith(JSON.stringify({
+ let mock;
+
+ beforeEach((done) => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/test').reply(200, {
cells: [{
cell_type: 'markdown',
source: ['# test'],
@@ -31,13 +35,7 @@ describe('iPython notebook renderer', () => {
],
outputs: [],
}],
- }), {
- status: 200,
- }));
- };
-
- beforeEach((done) => {
- Vue.http.interceptors.push(response);
+ });
renderNotebook();
@@ -47,9 +45,7 @@ describe('iPython notebook renderer', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, response,
- );
+ mock.reset();
});
it('does not show loading icon', () => {
@@ -86,14 +82,11 @@ describe('iPython notebook renderer', () => {
});
describe('error in JSON response', () => {
- const response = (request, next) => {
- next(request.respondWith('{ "cells": [{"cell_type": "markdown"} }', {
- status: 200,
- }));
- };
+ let mock;
beforeEach((done) => {
- Vue.http.interceptors.push(response);
+ mock = new MockAdapter(axios);
+ mock.onGet('/test').reply(() => Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' }));
renderNotebook();
@@ -103,9 +96,7 @@ describe('iPython notebook renderer', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, response,
- );
+ mock.reset();
});
it('does not show loading icon', () => {
@@ -122,14 +113,11 @@ describe('iPython notebook renderer', () => {
});
describe('error getting file', () => {
- const response = (request, next) => {
- next(request.respondWith('', {
- status: 500,
- }));
- };
+ let mock;
beforeEach((done) => {
- Vue.http.interceptors.push(response);
+ mock = new MockAdapter(axios);
+ mock.onGet('/test').reply(500, '');
renderNotebook();
@@ -139,9 +127,7 @@ describe('iPython notebook renderer', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, response,
- );
+ mock.reset();
});
it('does not show loading icon', () => {
diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js
index 2ee3792dd65..f757dadfada 100644
--- a/spec/javascripts/boards/board_blank_state_spec.js
+++ b/spec/javascripts/boards/board_blank_state_spec.js
@@ -1,9 +1,8 @@
/* global BoardService */
-/* global mockBoardService */
import Vue from 'vue';
import '~/boards/stores/boards_store';
import boardBlankState from '~/boards/components/board_blank_state';
-import './mock_data';
+import { mockBoardService } from './mock_data';
describe('Boards blank state', () => {
let vm;
@@ -20,17 +19,15 @@ describe('Boards blank state', () => {
reject();
} else {
resolve({
- json() {
- return [{
- id: 1,
- title: 'To Do',
- label: { id: 1 },
- }, {
- id: 2,
- title: 'Doing',
- label: { id: 2 },
- }];
- },
+ data: [{
+ id: 1,
+ title: 'To Do',
+ label: { id: 1 },
+ }, {
+ id: 2,
+ title: 'Doing',
+ label: { id: 2 },
+ }],
});
}
}));
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 8f607899b20..4e73fa1fe87 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -1,12 +1,11 @@
/* global List */
/* global ListAssignee */
/* global ListLabel */
-/* global listObj */
-/* global boardsMockInterceptor */
/* global BoardService */
-/* global mockBoardService */
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';
@@ -14,13 +13,15 @@ import '~/boards/models/list';
import '~/boards/models/label';
import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
-import './mock_data';
+import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
describe('Board card', () => {
let vm;
+ let mock;
beforeEach((done) => {
- Vue.http.interceptors.push(boardsMockInterceptor);
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
@@ -54,7 +55,7 @@ describe('Board card', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ mock.reset();
});
it('returns false when detailIssue is empty', () => {
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 6bd00943a8f..7c5888b6d82 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -1,11 +1,9 @@
/* global BoardService */
-/* global boardsMockInterceptor */
/* global List */
-/* global listObj */
/* global ListIssue */
-/* global mockBoardService */
import Vue from 'vue';
-import _ from 'underscore';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Sortable from 'vendor/Sortable';
import BoardList from '~/boards/components/board_list';
import eventHub from '~/boards/eventhub';
@@ -13,18 +11,20 @@ import '~/boards/mixins/sortable_default_options';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/stores/boards_store';
-import './mock_data';
+import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
window.Sortable = Sortable;
describe('Board list component', () => {
+ let mock;
let component;
beforeEach((done) => {
const el = document.createElement('div');
document.body.appendChild(el);
- Vue.http.interceptors.push(boardsMockInterceptor);
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
@@ -60,7 +60,7 @@ describe('Board list component', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ mock.reset();
});
it('renders component', () => {
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index 02e6692dda8..c62c537841c 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -1,24 +1,22 @@
-/* global boardsMockInterceptor */
/* global BoardService */
/* global List */
-/* global listObj */
-/* global mockBoardService */
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import boardNewIssue from '~/boards/components/board_new_issue';
import '~/boards/models/list';
-import './mock_data';
+import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
describe('Issue boards new issue form', () => {
let vm;
let list;
+ let mock;
let newIssueMock;
const promiseReturn = {
- json() {
- return {
- iid: 100,
- };
+ data: {
+ iid: 100,
},
};
@@ -35,7 +33,9 @@ describe('Issue boards new issue form', () => {
const BoardNewIssueComp = Vue.extend(boardNewIssue);
- Vue.http.interceptors.push(boardsMockInterceptor);
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
@@ -56,7 +56,10 @@ describe('Issue boards new issue form', () => {
.catch(done.fail);
});
- afterEach(() => vm.$destroy());
+ afterEach(() => {
+ vm.$destroy();
+ mock.reset();
+ });
it('calls submit if submit button is clicked', (done) => {
spyOn(vm, 'submit').and.callFake(e => e.preventDefault());
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 9e5b0bd3efe..49fb20f4c84 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -1,26 +1,26 @@
/* eslint-disable comma-dangle, one-var, no-unused-vars */
/* global BoardService */
-/* global boardsMockInterceptor */
-/* global listObj */
-/* global listObjDuplicate */
/* global ListIssue */
-/* global mockBoardService */
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
-import '~/lib/utils/url_utility';
import '~/boards/models/issue';
import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
-import './mock_data';
+import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
describe('Store', () => {
+ let mock;
+
beforeEach(() => {
- Vue.http.interceptors.push(boardsMockInterceptor);
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
@@ -35,7 +35,7 @@ describe('Store', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ mock.reset();
});
it('starts with a blank state', () => {
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index 8dacac20cad..19346e305cf 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -1,9 +1,8 @@
-/* global mockBoardService */
import Vue from 'vue';
import '~/boards/services/board_service';
import '~/boards/components/board';
import '~/boards/models/list';
-import '../mock_data';
+import { mockBoardService } from '../mock_data';
describe('Board component', () => {
let vm;
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 7d430ec35e2..8ef221257be 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -1,6 +1,5 @@
/* global ListAssignee */
/* global ListLabel */
-/* global listObj */
/* global ListIssue */
import Vue from 'vue';
@@ -11,7 +10,7 @@ import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/stores/boards_store';
import '~/boards/components/issue_card_inner';
-import './mock_data';
+import { listObj } from './mock_data';
describe('Issue card component', () => {
const user = new ListAssignee({
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 10b88878c2a..dbbe14fe3e0 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -1,17 +1,15 @@
/* eslint-disable comma-dangle */
/* global BoardService */
/* global ListIssue */
-/* global mockBoardService */
import Vue from 'vue';
-import '~/lib/utils/url_utility';
import '~/boards/models/issue';
import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
-import './mock_data';
+import { mockBoardService } from './mock_data';
describe('Issue model', () => {
let issue;
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index d4627223a12..645ce831b53 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -1,28 +1,26 @@
/* eslint-disable comma-dangle */
-/* global boardsMockInterceptor */
/* global BoardService */
-/* global mockBoardService */
/* global List */
/* global ListIssue */
-/* global listObj */
-/* global listObjDuplicate */
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
-import '~/lib/utils/url_utility';
import '~/boards/models/issue';
import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
-import './mock_data';
+import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
describe('List model', () => {
let list;
+ let mock;
beforeEach(() => {
- Vue.http.interceptors.push(boardsMockInterceptor);
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
gl.boardService = mockBoardService({
bulkUpdatePath: '/test/issue-boards/board/1/lists',
});
@@ -32,7 +30,7 @@ describe('List model', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ mock.reset();
});
it('gets issues when created', (done) => {
@@ -159,10 +157,8 @@ describe('List model', () => {
describe('newIssue', () => {
beforeEach(() => {
spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({
- json() {
- return {
- id: 42,
- };
+ data: {
+ id: 42,
},
}));
});
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 0a93086985e..9ae2d535398 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,20 +1,20 @@
/* global BoardService */
/* eslint-disable comma-dangle, no-unused-vars, quote-props */
-const listObj = {
- id: _.random(10000),
+export const listObj = {
+ id: 300,
position: 0,
title: 'Test',
list_type: 'label',
label: {
- id: _.random(10000),
+ id: 5000,
title: 'Testing',
color: 'red',
description: 'testing;'
}
};
-const listObjDuplicate = {
+export const listObjDuplicate = {
id: listObj.id,
position: 1,
title: 'Test',
@@ -27,9 +27,9 @@ const listObjDuplicate = {
}
};
-const BoardsMockData = {
+export const BoardsMockData = {
'GET': {
- '/test/boards/1{/id}/issues': {
+ '/test/-/boards/1/lists/300/issues?id=300&page=1&=': {
issues: [{
title: 'Testing',
id: 1,
@@ -41,7 +41,7 @@ const BoardsMockData = {
}
},
'POST': {
- '/test/boards/1{/id}': listObj
+ '/test/-/boards/1/lists': listObj
},
'PUT': {
'/test/issue-boards/board/1/lists{/id}': {}
@@ -51,17 +51,14 @@ const BoardsMockData = {
}
};
-const boardsMockInterceptor = (request, next) => {
- const body = BoardsMockData[request.method][request.url];
-
- next(request.respondWith(JSON.stringify(body), {
- status: 200
- }));
+export const boardsMockInterceptor = (config) => {
+ const body = BoardsMockData[config.method.toUpperCase()][config.url];
+ return [200, body];
};
-const mockBoardService = (opts = {}) => {
- const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/board';
- const listsEndpoint = opts.listsEndpoint || '/test/boards/1';
+export const mockBoardService = (opts = {}) => {
+ const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/boards.json';
+ const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
const bulkUpdatePath = opts.bulkUpdatePath || '';
const boardId = opts.boardId || '1';
@@ -72,9 +69,3 @@ const mockBoardService = (opts = {}) => {
boardId,
});
};
-
-window.listObj = listObj;
-window.listObjDuplicate = listObjDuplicate;
-window.BoardsMockData = BoardsMockData;
-window.boardsMockInterceptor = boardsMockInterceptor;
-window.mockBoardService = mockBoardService;
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index 7460da031c4..1a8affad4e3 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -21,6 +21,7 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
},
});
});
@@ -33,6 +34,10 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined();
});
+ it('renders a row for Prometheus', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined();
+ });
+
/* * /
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index af6b6a73819..253b3c45243 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -22,6 +22,11 @@ const CLUSTERS_MOCK_DATA = {
name: 'runner',
status: APPLICATION_INSTALLING,
status_reason: null,
+ },
+ {
+ name: 'prometheus',
+ status: APPLICATION_ERROR,
+ status_reason: 'Cannot connect',
}],
},
},
@@ -30,6 +35,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/helm': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
},
};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index cb8b3d38e2e..ec2889355e6 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -82,6 +82,13 @@ describe('Clusters Store', () => {
requestStatus: null,
requestReason: null,
},
+ prometheus: {
+ title: 'Prometheus',
+ status: mockResponseData.applications[3].status,
+ statusReason: mockResponseData.applications[3].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ },
},
});
});
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index 974815fe939..5026eaafaca 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -1,7 +1,6 @@
-/* global Sidebar */
/* eslint-disable no-new */
import _ from 'underscore';
-import '~/right_sidebar';
+import Sidebar from '~/right_sidebar';
describe('Issuable right sidebar collapsed todo toggle', () => {
const fixtureName = 'issues/open-issue.html.raw';
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index e5a5e3293b9..d0176520440 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -1,5 +1,4 @@
import 'vendor/jquery.endless-scroll';
-import '~/pager';
import CommitsList from '~/commits';
describe('Commits List', () => {
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index 0f7bf9ec712..2e5b65f5610 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -1,110 +1,108 @@
import * as datetimeUtility from '~/lib/utils/datetime_utility';
-(() => {
- describe('Date time utils', () => {
- describe('timeFor', () => {
- it('returns `past due` when in past', () => {
- const date = new Date();
- date.setFullYear(date.getFullYear() - 1);
-
- expect(
- gl.utils.timeFor(date),
- ).toBe('Past due');
- });
-
- it('returns remaining time when in the future', () => {
- const date = new Date();
- date.setFullYear(date.getFullYear() + 1);
-
- // Add a day to prevent a transient error. If date is even 1 second
- // short of a full year, timeFor will return '11 months remaining'
- date.setDate(date.getDate() + 1);
-
- expect(
- gl.utils.timeFor(date),
- ).toBe('1 year remaining');
- });
+describe('Date time utils', () => {
+ describe('timeFor', () => {
+ it('returns `past due` when in past', () => {
+ const date = new Date();
+ date.setFullYear(date.getFullYear() - 1);
+
+ expect(
+ datetimeUtility.timeFor(date),
+ ).toBe('Past due');
});
- describe('get day name', () => {
- it('should return Sunday', () => {
- const day = gl.utils.getDayName(new Date('07/17/2016'));
- expect(day).toBe('Sunday');
- });
-
- it('should return Monday', () => {
- const day = gl.utils.getDayName(new Date('07/18/2016'));
- expect(day).toBe('Monday');
- });
-
- it('should return Tuesday', () => {
- const day = gl.utils.getDayName(new Date('07/19/2016'));
- expect(day).toBe('Tuesday');
- });
-
- it('should return Wednesday', () => {
- const day = gl.utils.getDayName(new Date('07/20/2016'));
- expect(day).toBe('Wednesday');
- });
-
- it('should return Thursday', () => {
- const day = gl.utils.getDayName(new Date('07/21/2016'));
- expect(day).toBe('Thursday');
- });
-
- it('should return Friday', () => {
- const day = gl.utils.getDayName(new Date('07/22/2016'));
- expect(day).toBe('Friday');
- });
-
- it('should return Saturday', () => {
- const day = gl.utils.getDayName(new Date('07/23/2016'));
- expect(day).toBe('Saturday');
- });
- });
+ it('returns remaining time when in the future', () => {
+ const date = new Date();
+ date.setFullYear(date.getFullYear() + 1);
+
+ // Add a day to prevent a transient error. If date is even 1 second
+ // short of a full year, timeFor will return '11 months remaining'
+ date.setDate(date.getDate() + 1);
- describe('get day difference', () => {
- it('should return 7', () => {
- const firstDay = new Date('07/01/2016');
- const secondDay = new Date('07/08/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(7);
- });
-
- it('should return 31', () => {
- const firstDay = new Date('07/01/2016');
- const secondDay = new Date('08/01/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(31);
- });
-
- it('should return 365', () => {
- const firstDay = new Date('07/02/2015');
- const secondDay = new Date('07/01/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(365);
- });
+ expect(
+ datetimeUtility.timeFor(date),
+ ).toBe('1 year remaining');
});
});
- describe('timeIntervalInWords', () => {
- it('should return string with number of minutes and seconds', () => {
- expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds');
- expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second');
- expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
- expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
+ describe('get day name', () => {
+ it('should return Sunday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/17/2016'));
+ expect(day).toBe('Sunday');
+ });
+
+ it('should return Monday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/18/2016'));
+ expect(day).toBe('Monday');
+ });
+
+ it('should return Tuesday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/19/2016'));
+ expect(day).toBe('Tuesday');
+ });
+
+ it('should return Wednesday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/20/2016'));
+ expect(day).toBe('Wednesday');
+ });
+
+ it('should return Thursday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/21/2016'));
+ expect(day).toBe('Thursday');
+ });
+
+ it('should return Friday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/22/2016'));
+ expect(day).toBe('Friday');
+ });
+
+ it('should return Saturday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/23/2016'));
+ expect(day).toBe('Saturday');
});
});
- describe('dateInWords', () => {
- const date = new Date('07/01/2016');
+ describe('get day difference', () => {
+ it('should return 7', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('07/08/2016');
+ const difference = datetimeUtility.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(7);
+ });
- it('should return date in words', () => {
- expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016');
+ it('should return 31', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('08/01/2016');
+ const difference = datetimeUtility.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(31);
});
- it('should return abbreviated month name', () => {
- expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
+ it('should return 365', () => {
+ const firstDay = new Date('07/02/2015');
+ const secondDay = new Date('07/01/2016');
+ const difference = datetimeUtility.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(365);
});
});
-})();
+});
+
+describe('timeIntervalInWords', () => {
+ it('should return string with number of minutes and seconds', () => {
+ expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds');
+ expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second');
+ expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
+ expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
+ });
+});
+
+describe('dateInWords', () => {
+ const date = new Date('07/01/2016');
+
+ it('should return date in words', () => {
+ expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016');
+ });
+
+ it('should return abbreviated month name', () => {
+ expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
index 5b64cbb2dfc..2f28c5bbf01 100644
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import DeployKeysStore from '~/deploy_keys/store';
import key from '~/deploy_keys/components/key.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Deploy keys key', () => {
let vm;
@@ -37,7 +38,7 @@ describe('Deploy keys key', () => {
it('renders human friendly formatted created date', () => {
expect(
vm.$el.querySelector('.key-created-at').textContent.trim(),
- ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`);
+ ).toBe(`created ${getTimeago().format(deployKey.created_at)}`);
});
it('shows edit button', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 230c15e5de6..b8890e4cda1 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,8 +1,8 @@
+import * as urlUtils from '~/lib/utils/url_utility';
import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import '~/lib/utils/url_utility';
import '~/lib/utils/common_utils';
import '~/filtered_search/filtered_search_token_keys';
import '~/filtered_search/filtered_search_tokenizer';
@@ -162,7 +162,7 @@ describe('Filtered Search Manager', () => {
it('should search with a single word', (done) => {
input.value = 'searchTerm';
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
});
@@ -173,7 +173,7 @@ describe('Filtered Search Manager', () => {
it('should search with multiple words', (done) => {
input.value = 'awesome search terms';
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
});
@@ -184,7 +184,7 @@ describe('Filtered Search Manager', () => {
it('should search with special characters', (done) => {
input.value = '~!@#$%^&*()_+{}:<>,.?/';
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
});
@@ -198,7 +198,7 @@ describe('Filtered Search Manager', () => {
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
@@ -252,6 +252,7 @@ describe('Filtered Search Manager', () => {
it('removes last token', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
dispatchBackspaceEvent(input, 'keyup');
+ dispatchBackspaceEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
});
@@ -259,6 +260,7 @@ describe('Filtered Search Manager', () => {
it('sets the input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
dispatchDeleteEvent(input, 'keyup');
+ dispatchDeleteEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
expect(input.value).toEqual('~bug');
@@ -276,6 +278,18 @@ describe('Filtered Search Manager', () => {
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
expect(input.value).toEqual('text');
});
+
+ it('does not remove previous token on single backspace press', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
+
+ input.value = 't';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('t');
+ });
});
describe('removeToken', () => {
diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml
index 85ee61f0b54..0161c0550d1 100644
--- a/spec/javascripts/fixtures/pipelines.html.haml
+++ b/spec/javascripts/fixtures/pipelines.html.haml
@@ -7,4 +7,6 @@
"new-pipeline-path" => 'foo',
"can-create-pipeline" => 'true',
"has-ci" => 'foo',
- "ci-lint-path" => 'foo' } }
+ "ci-lint-path" => 'foo',
+ "reset-cache-path" => 'foo' } }
+
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index 4f20e31f511..a3fa07d5bc2 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -253,7 +253,7 @@ describe('Fly out sidebar navigation', () => {
it('shows collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-icons-only');
+ sidebar.classList.add('sidebar-collapsed-desktop');
subItems.classList.add('is-fly-out-only');
setSidebar(sidebar);
@@ -343,7 +343,7 @@ describe('Fly out sidebar navigation', () => {
it('returns true when active & collapsed sidebar', () => {
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-icons-only');
+ sidebar.classList.add('sidebar-collapsed-desktop');
el.classList.add('active');
setSidebar(sidebar);
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index ca048123bf7..b13d1bf8dff 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -2,7 +2,7 @@
import '~/gl_dropdown';
import '~/lib/utils/common_utils';
-import '~/lib/utils/url_utility';
+import * as urlUtils from '~/lib/utils/url_utility';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
@@ -137,13 +137,13 @@ describe('glDropdown', function describeDropdown() {
expect(this.dropdownContainerElement).toHaveClass('open');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
- spyOn(gl.utils, 'visitUrl').and.stub();
+ spyOn(urlUtils, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
+ if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 861f26e162f..6599839a526 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,8 +1,10 @@
/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */
-
-import d3 from 'd3';
+import { scaleLinear, scaleTime } from 'd3-scale';
+import { timeParse } from 'd3-time-format';
import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph';
+const d3 = { scaleLinear, scaleTime, timeParse };
+
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
it("set the x_domain", function () {
@@ -53,7 +55,7 @@ describe("ContributorsGraph", function () {
it("sets the instance's x domain using the prototype's x_domain", function () {
ContributorsGraph.prototype.x_domain = 20;
var instance = new ContributorsGraph();
- instance.x = d3.time.scale().range([0, 100]).clamp(true);
+ instance.x = d3.scaleTime().range([0, 100]).clamp(true);
spyOn(instance.x, 'domain');
instance.set_x_domain();
expect(instance.x.domain).toHaveBeenCalledWith(20);
@@ -64,7 +66,7 @@ describe("ContributorsGraph", function () {
it("sets the instance's y domain using the prototype's y_domain", function () {
ContributorsGraph.prototype.y_domain = 30;
var instance = new ContributorsGraph();
- instance.y = d3.scale.linear().range([100, 0]).nice();
+ instance.y = d3.scaleLinear().range([100, 0]).nice();
spyOn(instance.y, 'domain');
instance.set_y_domain();
expect(instance.y.domain).toHaveBeenCalledWith(30);
@@ -118,7 +120,7 @@ describe("ContributorsMasterGraph", function () {
describe("#parse_dates", function () {
it("parses the dates", function () {
var graph = new ContributorsMasterGraph();
- var parseDate = d3.time.format("%Y-%m-%d").parse;
+ var parseDate = d3.timeParse("%Y-%m-%d");
var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }];
var correct = [{ date: parseDate(data[0].date) }, { date: parseDate(data[1].date) }];
graph.parse_dates(data);
diff --git a/spec/javascripts/graphs/stat_graph_contributors_spec.js b/spec/javascripts/graphs/stat_graph_contributors_spec.js
new file mode 100644
index 00000000000..962423462e7
--- /dev/null
+++ b/spec/javascripts/graphs/stat_graph_contributors_spec.js
@@ -0,0 +1,26 @@
+import ContributorsStatGraph from '~/graphs/stat_graph_contributors';
+import { ContributorsGraph } from '~/graphs/stat_graph_contributors_graph';
+
+import { setLanguage } from '../helpers/locale_helper';
+
+describe('ContributorsStatGraph', () => {
+ describe('change_date_header', () => {
+ beforeAll(() => {
+ setLanguage('de');
+ });
+
+ afterAll(() => {
+ setLanguage(null);
+ });
+
+ it('uses the locale to display date ranges', () => {
+ ContributorsGraph.init_x_domain([{ date: '2013-01-31' }, { date: '2012-01-31' }]);
+ setFixtures('<div id="date_header"></div>');
+ const graph = new ContributorsStatGraph();
+
+ graph.change_date_header();
+
+ expect(document.getElementById('date_header').innerText).toBe('31. Januar 2012 – 31. Januar 2013');
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index 59d4f7c45c6..97e39f6411b 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
+import * as utils from '~/lib/utils/url_utility';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
-
import eventHub from '~/groups/event_hub';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
@@ -176,7 +176,7 @@ describe('AppComponent', () => {
it('should fetch groups for provided page details and update window state', (done) => {
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
spyOn(vm, 'updateGroups').and.callThrough();
- spyOn(gl.utils, 'mergeUrlParams').and.callThrough();
+ spyOn(utils, 'mergeUrlParams').and.callThrough();
spyOn(window.history, 'replaceState');
spyOn($, 'scrollTo');
@@ -192,7 +192,7 @@ describe('AppComponent', () => {
setTimeout(() => {
expect(vm.isLoading).toBeFalsy();
expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(gl.utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
+ expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
expect(window.history.replaceState).toHaveBeenCalledWith({
page: jasmine.any(String),
}, jasmine.any(String), jasmine.any(String));
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index 0f4fbdae445..618d0022e4f 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-
+import * as urlUtils from '~/lib/utils/url_utility';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import eventHub from '~/groups/event_hub';
@@ -136,13 +136,13 @@ describe('GroupItemComponent', () => {
const group = Object.assign({}, mockParentGroupItem);
group.childrenCount = 0;
const newVm = createComponent(group);
- spyOn(gl.utils, 'visitUrl').and.stub();
+ spyOn(urlUtils, 'visitUrl').and.stub();
spyOn(eventHub, '$emit');
newVm.onClickRowGroup(event);
setTimeout(() => {
expect(eventHub.$emit).not.toHaveBeenCalled();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
done();
}, 0);
});
diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js
index 2ce1a749a96..6d6fb410859 100644
--- a/spec/javascripts/groups/components/item_actions_spec.js
+++ b/spec/javascripts/groups/components/item_actions_spec.js
@@ -36,28 +36,22 @@ describe('ItemActionsComponent', () => {
describe('methods', () => {
describe('onLeaveGroup', () => {
- it('should change `dialogStatus` prop to `true` which shows confirmation dialog', () => {
- expect(vm.dialogStatus).toBeFalsy();
+ it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => {
+ expect(vm.modalStatus).toBeFalsy();
vm.onLeaveGroup();
- expect(vm.dialogStatus).toBeTruthy();
+ expect(vm.modalStatus).toBeTruthy();
});
});
describe('leaveGroup', () => {
- it('should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => {
+ it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => {
spyOn(eventHub, '$emit');
- vm.dialogStatus = true;
- vm.leaveGroup(true);
- expect(vm.dialogStatus).toBeFalsy();
- expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup);
- });
+ vm.modalStatus = true;
- it('should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => {
- spyOn(eventHub, '$emit');
- vm.dialogStatus = true;
- vm.leaveGroup(false);
- expect(vm.dialogStatus).toBeFalsy();
- expect(eventHub.$emit).not.toHaveBeenCalled();
+ vm.leaveGroup();
+
+ expect(vm.modalStatus).toBeFalsy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup);
});
});
});
@@ -99,9 +93,9 @@ describe('ItemActionsComponent', () => {
newVm.$destroy();
});
- it('should show modal dialog when `dialogStatus` is set to `true`', () => {
- vm.dialogStatus = true;
- const modalDialogEl = vm.$el.querySelector('.modal.popup-dialog');
+ it('should show modal dialog when `modalStatus` is set to `true`', () => {
+ vm.modalStatus = true;
+ const modalDialogEl = vm.$el.querySelector('.modal');
expect(modalDialogEl).toBeDefined();
expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js
index 4310a07e6e6..8faad455825 100644
--- a/spec/javascripts/groups/components/item_caret_spec.js
+++ b/spec/javascripts/groups/components/item_caret_spec.js
@@ -16,24 +16,20 @@ describe('ItemCaretComponent', () => {
describe('template', () => {
it('should render component template correctly', () => {
const vm = createComponent();
- vm.$mount();
expect(vm.$el.classList.contains('folder-caret')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('svg').length).toBe(1);
vm.$destroy();
});
it('should render caret down icon if `isGroupOpen` prop is `true`', () => {
const vm = createComponent(true);
- vm.$mount();
- expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(1);
- expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(0);
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-down');
vm.$destroy();
});
it('should render caret right icon if `isGroupOpen` prop is `false`', () => {
const vm = createComponent();
- vm.$mount();
- expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(0);
- expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(1);
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-right');
vm.$destroy();
});
});
diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js
index e200f9f08bd..55a7a713ca6 100644
--- a/spec/javascripts/groups/components/item_stats_spec.js
+++ b/spec/javascripts/groups/components/item_stats_spec.js
@@ -26,7 +26,6 @@ describe('ItemStatsComponent', () => {
Object.keys(VISIBILITY_TYPE_ICON).forEach((visibility) => {
const item = Object.assign({}, mockParentGroupItem, { visibility });
const vm = createComponent(item);
- vm.$mount();
expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]);
vm.$destroy();
});
@@ -41,7 +40,6 @@ describe('ItemStatsComponent', () => {
type: ITEM_TYPE.GROUP,
});
const vm = createComponent(item);
- vm.$mount();
expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]);
vm.$destroy();
});
@@ -54,7 +52,6 @@ describe('ItemStatsComponent', () => {
type: ITEM_TYPE.PROJECT,
});
const vm = createComponent(item);
- vm.$mount();
expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]);
vm.$destroy();
});
@@ -68,13 +65,11 @@ describe('ItemStatsComponent', () => {
item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT });
vm = createComponent(item);
- vm.$mount();
expect(vm.isProject).toBeTruthy();
vm.$destroy();
item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
vm = createComponent(item);
- vm.$mount();
expect(vm.isProject).toBeFalsy();
vm.$destroy();
});
@@ -87,13 +82,11 @@ describe('ItemStatsComponent', () => {
item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
vm = createComponent(item);
- vm.$mount();
expect(vm.isGroup).toBeTruthy();
vm.$destroy();
item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT });
vm = createComponent(item);
- vm.$mount();
expect(vm.isGroup).toBeFalsy();
vm.$destroy();
});
@@ -101,57 +94,37 @@ describe('ItemStatsComponent', () => {
});
describe('template', () => {
- it('should render component template correctly', () => {
+ it('renders component container element correctly', () => {
const vm = createComponent();
- vm.$mount();
- const visibilityIconEl = vm.$el.querySelector('.item-visibility');
- expect(vm.$el.classList.contains('.stats')).toBeDefined();
- expect(visibilityIconEl).toBeDefined();
- expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip);
- expect(visibilityIconEl.querySelector('i.fa')).toBeDefined();
+ expect(vm.$el.classList.contains('stats')).toBeTruthy();
vm.$destroy();
});
- it('should render stat icons if `item.type` is Group', () => {
- const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
- const vm = createComponent(item);
- vm.$mount();
-
- const subgroupIconEl = vm.$el.querySelector('span.number-subgroups');
- expect(subgroupIconEl).toBeDefined();
- expect(subgroupIconEl.dataset.originalTitle).toBe('Subgroups');
- expect(subgroupIconEl.querySelector('i.fa.fa-folder')).toBeDefined();
- expect(subgroupIconEl.innerText.trim()).toBe(`${vm.item.subgroupCount}`);
-
- const projectsIconEl = vm.$el.querySelector('span.number-projects');
- expect(projectsIconEl).toBeDefined();
- expect(projectsIconEl.dataset.originalTitle).toBe('Projects');
- expect(projectsIconEl.querySelector('i.fa.fa-bookmark')).toBeDefined();
- expect(projectsIconEl.innerText.trim()).toBe(`${vm.item.projectCount}`);
-
- const membersIconEl = vm.$el.querySelector('span.number-users');
- expect(membersIconEl).toBeDefined();
- expect(membersIconEl.dataset.originalTitle).toBe('Members');
- expect(membersIconEl.querySelector('i.fa.fa-users')).toBeDefined();
- expect(membersIconEl.innerText.trim()).toBe(`${vm.item.memberCount}`);
+ it('renders item visibility icon and tooltip correctly', () => {
+ const vm = createComponent();
+
+ const visibilityIconEl = vm.$el.querySelector('.item-visibility');
+ expect(visibilityIconEl).not.toBe(null);
+ expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip);
+ expect(visibilityIconEl.querySelectorAll('svg').length > 0).toBeTruthy();
vm.$destroy();
});
- it('should render stat icons if `item.type` is Project', () => {
+ it('renders start count and last updated information for project item correctly', () => {
const item = Object.assign({}, mockParentGroupItem, {
type: ITEM_TYPE.PROJECT,
starCount: 4,
});
const vm = createComponent(item);
- vm.$mount();
const projectStarIconEl = vm.$el.querySelector('.project-stars');
- expect(projectStarIconEl).toBeDefined();
- expect(projectStarIconEl.querySelector('i.fa.fa-star')).toBeDefined();
- expect(projectStarIconEl.innerText.trim()).toBe(`${vm.item.starCount}`);
+ expect(projectStarIconEl).not.toBe(null);
+ expect(projectStarIconEl.querySelectorAll('svg').length > 0).toBeTruthy();
+ expect(projectStarIconEl.querySelectorAll('.stat-value').length > 0).toBeTruthy();
+ expect(vm.$el.querySelectorAll('.last-updated').length > 0).toBeTruthy();
vm.$destroy();
});
diff --git a/spec/javascripts/groups/components/item_stats_value_spec.js b/spec/javascripts/groups/components/item_stats_value_spec.js
new file mode 100644
index 00000000000..e990870aaa6
--- /dev/null
+++ b/spec/javascripts/groups/components/item_stats_value_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+
+import itemStatsValueComponent from '~/groups/components/item_stats_value.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => {
+ const Component = Vue.extend(itemStatsValueComponent);
+
+ return mountComponent(Component, {
+ title,
+ cssClass,
+ iconName,
+ tooltipPlacement,
+ value,
+ });
+};
+
+describe('ItemStatsValueComponent', () => {
+ describe('computed', () => {
+ let vm;
+ const itemConfig = {
+ title: 'Subgroups',
+ cssClass: 'number-subgroups',
+ iconName: 'folder',
+ tooltipPlacement: 'left',
+ };
+
+ describe('isValuePresent', () => {
+ it('returns true if non-empty `value` is present', () => {
+ vm = createComponent(Object.assign({}, itemConfig, { value: 10 }));
+ expect(vm.isValuePresent).toBeTruthy();
+ });
+
+ it('returns false if empty `value` is present', () => {
+ vm = createComponent(itemConfig);
+ expect(vm.isValuePresent).toBeFalsy();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ beforeEach(() => {
+ vm = createComponent({
+ title: 'Subgroups',
+ cssClass: 'number-subgroups',
+ iconName: 'folder',
+ tooltipPlacement: 'left',
+ value: 10,
+ });
+ });
+
+ it('renders component element correctly', () => {
+ expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('svg').length > 0).toBeTruthy();
+ expect(vm.$el.querySelectorAll('.stat-value').length > 0).toBeTruthy();
+ });
+
+ it('renders element tooltip correctly', () => {
+ expect(vm.$el.dataset.originalTitle).toBe('Subgroups');
+ expect(vm.$el.dataset.placement).toBe('left');
+ });
+
+ it('renders element icon correctly', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('folder');
+ });
+
+ it('renders value count correctly', () => {
+ expect(vm.$el.querySelector('.stat-value').innerText.trim()).toContain('10');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js
index 528e6ed1b4c..495cc97b475 100644
--- a/spec/javascripts/groups/components/item_type_icon_spec.js
+++ b/spec/javascripts/groups/components/item_type_icon_spec.js
@@ -28,12 +28,12 @@ describe('ItemTypeIconComponent', () => {
vm = createComponent(ITEM_TYPE.GROUP, true);
vm.$mount();
- expect(vm.$el.querySelector('i.fa.fa-folder-open')).toBeDefined();
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder-open');
vm.$destroy();
vm = createComponent(ITEM_TYPE.GROUP);
vm.$mount();
- expect(vm.$el.querySelector('i.fa.fa-folder')).toBeDefined();
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder');
vm.$destroy();
});
@@ -42,12 +42,12 @@ describe('ItemTypeIconComponent', () => {
vm = createComponent(ITEM_TYPE.PROJECT);
vm.$mount();
- expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(1);
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('bookmark');
vm.$destroy();
vm = createComponent(ITEM_TYPE.GROUP);
vm.$mount();
- expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(0);
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).not.toContain('bookmark');
vm.$destroy();
});
});
diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js
index 6184d671790..8bf6417487d 100644
--- a/spec/javascripts/groups/mock_data.js
+++ b/spec/javascripts/groups/mock_data.js
@@ -18,9 +18,9 @@ export const PROJECT_VISIBILITY_TYPE = {
};
export const VISIBILITY_TYPE_ICON = {
- public: 'fa-globe',
- internal: 'fa-shield',
- private: 'fa-lock',
+ public: 'earth',
+ internal: 'shield',
+ private: 'lock',
};
export const mockParentGroupItem = {
@@ -46,6 +46,7 @@ export const mockParentGroupItem = {
isOpen: true,
isChildrenLoading: false,
isBeingRemoved: false,
+ updatedAt: '2017-04-09T18:40:39.101Z',
};
export const mockRawChildren = [
@@ -69,6 +70,7 @@ export const mockRawChildren = [
subgroup_count: 2,
can_leave: false,
children: [],
+ updated_at: '2017-04-09T18:40:39.101Z',
},
];
@@ -96,6 +98,7 @@ export const mockChildren = [
isOpen: true,
isChildrenLoading: false,
isBeingRemoved: false,
+ updatedAt: '2017-04-09T18:40:39.101Z',
},
];
@@ -119,6 +122,7 @@ export const mockGroups = [
project_count: 2,
subgroup_count: 0,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
},
{
id: 67,
@@ -139,6 +143,7 @@ export const mockGroups = [
project_count: 0,
subgroup_count: 0,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
},
{
id: 54,
@@ -159,6 +164,7 @@ export const mockGroups = [
project_count: 0,
subgroup_count: 1,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
},
{
id: 5,
@@ -179,6 +185,7 @@ export const mockGroups = [
project_count: 1,
subgroup_count: 0,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
},
{
id: 4,
@@ -199,6 +206,7 @@ export const mockGroups = [
project_count: 2,
subgroup_count: 0,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
},
{
id: 3,
@@ -219,6 +227,7 @@ export const mockGroups = [
project_count: 1,
subgroup_count: 0,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
},
{
id: 2,
@@ -239,6 +248,7 @@ export const mockGroups = [
project_count: 4,
subgroup_count: 0,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
},
];
@@ -262,6 +272,7 @@ export const mockSearchedGroups = [
project_count: 1,
subgroup_count: 2,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
children: [
{
id: 57,
@@ -282,6 +293,7 @@ export const mockSearchedGroups = [
project_count: 4,
subgroup_count: 2,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
children: [
{
id: 60,
@@ -302,6 +314,7 @@ export const mockSearchedGroups = [
project_count: 0,
subgroup_count: 1,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
children: [
{
id: 61,
@@ -322,6 +335,7 @@ export const mockSearchedGroups = [
project_count: 2,
subgroup_count: 0,
can_leave: false,
+ updated_at: '2017-04-09T18:40:39.101Z',
children: [
{
id: 17,
@@ -336,6 +350,7 @@ export const mockSearchedGroups = [
permission: null,
edit_path: '/platform/hardware/bsp/kernel/common/v4.4/edit',
star_count: 0,
+ updated_at: '2017-09-12T06:37:04.925Z',
},
{
id: 16,
@@ -350,6 +365,7 @@ export const mockSearchedGroups = [
permission: null,
edit_path: '/platform/hardware/bsp/kernel/common/v4.1/edit',
star_count: 0,
+ updated_at: '2017-04-09T18:41:03.112Z',
},
],
},
diff --git a/spec/javascripts/helpers/locale_helper.js b/spec/javascripts/helpers/locale_helper.js
new file mode 100644
index 00000000000..99e6ce61234
--- /dev/null
+++ b/spec/javascripts/helpers/locale_helper.js
@@ -0,0 +1,11 @@
+/* eslint-disable import/prefer-default-export */
+
+export const setLanguage = (languageCode) => {
+ const htmlElement = document.querySelector('html');
+
+ if (languageCode) {
+ htmlElement.setAttribute('lang', languageCode);
+ } else {
+ htmlElement.removeAttribute('lang');
+ }
+};
diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
index fb9c7e59031..ce3add1fd90 100644
--- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js
+++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
@@ -89,15 +89,8 @@ describe('badge helper', () => {
});
it('should create icon comment button', () => {
- const iconEl = buttonEl.querySelector('i');
+ const iconEl = buttonEl.querySelector('svg');
expect(iconEl).toBeDefined();
- expect(iconEl.classList.contains('fa')).toEqual(true);
- expect(iconEl.classList.contains('fa-comment-o')).toEqual(true);
- });
-
- it('should have .image-comment-badge.inverted in button class', () => {
- expect(buttonEl.classList.contains('image-comment-badge')).toEqual(true);
- expect(buttonEl.classList.contains('inverted')).toEqual(true);
});
});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index b47a8bf705f..1454ca52018 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -1,34 +1,41 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import '~/render_math';
import '~/render_gfm';
+import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import issueShowData from '../mock_data';
+import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
}
+const REALTIME_REQUEST_STACK = [
+ issueShowData.initialRequest,
+ issueShowData.secondRequest,
+];
+
describe('Issuable output', () => {
- let requestData = issueShowData.initialRequest;
+ let mock;
+ let realtimeRequestCount = 0;
+ let vm;
document.body.innerHTML = '<span id="task_status"></span>';
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(requestData), {
- status: 200,
- }));
- };
-
- let vm;
-
beforeEach((done) => {
spyOn(eventHub, '$emit');
const IssuableDescriptionComponent = Vue.extend(issuableApp);
- requestData = issueShowData.initialRequest;
- Vue.http.interceptors.push(interceptor);
+ mock = new MockAdapter(axios);
+ mock.onGet('/gitlab-org/gitlab-shell/issues/9/realtime_changes/realtime_changes').reply(() => {
+ const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
+ realtimeRequestCount += 1;
+ return res;
+ });
vm = new IssuableDescriptionComponent({
propsData: {
@@ -52,9 +59,11 @@ describe('Issuable output', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ mock.reset();
+ realtimeRequestCount = 0;
vm.poll.stop();
+ vm.$destroy();
});
it('should render a title/description/edited and update title/description/edited on update', (done) => {
@@ -73,7 +82,6 @@ describe('Issuable output', () => {
expect(editedText.querySelector('time')).toBeTruthy();
})
.then(() => {
- requestData = issueShowData.secondRequest;
vm.poll.makeRequest();
})
.then(() => new Promise(resolve => setTimeout(resolve)))
@@ -137,24 +145,19 @@ describe('Issuable output', () => {
spyOn(vm.service, 'getData').and.callThrough();
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
- json() {
- return {
- confidential: false,
- web_url: location.pathname,
- };
+ data: {
+ confidential: false,
+ web_url: location.pathname,
},
});
}));
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(
- vm.service.getData,
- ).toHaveBeenCalled();
-
- done();
- });
+ vm.updateIssuable()
+ .then(() => {
+ expect(vm.service.getData).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
it('correctly updates issuable data', (done) => {
@@ -162,29 +165,22 @@ describe('Issuable output', () => {
resolve();
}));
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(
- vm.service.updateIssuable,
- ).toHaveBeenCalledWith(vm.formState);
- expect(
- eventHub.$emit,
- ).toHaveBeenCalledWith('close.form');
-
- done();
- });
+ vm.updateIssuable()
+ .then(() => {
+ expect(vm.service.updateIssuable).toHaveBeenCalledWith(vm.formState);
+ expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
+ })
+ .then(done)
+ .catch(done.fail);
});
it('does not redirect if issue has not moved', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
- json() {
- return {
- web_url: location.pathname,
- confidential: vm.isConfidential,
- };
+ data: {
+ web_url: location.pathname,
+ confidential: vm.isConfidential,
},
});
}));
@@ -193,7 +189,7 @@ describe('Issuable output', () => {
setTimeout(() => {
expect(
- gl.utils.visitUrl,
+ urlUtils.visitUrl,
).not.toHaveBeenCalled();
done();
@@ -201,14 +197,12 @@ describe('Issuable output', () => {
});
it('redirects if returned web_url has changed', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
- json() {
- return {
- web_url: '/testing-issue-move',
- confidential: vm.isConfidential,
- };
+ data: {
+ web_url: '/testing-issue-move',
+ confidential: vm.isConfidential,
},
});
}));
@@ -217,7 +211,7 @@ describe('Issuable output', () => {
setTimeout(() => {
expect(
- gl.utils.visitUrl,
+ urlUtils.visitUrl,
).toHaveBeenCalledWith('/testing-issue-move');
done();
@@ -268,13 +262,57 @@ describe('Issuable output', () => {
});
});
+ it('opens recaptcha modal if update rejected as spam', (done) => {
+ function mockScriptSrc() {
+ const recaptchaChild = vm.$children
+ .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle
+
+ recaptchaChild.scriptSrc = '//scriptsrc';
+ }
+
+ let modal;
+ const promise = new Promise((resolve) => {
+ resolve({
+ data: {
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ },
+ });
+ });
+
+ spyOn(vm.service, 'updateIssuable').and.returnValue(promise);
+
+ vm.canUpdate = true;
+ vm.showForm = true;
+
+ vm.$nextTick()
+ .then(() => mockScriptSrc())
+ .then(() => vm.updateIssuable())
+ .then(promise)
+ .then(() => setTimeoutPromise())
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-modal');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => modal.querySelector('.close').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
- json() {
- return { web_url: '/test' };
+ data: {
+ web_url: '/test',
},
});
}));
@@ -283,7 +321,7 @@ describe('Issuable output', () => {
setTimeout(() => {
expect(
- gl.utils.visitUrl,
+ urlUtils.visitUrl,
).toHaveBeenCalledWith('/test');
done();
@@ -291,12 +329,12 @@ describe('Issuable output', () => {
});
it('stops polling when deleting', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.poll, 'stop').and.callThrough();
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
- json() {
- return { web_url: '/test' };
+ data: {
+ web_url: '/test',
},
});
}));
@@ -335,22 +373,21 @@ describe('Issuable output', () => {
describe('open form', () => {
it('shows locked warning if form is open & data is different', (done) => {
- Vue.nextTick()
+ vm.$nextTick()
.then(() => {
vm.openForm();
- requestData = issueShowData.secondRequest;
vm.poll.makeRequest();
})
- .then(() => new Promise(resolve => setTimeout(resolve)))
+ // Wait for the request
+ .then(vm.$nextTick)
+ // Wait for the successCallback to update the store state
+ .then(vm.$nextTick)
+ // Wait for the new state to flow to the Vue components
+ .then(vm.$nextTick)
.then(() => {
- expect(
- vm.formState.lockedWarningVisible,
- ).toBeTruthy();
-
- expect(
- vm.$el.querySelector('.alert'),
- ).not.toBeNull();
+ expect(vm.formState.lockedWarningVisible).toEqual(true);
+ expect(vm.$el.querySelector('.alert')).not.toBeNull();
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 163e5cdd062..0da25bdca9c 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -51,6 +51,35 @@ describe('Description component', () => {
});
});
+ it('opens recaptcha dialog if update rejected as spam', (done) => {
+ let modal;
+ const recaptchaChild = vm.$children
+ .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle
+
+ recaptchaChild.scriptSrc = '//scriptsrc';
+
+ vm.taskListUpdateSuccess({
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-modal');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => modal.querySelector('.close').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
describe('TaskList', () => {
beforeEach(() => {
vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
@@ -86,6 +115,7 @@ describe('Description component', () => {
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
+ onSuccess: jasmine.any(Function),
});
done();
});
diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js
index eb3111412a7..74b3efb014b 100644
--- a/spec/javascripts/issue_show/mock_data.js
+++ b/spec/javascripts/issue_show/mock_data.js
@@ -19,14 +19,4 @@ export default {
updated_by_name: 'Other User',
updated_by_path: '/other_user',
},
- issueSpecRequest: {
- title: '<p>this is a title</p>',
- title_text: 'this is a title',
- description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>',
- description_text: '- [ ] Task List Item',
- task_status: '0 of 1 completed',
- updated_at: '2017-05-15T12:31:04.428Z',
- updated_by_name: 'Last User',
- updated_by_path: '/last_user',
- },
};
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 3636aac79a0..2cd2e63b15d 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -55,7 +55,7 @@ describe('Issue', function() {
}
function findElements(isIssueInitiallyOpen) {
- $boxClosed = $('div.status-box-closed');
+ $boxClosed = $('div.status-box-issue-closed');
expect($boxClosed).toExist();
expect($boxClosed).toHaveText('Closed');
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index 20c4caa865d..b740c9ed893 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -1,6 +1,6 @@
-import { bytesToKiB } from '~/lib/utils/number_utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility';
-import '~/lib/utils/url_utility';
import Job from '~/job';
import '~/breakpoints';
@@ -65,7 +65,7 @@ describe('Job', () => {
const deferred2 = $.Deferred();
const deferred3 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update<span>',
@@ -103,7 +103,7 @@ describe('Job', () => {
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update<span>',
@@ -134,7 +134,7 @@ describe('Job', () => {
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', () => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
@@ -153,7 +153,7 @@ describe('Job', () => {
it('shows the size in KiB', () => {
const size = 50;
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
@@ -169,7 +169,7 @@ describe('Job', () => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(size)}`);
+ ).toEqual(`${numberToHumanSize(size)}`);
});
it('shows incremented size', () => {
@@ -179,7 +179,7 @@ describe('Job', () => {
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update</span>',
@@ -195,7 +195,7 @@ describe('Job', () => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(50)}`);
+ ).toEqual(`${numberToHumanSize(50)}`);
jasmine.clock().tick(4001);
@@ -209,12 +209,12 @@ describe('Job', () => {
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(60)}`);
+ ).toEqual(`${numberToHumanSize(60)}`);
});
it('renders the raw link', () => {
const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
@@ -236,7 +236,7 @@ describe('Job', () => {
describe('when size is equal than total', () => {
it('does not show the trunctated information', () => {
const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
@@ -257,7 +257,7 @@ describe('Job', () => {
describe('output trace', () => {
beforeEach(() => {
const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index 4a210faa017..83395ea451e 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import headerComponent from '~/jobs/components/header.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('Job details header', () => {
let HeaderComponent;
@@ -35,7 +36,7 @@ describe('Job details header', () => {
isLoading: false,
};
- vm = new HeaderComponent({ propsData: props }).$mount();
+ vm = mountComponent(HeaderComponent, props);
});
afterEach(() => {
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index 1f46c225071..6f8dad6b835 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -62,4 +62,14 @@ describe('text_utility', () => {
expect(textUtils.slugify('João')).toEqual('joão');
});
});
+
+ describe('stripeHtml', () => {
+ it('replaces html tag with the default replacement', () => {
+ expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
+ });
+
+ it('replaces html tags with the provided replacement', () => {
+ expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/tick_formats_spec.js b/spec/javascripts/lib/utils/tick_formats_spec.js
new file mode 100644
index 00000000000..283989b4fc8
--- /dev/null
+++ b/spec/javascripts/lib/utils/tick_formats_spec.js
@@ -0,0 +1,40 @@
+import { dateTickFormat, initDateFormats } from '~/lib/utils/tick_formats';
+
+import { setLanguage } from '../../helpers/locale_helper';
+
+describe('tick formats', () => {
+ describe('dateTickFormat', () => {
+ beforeAll(() => {
+ setLanguage('de');
+ initDateFormats();
+ });
+
+ afterAll(() => {
+ setLanguage(null);
+ });
+
+ it('returns year for first of January', () => {
+ const tick = dateTickFormat(new Date('2001-01-01'));
+
+ expect(tick).toBe('2001');
+ });
+
+ it('returns month for first of February', () => {
+ const tick = dateTickFormat(new Date('2001-02-01'));
+
+ expect(tick).toBe('Februar');
+ });
+
+ it('returns day and month for second of February', () => {
+ const tick = dateTickFormat(new Date('2001-02-02'));
+
+ expect(tick).toBe('2. Feb.');
+ });
+
+ it('ignores time', () => {
+ const tick = dateTickFormat(new Date('2001-02-02 12:34:56'));
+
+ expect(tick).toBe('2. Feb.');
+ });
+ });
+});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index 645664a5219..89f4b85541d 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,7 +1,6 @@
/* 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 */
-/* global LineHighlighter */
-import '~/line_highlighter';
+import LineHighlighter from '~/line_highlighter';
(function() {
describe('LineHighlighter', function() {
diff --git a/spec/javascripts/locale/index_spec.js b/spec/javascripts/locale/index_spec.js
new file mode 100644
index 00000000000..29b0b21eed7
--- /dev/null
+++ b/spec/javascripts/locale/index_spec.js
@@ -0,0 +1,35 @@
+import { createDateTimeFormat, languageCode } from '~/locale';
+
+import { setLanguage } from '../helpers/locale_helper';
+
+describe('locale', () => {
+ afterEach(() => {
+ setLanguage(null);
+ });
+
+ describe('languageCode', () => {
+ it('parses the lang attribute', () => {
+ setLanguage('ja');
+
+ expect(languageCode()).toBe('ja');
+ });
+
+ it('falls back to English', () => {
+ setLanguage(null);
+
+ expect(languageCode()).toBe('en');
+ });
+ });
+
+ describe('createDateTimeFormat', () => {
+ beforeEach(() => {
+ setLanguage('de');
+ });
+
+ it('creates an instance of Intl.DateTimeFormat', () => {
+ const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
+
+ expect(dateFormat.format(new Date(2015, 6, 3))).toBe('3. Juli 2015');
+ });
+ });
+});
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
index 6054b75d0b8..e983e4de3fc 100644
--- a/spec/javascripts/merge_request_notes_spec.js
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -1,11 +1,9 @@
-/* global Notes */
-
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
import '~/render_math';
-import '~/notes';
+import Notes from '~/notes';
const upArrowKeyCode = 38;
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 70ae63ba036..2f02c11482f 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable space-before-function-paren, no-return-assign */
-/* global MergeRequest */
-import '~/merge_request';
+import MergeRequest from '~/merge_request';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import IssuablesHelper from '~/helpers/issuables_helper';
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index e441d1153ed..a6be474805b 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,12 +1,12 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
-/* global Notes */
-import '~/merge_request_tabs';
+import * as urlUtils from '~/lib/utils/url_utility';
+import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
import '~/lib/utils/common_utils';
import Diff from '~/diff';
-import '~/notes';
+import Notes from '~/notes';
import 'vendor/jquery.scrollTo';
(function () {
@@ -31,7 +31,7 @@ import 'vendor/jquery.scrollTo';
);
beforeEach(function () {
- this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
+ this.class = new MergeRequestTabs({ stubLocation: stubLocation });
setLocation();
this.spies = {
@@ -278,8 +278,8 @@ import 'vendor/jquery.scrollTo';
loadFixtures('merge_requests/diff_comment.html.raw');
$('body').attr('data-page', 'projects:merge_requests:show');
window.gl.ImageFile = () => {};
- window.notes = new Notes('', []);
- spyOn(window.notes, 'toggleDiffNote').and.callThrough();
+ Notes.initialize('', []);
+ spyOn(Notes.instance, 'toggleDiffNote').and.callThrough();
});
afterEach(() => {
@@ -290,15 +290,18 @@ import 'vendor/jquery.scrollTo';
$('body').removeAttr('data-page');
});
- it('requires an absolute pathname', function () {
- spyOn($, 'ajax').and.callFake(function (options) {
- expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json');
+ it('triggers Ajax request to JSON endpoint', function (done) {
+ const url = '/foo/bar/merge_requests/1/diffs';
+ spyOn(this.class, 'ajaxGet').and.callFake((options) => {
+ expect(options.url).toEqual(`${url}.json`);
+ done();
});
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ this.class.loadDiff(url);
});
- it('triggers scroll event when diff already loaded', function () {
+ it('triggers scroll event when diff already loaded', function (done) {
+ spyOn(this.class, 'ajaxGet').and.callFake(() => done.fail());
spyOn(document, 'dispatchEvent');
this.class.diffsLoaded = true;
@@ -307,6 +310,7 @@ import 'vendor/jquery.scrollTo';
expect(
document.dispatchEvent,
).toHaveBeenCalledWith(new CustomEvent('scroll'));
+ done();
});
describe('with inline diff', () => {
@@ -333,11 +337,11 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteId.length).toBeGreaterThan(0);
- expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({
+ expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
target: jasmine.any(Object),
lineType: 'old',
forceShow: true,
@@ -345,20 +349,20 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
});
});
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
- expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
});
});
});
@@ -387,12 +391,12 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteId.length).toBeGreaterThan(0);
- expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({
+ expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
target: jasmine.any(Object),
lineType: 'new',
forceShow: true,
@@ -400,20 +404,20 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
});
});
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
- expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js
index dea42d755d4..d07db871d69 100644
--- a/spec/javascripts/monitoring/graph/deployment_spec.js
+++ b/spec/javascripts/monitoring/graph/deployment_spec.js
@@ -11,145 +11,38 @@ const createComponent = (propsData) => {
};
describe('MonitoringDeployment', () => {
- const reducedDeploymentData = [deploymentData[0]];
- reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name;
- reducedDeploymentData[0].xPos = 10;
- reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at);
describe('Methods', () => {
- it('refText shows the ref when a tag is available', () => {
- reducedDeploymentData[0].tag = '1.0';
- const component = createComponent({
- showDeployInfo: false,
- deploymentData: reducedDeploymentData,
- graphWidth: 440,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.refText(reducedDeploymentData[0]),
- ).toEqual(reducedDeploymentData[0].ref);
- });
-
- it('refText shows the sha when no tag is available', () => {
- reducedDeploymentData[0].tag = null;
- const component = createComponent({
- showDeployInfo: false,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphWidth: 440,
- graphHeightOffset: 120,
- });
-
- expect(
- component.refText(reducedDeploymentData[0]),
- ).toContain('f5bcd1');
- });
-
- it('nameDeploymentClass creates a class with the prefix deploy-info-', () => {
+ it('should contain a hidden gradient', () => {
const component = createComponent({
- showDeployInfo: false,
- deploymentData: reducedDeploymentData,
+ showDeployInfo: true,
+ deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
- expect(
- component.nameDeploymentClass(reducedDeploymentData[0]),
- ).toContain('deploy-info');
+ expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
});
it('transformDeploymentGroup translates an available deployment', () => {
const component = createComponent({
showDeployInfo: false,
- deploymentData: reducedDeploymentData,
+ deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
- component.transformDeploymentGroup(reducedDeploymentData[0]),
+ component.transformDeploymentGroup({ xPos: 16 }),
).toContain('translate(11, 20)');
});
- it('hides the deployment flag', () => {
- reducedDeploymentData[0].showDeploymentFlag = false;
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphWidth: 440,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
- });
-
- it('positions the flag to the left when the xPos is too far right', () => {
- reducedDeploymentData[0].showDeploymentFlag = false;
- reducedDeploymentData[0].xPos = 250;
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphWidth: 440,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.positionFlag(reducedDeploymentData[0]),
- ).toBeLessThan(0);
- });
-
- it('shows the deployment flag', () => {
- reducedDeploymentData[0].showDeploymentFlag = true;
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphWidth: 440,
- graphHeightOffset: 120,
- });
-
- expect(
- component.$el.querySelector('.js-deploy-info-box').style.display,
- ).not.toEqual('display: none;');
- });
-
- it('shows the refText inside a text element with the deploy-info-text class', () => {
- reducedDeploymentData[0].showDeploymentFlag = true;
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphWidth: 440,
- graphHeightOffset: 120,
- });
-
- expect(
- component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(),
- ).toEqual(component.refText(reducedDeploymentData[0]));
- });
-
- it('should contain a hidden gradient', () => {
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphWidth: 440,
- graphHeightOffset: 120,
- });
-
- expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
- });
-
describe('Computed props', () => {
it('calculatedHeight', () => {
const component = createComponent({
showDeployInfo: true,
- deploymentData: reducedDeploymentData,
+ deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index 8ee1171419d..2d474e9092f 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import GraphFlag from '~/monitoring/components/graph/flag.vue';
+import { deploymentData } from '../mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(GraphFlag);
@@ -9,11 +10,6 @@ const createComponent = (propsData) => {
}).$mount();
};
-function getCoordinate(component, selector, coordinate) {
- const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate);
- return parseInt(coordinateVal, 10);
-}
-
const defaultValuesComponent = {
currentXCoordinate: 200,
currentYCoordinate: 100,
@@ -25,31 +21,111 @@ const defaultValuesComponent = {
graphHeight: 300,
graphHeightOffset: 120,
showFlagContent: true,
+ realPixelRatio: 1,
+ timeSeries: [{
+ values: [{
+ time: new Date('2017-06-04T18:17:33.501Z'),
+ value: '1.49609375',
+ }],
+ }],
+ unitOfDisplay: 'ms',
+ currentDataIndex: 0,
+ legendTitle: 'Average',
+};
+
+const deploymentFlagData = {
+ ...deploymentData[0],
+ ref: deploymentData[0].ref.name,
+ xPos: 10,
+ time: new Date(deploymentData[0].created_at),
};
describe('GraphFlag', () => {
- it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
- const component = createComponent(defaultValuesComponent);
+ let component;
- expect(getCoordinate(component, '.selected-metric-line', 'x1'))
- .toEqual(component.currentXCoordinate);
- expect(getCoordinate(component, '.selected-metric-line', 'x2'))
- .toEqual(component.currentXCoordinate);
+ it('has a line at the currentXCoordinate', () => {
+ component = createComponent(defaultValuesComponent);
+
+ expect(component.$el.style.left)
+ .toEqual(`${70 + component.currentXCoordinate}px`);
});
- it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
- const component = createComponent(defaultValuesComponent);
+ describe('Deployment flag', () => {
+ it('shows a deployment flag when deployment data provided', () => {
+ const deploymentFlagComponent = createComponent({
+ ...defaultValuesComponent,
+ deploymentFlagData,
+ });
+
+ expect(
+ deploymentFlagComponent.$el.querySelector('.popover-title'),
+ ).toContainText('Deployed');
+ });
+
+ it('contains the ref when a tag is available', () => {
+ const deploymentFlagComponent = createComponent({
+ ...defaultValuesComponent,
+ deploymentFlagData: {
+ ...deploymentFlagData,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ tag: true,
+ ref: '1.0',
+ },
+ });
+
+ expect(
+ deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
+ ).toContainText('f5bcd1d9');
+
+ expect(
+ deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
+ ).toContainText('1.0');
+ });
+
+ it('does not contain the ref when a tag is unavailable', () => {
+ const deploymentFlagComponent = createComponent({
+ ...defaultValuesComponent,
+ deploymentFlagData: {
+ ...deploymentFlagData,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ tag: false,
+ ref: '1.0',
+ },
+ });
+
+ expect(
+ deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
+ ).toContainText('f5bcd1d9');
- const svg = component.$el.querySelector('.rect-text-metric');
- expect(svg.tagName).toEqual('svg');
- expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition);
+ expect(
+ deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
+ ).not.toContainText('1.0');
+ });
});
describe('Computed props', () => {
- it('calculatedHeight', () => {
- const component = createComponent(defaultValuesComponent);
+ beforeEach(() => {
+ component = createComponent(defaultValuesComponent);
+ });
+
+ it('formatTime', () => {
+ expect(component.formatTime).toMatch(/\d:17PM/);
+ });
+
+ it('formatDate', () => {
+ expect(component.formatDate).toEqual('Sun, Jun 4');
+ });
+
+ it('cursorStyle', () => {
+ expect(component.cursorStyle).toEqual({
+ top: '20px',
+ left: '270px',
+ height: '180px',
+ });
+ });
- expect(component.calculatedHeight).toEqual(180);
+ it('flagOrientation', () => {
+ expect(component.flagOrientation).toEqual('left');
});
});
});
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index fd79abe241a..b1d69752bad 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -4,6 +4,8 @@ import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import eventHub from '~/monitoring/event_hub';
import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
+const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
+const projectPath = 'http://test.host/frontend-fixtures/environments-project';
const createComponent = (propsData) => {
const Component = Vue.extend(Graph);
@@ -25,6 +27,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title);
@@ -37,6 +41,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
const transformedHeight = `${component.graphHeight - 100}`;
@@ -50,6 +56,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
const viewBoxArray = component.outerViewBox.split(' ');
@@ -65,6 +73,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
spyOn(eventHub, '$emit');
@@ -81,6 +91,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
expect(component.yAxisLabel).toEqual(component.graphData.y_label);
@@ -98,6 +110,8 @@ describe('Graph', () => {
hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'),
currentDeployXPos: null,
},
+ tagsPath,
+ projectPath,
});
component.positionFlag();
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 6b34855b8b2..2bbe963e393 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,6 +1,8 @@
/* eslint-disable quote-props, indent, comma-dangle */
-const metricsGroupsAPIResponse = {
+export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
+
+export const metricsGroupsAPIResponse = {
'success': true,
'data': [
{
@@ -2430,33 +2432,39 @@ export const deploymentData = [
id: 111,
iid: 3,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master'
},
created_at: '2017-05-31T21:23:37.881Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': true
},
{
id: 110,
iid: 2,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master'
},
created_at: '2017-05-30T20:08:04.629Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false
},
{
id: 109,
iid: 1,
sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
ref: {
name: 'update2-readme'
},
created_at: '2017-05-30T17:42:38.409Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false
}
];
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js
index 04a7f8e32f1..20e352dd8bd 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/comment_form_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import Autosize from 'autosize';
import store from '~/notes/stores';
-import issueCommentForm from '~/notes/components/issue_comment_form.vue';
+import issueCommentForm from '~/notes/components/comment_form.vue';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 8e43037f356..7c8d6685ee1 100644
--- a/spec/javascripts/notes/components/issue_note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -1,26 +1,15 @@
import Vue from 'vue';
-import issueNotesApp from '~/notes/components/issue_notes_app.vue';
+import notesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
import * as mockData from '../mock_data';
+import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-describe('issue_note_app', () => {
+describe('note_app', () => {
let mountComponent;
let vm;
- const individualNoteInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), {
- status: 200,
- }));
- };
-
- const discussionNoteInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- const IssueNotesApp = Vue.extend(issueNotesApp);
+ const IssueNotesApp = Vue.extend(notesApp);
mountComponent = (data) => {
const props = data || {
@@ -74,16 +63,16 @@ describe('issue_note_app', () => {
describe('render', () => {
beforeEach(() => {
- Vue.http.interceptors.push(individualNoteInterceptor);
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
it('should render list of notes', (done) => {
- const note = mockData.individualNoteServerResponse[0].notes[0];
+ const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0];
setTimeout(() => {
expect(
@@ -129,13 +118,16 @@ describe('issue_note_app', () => {
describe('update note', () => {
describe('individual note', () => {
beforeEach(() => {
- Vue.http.interceptors.push(individualNoteInterceptor);
- spyOn(service, 'updateNote').and.callFake(() => Promise.resolve());
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
+ spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors,
+ mockData.individualNoteInterceptor,
+ );
});
it('renders edit form', (done) => {
@@ -149,28 +141,36 @@ describe('issue_note_app', () => {
});
it('calls the service to update the note', (done) => {
- setTimeout(() => {
- vm.$el.querySelector('.js-note-edit').click();
- Vue.nextTick(() => {
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
expect(service.updateNote).toHaveBeenCalled();
- done();
- });
- }, 0);
+ })
+ // Wait for the requests to finish before destroying
+ .then(Vue.nextTick)
+ .then(done)
+ .catch(done.fail);
});
});
describe('dicussion note', () => {
beforeEach(() => {
- Vue.http.interceptors.push(discussionNoteInterceptor);
- spyOn(service, 'updateNote').and.callFake(() => Promise.resolve());
+ Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
+ spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor);
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors,
+ mockData.discussionNoteInterceptor,
+ );
});
it('renders edit form', (done) => {
@@ -184,16 +184,21 @@ describe('issue_note_app', () => {
});
it('updates the note and resets the edit form', (done) => {
- setTimeout(() => {
- vm.$el.querySelector('.js-note-edit').click();
- Vue.nextTick(() => {
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
expect(service.updateNote).toHaveBeenCalled();
- done();
- });
- }, 0);
+ })
+ // Wait for the requests to finish before destroying
+ .then(Vue.nextTick)
+ .then(done)
+ .catch(done.fail);
});
});
});
@@ -216,12 +221,12 @@ describe('issue_note_app', () => {
describe('edit form', () => {
beforeEach(() => {
- Vue.http.interceptors.push(individualNoteInterceptor);
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
it('should render markdown docs url', (done) => {
diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js
index 37aad50737b..b42e7943b98 100644
--- a/spec/javascripts/notes/components/issue_note_body_spec.js
+++ b/spec/javascripts/notes/components/note_body_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import noteBody from '~/notes/components/issue_note_body.vue';
+import noteBody from '~/notes/components/note_body.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note_body component', () => {
diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js
index d42ef239711..86e9e2a32a9 100644
--- a/spec/javascripts/notes/components/issue_note_form_spec.js
+++ b/spec/javascripts/notes/components/note_form_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import issueNoteForm from '~/notes/components/issue_note_form.vue';
+import issueNoteForm from '~/notes/components/note_form.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index b6ae55d44f5..19504e4f7c8 100644
--- a/spec/javascripts/notes/components/issue_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import issueDiscussion from '~/notes/components/issue_discussion.vue';
+import issueDiscussion from '~/notes/components/noteable_discussion.vue';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
describe('issue_discussion component', () => {
@@ -30,7 +30,7 @@ describe('issue_discussion component', () => {
it('should render discussion header', () => {
expect(vm.$el.querySelector('.discussion-header')).toBeDefined();
- expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length);
+ expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length);
});
describe('actions', () => {
diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js
index 73fd188dbe5..c8a6cb7e612 100644
--- a/spec/javascripts/notes/components/issue_note_spec.js
+++ b/spec/javascripts/notes/components/noteable_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import issueNote from '~/notes/components/issue_note.vue';
+import issueNote from '~/notes/components/noteable_note.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note', () => {
@@ -41,4 +41,19 @@ describe('issue_note', () => {
it('should render issue body', () => {
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
});
+
+ 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');
+ vm.updateNote = () => new Promise($.noop);
+
+ vm.formUpdateHandler(noteBody, null, $.noop);
+
+ setTimeout(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(vm.note.note_html).toEqual(_.escape(noteBody));
+ done();
+ }, 0);
+ });
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 42497de3c55..6b608adff15 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -312,138 +312,212 @@ export const loggedOutnoteableData = {
"preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue"
}
-export const individualNoteServerResponse = [{
- "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "expanded": true,
- "notes": [{
- "id": 1390,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-01T17:09:33.762Z",
- "updated_at": "2017-08-01T17:09:33.762Z",
- "system": false,
- "noteable_id": 98,
- "noteable_type": "Issue",
- "type": null,
- "human_access": "Owner",
- "note": "sdfdsaf",
- "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e",
- "current_user": {
- "can_edit": true
+export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
+ 'GET': {
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
+ "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "expanded": true,
+ "notes": [{
+ "id": 1390,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-01T17:09:33.762Z",
+ "updated_at": "2017-08-01T17:09:33.762Z",
+ "system": false,
+ "noteable_id": 98,
+ "noteable_type": "Issue",
+ "type": null,
+ "human_access": "Owner",
+ "note": "sdfdsaf",
+ "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "emoji_awardable": true,
+ "award_emoji": [{
+ "name": "baseball",
+ "user": {
+ "id": 1,
+ "name": "Root",
+ "username": "root"
+ }
+ }, {
+ "name": "art",
+ "user": {
+ "id": 1,
+ "name": "Root",
+ "username": "root"
+ }
+ }],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1390"
+ }],
+ "individual_note": true
+ }, {
+ "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "expanded": true,
+ "notes": [{
+ "id": 1391,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-02T10:51:38.685Z",
+ "updated_at": "2017-08-02T10:51:38.685Z",
+ "system": false,
+ "noteable_id": 98,
+ "noteable_type": "Issue",
+ "type": null,
+ "human_access": "Owner",
+ "note": "New note!",
+ "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1391"
+ }],
+ "individual_note": true
+ }],
+ '/gitlab-org/gitlab-ce/noteable/issue/98/notes': {
+ last_fetched_at: 1512900838,
+ notes: [],
},
- "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "emoji_awardable": true,
- "award_emoji": [{
- "name": "baseball",
- "user": {
+ },
+ 'PUT': {
+ '/gitlab-org/gitlab-ce/notes/1471': {
+ "commands_changes": null,
+ "valid": true,
+ "id": 1471,
+ "attachment": null,
+ "author": {
"id": 1,
"name": "Root",
- "username": "root"
- }
- }, {
- "name": "art",
- "user": {
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-08T16:53:00.666Z",
+ "updated_at": "2017-12-10T11:03:21.876Z",
+ "system": false,
+ "noteable_id": 124,
+ "noteable_type": "Issue",
+ "noteable_iid": 29,
+ "type": "DiscussionNote",
+ "human_access": "Owner",
+ "note": "Adding a comment",
+ "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
+ "last_edited_at": "2017-12-10T11:03:21.876Z",
+ "last_edited_by": {
"id": 1,
- "name": "Root",
- "username": "root"
- }
- }],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1390"
- }],
- "individual_note": true
- }, {
- "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "expanded": true,
- "notes": [{
- "id": 1391,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-02T10:51:38.685Z",
- "updated_at": "2017-08-02T10:51:38.685Z",
- "system": false,
- "noteable_id": 98,
- "noteable_type": "Issue",
- "type": null,
- "human_access": "Owner",
- "note": "New note!",
- "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e",
- "current_user": {
- "can_edit": true
+ "name": 'Root',
+ "username": 'root',
+ "state": 'active',
+ "avatar_url": null,
+ "path": '/root',
+ },
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1471"
},
- "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "emoji_awardable": true,
- "award_emoji": [],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1391"
- }],
- "individual_note": true
-}];
+ }
+};
-export const discussionNoteServerResponse = [{
- "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "expanded": true,
- "notes": [{
- "id": 1471,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-08T16:53:00.666Z",
- "updated_at": "2017-08-08T16:53:00.666Z",
- "system": false,
- "noteable_id": 124,
- "noteable_type": "Issue",
- "noteable_iid": 29,
- "type": "DiscussionNote",
- "human_access": "Owner",
- "note": "Adding a comment",
- "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
- "current_user": {
- "can_edit": true
- },
- "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "emoji_awardable": true,
- "award_emoji": [],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1471"
- }],
- "individual_note": false
-}];
+export const DISCUSSION_NOTE_RESPONSE_MAP = {
+ ...INDIVIDUAL_NOTE_RESPONSE_MAP,
+ 'GET': {
+ ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET,
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
+ "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "expanded": true,
+ "notes": [{
+ "id": 1471,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-08T16:53:00.666Z",
+ "updated_at": "2017-08-08T16:53:00.666Z",
+ "system": false,
+ "noteable_id": 124,
+ "noteable_type": "Issue",
+ "noteable_iid": 29,
+ "type": "DiscussionNote",
+ "human_access": "Owner",
+ "note": "Adding a comment",
+ "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1471"
+ }],
+ "individual_note": false
+ }],
+ },
+};
+
+export function individualNoteInterceptor(request, next) {
+ const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }));
+}
+
+export function discussionNoteInterceptor(request, next) {
+ const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }));
+}
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 677a389b88f..167f074fb9b 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,11 +1,10 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
-/* global Notes */
-
+import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
-import '~/notes';
+import Notes from '~/notes';
(function() {
window.gon || (window.gon = {});
@@ -168,8 +167,7 @@ import '~/notes';
});
it('sets target when hash matches', () => {
- spyOn(gl.utils, 'getLocationHash');
- gl.utils.getLocationHash.and.returnValue(hash);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(hash);
Notes.updateNoteTargetSelector($note);
@@ -178,8 +176,7 @@ import '~/notes';
});
it('unsets target when hash does not match', () => {
- spyOn(gl.utils, 'getLocationHash');
- gl.utils.getLocationHash.and.returnValue('note_doesnotexist');
+ spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist');
Notes.updateNoteTargetSelector($note);
@@ -187,8 +184,7 @@ import '~/notes';
});
it('unsets target when there is not a hash fragment anymore', () => {
- spyOn(gl.utils, 'getLocationHash');
- gl.utils.getLocationHash.and.returnValue(null);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(null);
Notes.updateNoteTargetSelector($note);
@@ -224,7 +220,6 @@ import '~/notes';
notes.note_ids = [];
notes.updatedNotesTrackingMap = {};
- spyOn(gl.utils, 'localTimeAgo');
spyOn(Notes, 'isNewNote').and.callThrough();
spyOn(Notes, 'isUpdatedNote').and.callThrough();
spyOn(Notes, 'animateAppendNote').and.callThrough();
@@ -351,7 +346,6 @@ import '~/notes';
]);
notes.note_ids = [];
- spyOn(gl.utils, 'localTimeAgo');
spyOn(Notes, 'isNewNote');
spyOn(Notes, 'animateAppendNote');
Notes.isNewNote.and.returnValue(true);
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index 1d3e1263371..2fd87754238 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -1,14 +1,9 @@
/* global fixture */
-import '~/pager';
+import * as utils from '~/lib/utils/url_utility';
+import Pager from '~/pager';
describe('pager', () => {
- const Pager = window.Pager;
-
- it('is defined on window', () => {
- expect(window.Pager).toBeDefined();
- });
-
describe('init', () => {
const originalHref = window.location.href;
@@ -30,7 +25,7 @@ describe('pager', () => {
it('should use current url if data-href attribute not provided', () => {
const href = `${gl.TEST_HOST}/some_list`;
- spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ spyOn(utils, 'removeParams').and.returnValue(href);
Pager.init();
expect(Pager.url).toBe(href);
});
@@ -44,9 +39,9 @@ describe('pager', () => {
it('keeps extra query parameters from url', () => {
window.history.replaceState({}, null, '?filter=test&offset=100');
const href = `${gl.TEST_HOST}/some_list?filter=test`;
- spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ spyOn(utils, 'removeParams').and.returnValue(href);
Pager.init();
- expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+ expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
expect(Pager.url).toEqual(href);
});
});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index 23c87610d83..35e36e9c353 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -113,4 +113,35 @@ describe('pipeline graph job component', () => {
component.$el.querySelector('a').classList.contains('css-class-job-name'),
).toBe(true);
});
+
+ describe('status label', () => {
+ it('should not render status label when it is not provided', () => {
+ component = mountComponent(JobComponent, {
+ job: {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ },
+ },
+ });
+
+ expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test');
+ });
+
+ it('should not render status label when it is provided', () => {
+ component = mountComponent(JobComponent, {
+ job: {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ label: 'success',
+ },
+ },
+ });
+
+ expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success');
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js
index f1697840fcd..09a0c14d96c 100644
--- a/spec/javascripts/pipelines/nav_controls_spec.js
+++ b/spec/javascripts/pipelines/nav_controls_spec.js
@@ -14,6 +14,7 @@ describe('Pipelines Nav Controls', () => {
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
+ resetCachePath: 'foo',
canCreatePipeline: true,
};
@@ -31,6 +32,7 @@ describe('Pipelines Nav Controls', () => {
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
+ resetCachePath: 'foo',
canCreatePipeline: false,
};
@@ -41,12 +43,31 @@ describe('Pipelines Nav Controls', () => {
expect(component.$el.querySelector('.btn-create')).toEqual(null);
});
+ it('should render link for resetting runner caches', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ resetCachePath: 'foo',
+ canCreatePipeline: false,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches');
+ expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath);
+ });
+
it('should render link for CI lint', () => {
const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
+ resetCachePath: 'foo',
canCreatePipeline: true,
};
@@ -54,8 +75,8 @@ describe('Pipelines Nav Controls', () => {
propsData: mockData,
}).$mount();
- expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint');
- expect(component.$el.querySelector('.btn-default').getAttribute('href')).toEqual(mockData.ciLintPath);
+ expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint');
+ expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath);
});
it('should render link to help page when CI is not enabled', () => {
@@ -64,6 +85,7 @@ describe('Pipelines Nav Controls', () => {
hasCiEnabled: false,
helpPagePath: 'foo',
ciLintPath: 'foo',
+ resetCachePath: 'foo',
canCreatePipeline: true,
};
@@ -81,6 +103,7 @@ describe('Pipelines Nav Controls', () => {
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
+ resetCachePath: 'foo',
canCreatePipeline: true,
};
diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
index 2e94948cfb2..588b61196a5 100644
--- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js
+++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
@@ -51,7 +51,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
- expect(submitButton).toHaveClass('disabled');
+ expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(form.submit).not.toHaveBeenCalled();
})
@@ -68,7 +68,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
- expect(submitButton).not.toHaveClass('disabled');
+ expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(form.submit).toHaveBeenCalled();
})
@@ -101,7 +101,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
- expect(submitButton).toHaveClass('disabled');
+ expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(form.submit).not.toHaveBeenCalled();
})
@@ -118,7 +118,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
- expect(submitButton).not.toHaveClass('disabled');
+ expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(form.submit).toHaveBeenCalled();
})
diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js
index 850768f0e4f..c314ca8ab72 100644
--- a/spec/javascripts/projects/project_new_spec.js
+++ b/spec/javascripts/projects/project_new_spec.js
@@ -6,8 +6,12 @@ describe('New Project', () => {
beforeEach(() => {
setFixtures(`
- <input id="project_import_url" />
- <input id="project_path" />
+ <div class='toggle-import-form'>
+ <div class='import-url-data'>
+ <input id="project_import_url" />
+ <input id="project_path" />
+ </div>
+ </div>
`);
$projectImportUrl = $('#project_import_url');
@@ -25,7 +29,7 @@ describe('New Project', () => {
it('does not change project path for disabled $projectImportUrl', () => {
$projectImportUrl.attr('disabled', true);
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual(dummyImportUrl);
});
@@ -38,7 +42,7 @@ describe('New Project', () => {
it('does not change project path if it is set by user', () => {
$projectPath.keyup();
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual(dummyImportUrl);
});
@@ -46,7 +50,7 @@ describe('New Project', () => {
it('does not change project path for empty $projectImportUrl', () => {
$projectImportUrl.val('');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual(dummyImportUrl);
});
@@ -54,7 +58,7 @@ describe('New Project', () => {
it('does not change project path for whitespace $projectImportUrl', () => {
$projectImportUrl.val(' ');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual(dummyImportUrl);
});
@@ -62,7 +66,7 @@ describe('New Project', () => {
it('does not change project path for $projectImportUrl without slashes', () => {
$projectImportUrl.val('has-no-slash');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual(dummyImportUrl);
});
@@ -70,7 +74,7 @@ describe('New Project', () => {
it('changes project path to last $projectImportUrl component', () => {
$projectImportUrl.val('/this/is/last');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual('last');
});
@@ -78,7 +82,7 @@ describe('New Project', () => {
it('ignores trailing slashes in $projectImportUrl', () => {
$projectImportUrl.val('/has/trailing/slash/');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual('slash');
});
@@ -86,7 +90,7 @@ describe('New Project', () => {
it('ignores fragment identifier in $projectImportUrl', () => {
$projectImportUrl.val('/this/has/a#fragment-identifier/');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual('a');
});
@@ -94,7 +98,7 @@ describe('New Project', () => {
it('ignores query string in $projectImportUrl', () => {
$projectImportUrl.val('/url/with?query=string');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual('with');
});
@@ -102,7 +106,7 @@ describe('New Project', () => {
it('ignores trailing .git in $projectImportUrl', () => {
$projectImportUrl.val('/repository.git');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual('repository');
});
@@ -110,7 +114,7 @@ describe('New Project', () => {
it('changes project path for HTTPS URL in $projectImportUrl', () => {
$projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual('project');
});
@@ -118,7 +122,7 @@ describe('New Project', () => {
it('changes project path for SSH URL in $projectImportUrl', () => {
$projectImportUrl.val('git@gitlab.com:gitlab-org/gitlab-ce.git');
- projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath);
+ projectNew.deriveProjectPathFromUrl($projectImportUrl);
expect($projectPath.val()).toEqual('gitlab-ce');
});
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
index f750061a6a1..c4d3866c922 100644
--- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import listCollapsed from '~/repo/components/commit_sidebar/list_collapsed.vue';
+import store from '~/ide/stores';
+import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers';
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
index 18c9b46fcd9..fc7c9ae9dd7 100644
--- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import listItem from '~/repo/components/commit_sidebar/list_item.vue';
+import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers';
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
index df7e3c5de21..cb5240ad118 100644
--- a/spec/javascripts/repo/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import commitSidebarList from '~/repo/components/commit_sidebar/list.vue';
+import store from '~/ide/stores';
+import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers';
@@ -13,8 +13,11 @@ describe('Multi-file editor commit sidebar list', () => {
vm = createComponentWithStore(Component, store, {
title: 'Staged',
fileList: [],
- collapsed: false,
- }).$mount();
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
});
afterEach(() => {
@@ -43,30 +46,14 @@ describe('Multi-file editor commit sidebar list', () => {
describe('collapsed', () => {
beforeEach((done) => {
- vm.collapsed = true;
+ vm.$store.state.rightPanelCollapsed = true;
Vue.nextTick(done);
});
- it('adds collapsed class', () => {
- expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
- });
-
it('hides list', () => {
expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
expect(vm.$el.querySelector('.help-block')).toBeNull();
});
-
- it('hides collapse button', () => {
- expect(vm.$el.querySelector('.multi-file-commit-panel-collapse-btn')).toBeNull();
- });
- });
-
- it('clicking toggle collapse button emits toggle event', () => {
- spyOn(vm, '$emit');
-
- vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed');
});
});
diff --git a/spec/javascripts/repo/components/ide_context_bar_spec.js b/spec/javascripts/repo/components/ide_context_bar_spec.js
new file mode 100644
index 00000000000..3f8f37d2343
--- /dev/null
+++ b/spec/javascripts/repo/components/ide_context_bar_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideContextBar from '~/ide/components/ide_context_bar.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+
+describe('Multi-file editor right context bar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideContextBar);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed', () => {
+ beforeEach((done) => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+ });
+
+ it('shows correct icon', () => {
+ expect(vm.currentIcon).toBe('angle-double-left');
+ });
+ });
+
+ it('clicking toggle collapse button collapses the bar', () => {
+ spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve());
+
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.setPanelCollapsedStatus).toHaveBeenCalledWith({
+ side: 'right',
+ collapsed: true,
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/ide_repo_tree_spec.js
index df7cf8aabbb..e3bbda514da 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/ide_repo_tree_spec.js
@@ -1,20 +1,26 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoSidebar from '~/repo/components/repo_sidebar.vue';
+import store from '~/ide/stores';
+import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
import { file, resetStore } from '../helpers';
-describe('RepoSidebar', () => {
+describe('IdeRepoTree', () => {
let vm;
beforeEach(() => {
- const RepoSidebar = Vue.extend(repoSidebar);
+ const IdeRepoTree = Vue.extend(ideRepoTree);
- vm = new RepoSidebar({
+ vm = new IdeRepoTree({
store,
+ propsData: {
+ treeId: 'abcproject/mybranch',
+ },
});
+ vm.$store.state.currentBranch = 'master';
vm.$store.state.isRoot = true;
- vm.$store.state.tree.push(file());
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [file()],
+ };
vm.$mount();
});
@@ -26,25 +32,20 @@ describe('RepoSidebar', () => {
});
it('renders a sidebar', () => {
- const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
- expect(thead.querySelector('.name').textContent.trim()).toEqual('Name');
- expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit');
- expect(thead.querySelector('.last-update').textContent.trim()).toEqual('Last update');
expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy();
expect(tbody.querySelector('.file')).toBeTruthy();
});
- it('renders 5 loading files if tree is loading', (done) => {
- vm.$store.state.tree = [];
- vm.$store.state.loading = true;
+ it('renders 3 loading files if tree is loading', (done) => {
+ vm.treeId = '123';
Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3);
done();
});
diff --git a/spec/javascripts/repo/components/ide_side_bar_spec.js b/spec/javascripts/repo/components/ide_side_bar_spec.js
new file mode 100644
index 00000000000..30e45169205
--- /dev/null
+++ b/spec/javascripts/repo/components/ide_side_bar_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { resetStore } from '../helpers';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+
+describe('IdeSidebar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideSidebar);
+
+ vm = createComponentWithStore(Component, store).$mount();
+
+ vm.$store.state.leftPanelCollapsed = false;
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a sidebar', () => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
+ });
+
+ describe('collapsed', () => {
+ beforeEach((done) => {
+ vm.$store.state.leftPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.classList).toContain('is-collapsed');
+ });
+
+ it('shows correct icon', () => {
+ expect(vm.currentIcon).toBe('angle-double-right');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_spec.js b/spec/javascripts/repo/components/ide_spec.js
index b32d2c13af8..acfd63eb8de 100644
--- a/spec/javascripts/repo/components/repo_spec.js
+++ b/spec/javascripts/repo/components/ide_spec.js
@@ -1,16 +1,18 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repo from '~/repo/components/repo.vue';
+import store from '~/ide/stores';
+import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
-describe('repo component', () => {
+describe('ide component', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(repo);
+ const Component = Vue.extend(ide);
- vm = createComponentWithStore(Component, store).$mount();
+ vm = createComponentWithStore(Component, store, {
+ emptyStateSvgPath: 'svg',
+ }).$mount();
});
afterEach(() => {
@@ -24,7 +26,9 @@ describe('repo component', () => {
});
it('renders panel right when files are open', (done) => {
- vm.$store.state.tree.push(file());
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [file()],
+ };
Vue.nextTick(() => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js
index 9a705a1f0ed..cd1d073ec18 100644
--- a/spec/javascripts/repo/components/new_branch_form_spec.js
+++ b/spec/javascripts/repo/components/new_branch_form_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import newBranchForm from '~/repo/components/new_branch_form.vue';
+import store from '~/ide/stores';
+import newBranchForm from '~/ide/components/new_branch_form.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js
index 93b10fc1fee..6efbbf6d75e 100644
--- a/spec/javascripts/repo/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import newDropdown from '~/repo/components/new_dropdown/index.vue';
+import store from '~/ide/stores';
+import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
@@ -10,8 +10,12 @@ describe('new dropdown component', () => {
beforeEach(() => {
const component = Vue.extend(newDropdown);
- vm = createComponentWithStore(component, store);
+ vm = createComponentWithStore(component, store, {
+ branch: 'master',
+ path: '',
+ });
+ vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.path = '';
vm.$mount();
@@ -23,9 +27,10 @@ describe('new dropdown component', () => {
resetStore(vm.$store);
});
- it('renders new file and new directory links', () => {
+ it('renders new file, upload and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
- expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('New directory');
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
+ expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
@@ -36,7 +41,7 @@ describe('new dropdown component', () => {
});
it('sets modalType to tree when new directory is clicked', () => {
- vm.$el.querySelectorAll('a')[1].click();
+ vm.$el.querySelectorAll('a')[2].click();
expect(vm.modalType).toBe('tree');
});
@@ -52,16 +57,17 @@ describe('new dropdown component', () => {
});
});
- describe('toggleModalOpen', () => {
+ describe('hideModal', () => {
+ beforeAll((done) => {
+ vm.openModal = true;
+ Vue.nextTick(done);
+ });
+
it('closes modal after toggling', (done) => {
- vm.toggleModalOpen();
+ vm.hideModal();
Vue.nextTick()
.then(() => {
- expect(vm.$el.querySelector('.modal')).not.toBeNull();
- })
- .then(vm.toggleModalOpen)
- .then(() => {
expect(vm.$el.querySelector('.modal')).toBeNull();
})
.then(done)
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
index 1ff7590ec79..233cca06ed0 100644
--- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
@@ -1,12 +1,42 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import modal from '~/repo/components/new_dropdown/modal.vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import modal from '~/ide/components/new_dropdown/modal.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
let vm;
+ let projectTree;
+
+ beforeEach(() => {
+ spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
+ data: {
+ id: '123',
+ },
+ }));
+
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: {
+ id: '123branch',
+ },
+ }));
+
+ 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' }],
+ }),
+ }));
+ });
afterEach(() => {
vm.$destroy();
@@ -17,12 +47,26 @@ describe('new file modal component', () => {
['tree', 'blob'].forEach((type) => {
describe(type, () => {
beforeEach(() => {
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ store.state.trees = [];
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ projectTree = store.state.trees['abcproject/mybranch'];
+ store.state.currentProjectId = 'abcproject';
+
vm = createComponentWithStore(Component, store, {
type,
+ branchId: 'master',
path: '',
- }).$mount();
+ parent: projectTree,
+ });
vm.entryName = 'testing';
+
+ vm.$mount();
});
it(`sets modal title as ${type}`, () => {
@@ -50,6 +94,9 @@ describe('new file modal component', () => {
vm.createEntryInStore();
expect(vm.createTempEntry).toHaveBeenCalledWith({
+ projectId: 'abcproject',
+ branchId: 'master',
+ parent: projectTree,
name: 'testing',
type,
});
@@ -76,31 +123,18 @@ describe('new file modal component', () => {
});
it('opens newly created file', (done) => {
- vm.createEntryInStore();
-
- setTimeout(() => {
- expect(vm.$store.state.openFiles.length).toBe(1);
- expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
-
- done();
- });
- });
-
- it(`creates ${type} in the current stores path`, (done) => {
- vm.$store.state.path = 'app';
-
- vm.createEntryInStore();
-
- setTimeout(() => {
- expect(vm.$store.state.tree[0].path).toBe('app/testing');
- expect(vm.$store.state.tree[0].name).toBe('testing');
+ if (type === 'blob') {
+ vm.createEntryInStore();
- if (type === 'tree') {
- expect(vm.$store.state.tree[0].tree.length).toBe(1);
- }
+ setTimeout(() => {
+ expect(vm.$store.state.openFiles.length).toBe(1);
+ expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
+ done();
+ });
+ } else {
done();
- });
+ }
});
if (type === 'blob') {
@@ -108,25 +142,27 @@ describe('new file modal component', () => {
vm.createEntryInStore();
setTimeout(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('testing');
- expect(vm.$store.state.tree[0].type).toBe('blob');
- expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('testing');
+ expect(baseTree[0].type).toBe('blob');
+ expect(baseTree[0].tempFile).toBeTruthy();
done();
});
});
it('does not create temp file when file already exists', (done) => {
- vm.$store.state.tree.push(file('testing', '1', type));
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('testing', '1', type));
vm.createEntryInStore();
setTimeout(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('testing');
- expect(vm.$store.state.tree[0].type).toBe('blob');
- expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('testing');
+ expect(baseTree[0].type).toBe('blob');
+ expect(baseTree[0].tempFile).toBeFalsy();
done();
});
@@ -135,48 +171,47 @@ describe('new file modal component', () => {
it('creates new tree', () => {
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('testing');
- expect(vm.$store.state.tree[0].type).toBe('tree');
- expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
- expect(vm.$store.state.tree[0].tree.length).toBe(1);
- expect(vm.$store.state.tree[0].tree[0].name).toBe('.gitkeep');
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('testing');
+ expect(baseTree[0].type).toBe('tree');
+ expect(baseTree[0].tempFile).toBeTruthy();
});
it('creates multiple trees when entryName has slashes', () => {
vm.entryName = 'app/test';
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('app');
- expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
- expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('app');
});
it('creates tree in existing tree', () => {
- vm.$store.state.tree.push(file('app', '1', 'tree'));
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app/test';
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('app');
- expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
- expect(vm.$store.state.tree[0].tree[0].tempFile).toBeTruthy();
- expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
- expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('app');
+ expect(baseTree[0].tempFile).toBeFalsy();
+ expect(baseTree[0].tree[0].tempFile).toBeTruthy();
+ expect(baseTree[0].tree[0].name).toBe('test');
});
it('does not create new tree when already exists', () => {
- vm.$store.state.tree.push(file('app', '1', 'tree'));
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app';
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('app');
- expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
- expect(vm.$store.state.tree[0].tree.length).toBe(0);
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('app');
+ expect(baseTree[0].tempFile).toBeFalsy();
+ expect(baseTree[0].tree.length).toBe(0);
});
}
});
@@ -188,6 +223,8 @@ describe('new file modal component', () => {
vm = createComponentWithStore(Component, store, {
type: 'tree',
+ projectId: 'abcproject',
+ branchId: 'master',
path: '',
}).$mount('.js-test');
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
index bf7893029b1..788c08e5279 100644
--- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
@@ -1,19 +1,61 @@
import Vue from 'vue';
-import upload from '~/repo/components/new_dropdown/upload.vue';
-import store from '~/repo/stores';
+import upload from '~/ide/components/new_dropdown/upload.vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('new dropdown upload', () => {
let vm;
+ let projectTree;
beforeEach(() => {
+ spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
+ data: {
+ id: '123',
+ },
+ }));
+
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: {
+ id: '123branch',
+ },
+ }));
+
+ 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' }],
+ }),
+ }));
+
const Component = Vue.extend(upload);
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ store.state.currentProjectId = 'abcproject';
+ store.state.trees = [];
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ projectTree = store.state.trees['abcproject/mybranch'];
+
vm = createComponentWithStore(Component, store, {
+ branchId: 'master',
path: '',
+ parent: projectTree,
});
+ vm.entryName = 'testing';
+
vm.$mount();
});
@@ -65,23 +107,33 @@ describe('new dropdown upload', () => {
vm.createFile(target, file, true);
vm.$nextTick(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe(file.name);
- expect(vm.$store.state.tree[0].content).toBe(target.result);
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe(file.name);
+ expect(baseTree[0].content).toBe(target.result);
done();
});
});
it('creates new file in path', (done) => {
- vm.$store.state.path = 'testing';
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ path: 'testing',
+ tree: [],
+ };
+ baseTree.push(tree);
+
+ vm.parent = tree;
vm.createFile(target, file, true);
vm.$nextTick(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe(file.name);
- expect(vm.$store.state.tree[0].content).toBe(target.result);
- expect(vm.$store.state.tree[0].path).toBe(`testing/${file.name}`);
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].tree[0].name).toBe(file.name);
+ expect(baseTree[0].tree[0].content).toBe(target.result);
+ expect(baseTree[0].tree[0].path).toBe(`testing/${file.name}`);
done();
});
@@ -91,10 +143,11 @@ describe('new dropdown upload', () => {
vm.createFile(binaryTarget, file, false);
vm.$nextTick(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe(file.name);
- expect(vm.$store.state.tree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
- expect(vm.$store.state.tree[0].base64).toBe(true);
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe(file.name);
+ expect(baseTree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
+ expect(baseTree[0].base64).toBe(true);
done();
});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index 1c794123095..cd93fb3ccbf 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import service from '~/repo/services';
-import repoCommitSection from '~/repo/components/repo_commit_section.vue';
+import * as urlUtils from '~/lib/utils/url_utility';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
@@ -15,6 +16,18 @@ describe('RepoCommitSection', () => {
store,
}).$mount();
+ comp.$store.state.currentProjectId = 'abcproject';
+ comp.$store.state.currentBranchId = 'master';
+ comp.$store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
+ comp.$store.state.rightPanelCollapsed = false;
comp.$store.state.currentBranch = 'master';
comp.$store.state.openFiles = [file(), file()];
comp.$store.state.openFiles.forEach(f => Object.assign(f, {
@@ -28,7 +41,19 @@ describe('RepoCommitSection', () => {
beforeEach((done) => {
vm = createComponent();
- vm.collapsed = false;
+ 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' }],
+ }),
+ }));
Vue.nextTick(done);
});
@@ -44,7 +69,6 @@ describe('RepoCommitSection', () => {
const submitCommit = vm.$el.querySelector('form .btn');
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
- expect(vm.$el.querySelector('.multi-file-commit-panel-section header').textContent.trim()).toEqual('Staged');
expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => {
@@ -97,7 +121,7 @@ describe('RepoCommitSection', () => {
});
it('redirects to MR creation page if start new MR checkbox checked', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
vm.startNewMR = true;
vm.makeCommit();
@@ -105,7 +129,7 @@ describe('RepoCommitSection', () => {
getSetTimeoutPromise()
.then(() => Vue.nextTick())
.then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalled();
+ expect(urlUtils.visitUrl).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
index 44018464b35..2895b794506 100644
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoEditButton from '~/repo/components/repo_edit_button.vue';
+import store from '~/ide/stores';
+import repoEditButton from '~/ide/components/repo_edit_button.vue';
import { file, resetStore } from '../helpers';
describe('RepoEditButton', () => {
@@ -32,7 +32,7 @@ describe('RepoEditButton', () => {
vm.$mount();
expect(vm.$el.querySelector('.btn')).not.toBeNull();
- expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
});
it('renders edit button with cancel text', () => {
@@ -50,7 +50,7 @@ describe('RepoEditButton', () => {
vm.$el.querySelector('.btn').click();
vm.$nextTick(() => {
- expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
done();
});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index 81158cad639..e7b2ed08acd 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoEditor from '~/repo/components/repo_editor.vue';
-import monacoLoader from '~/repo/monaco_loader';
+import store from '~/ide/stores';
+import repoEditor from '~/ide/components/repo_editor.vue';
+import monacoLoader from '~/ide/monaco_loader';
import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
index d6e255e4810..115569a9117 100644
--- a/spec/javascripts/repo/components/repo_file_buttons_spec.js
+++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
+import store from '~/ide/stores';
+import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
import { file, resetStore } from '../helpers';
describe('RepoFileButtons', () => {
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index bf9181fb09c..0810da87e80 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoFile from '~/repo/components/repo_file.vue';
+import store from '~/ide/stores';
+import repoFile from '~/ide/components/repo_file.vue';
import { file, resetStore } from '../helpers';
describe('RepoFile', () => {
@@ -32,14 +32,9 @@ describe('RepoFile', () => {
vm.$mount();
const name = vm.$el.querySelector('.repo-file-name');
- const fileIcon = vm.$el.querySelector('.file-icon');
- expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px');
- expect(name.href).toMatch(`/${vm.file.url}`);
+ expect(name.href).toMatch('');
expect(name.textContent.trim()).toEqual(vm.file.name);
- expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy();
- expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`);
- expect(vm.$el.querySelectorAll('.animation-container').length).toBe(2);
});
it('does render if hasFiles is true and is loading tree', () => {
@@ -50,17 +45,6 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
});
- it('renders a spinner if the file is loading', () => {
- const f = file();
- f.loading = true;
- vm = createComponent({
- file: f,
- });
-
- expect(vm.$el.querySelector('.fa-spin.fa-spinner')).not.toBeNull();
- expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`);
- });
-
it('does not render commit message and datetime if mini', (done) => {
vm = createComponent({
file: file(),
@@ -75,16 +59,16 @@ describe('RepoFile', () => {
});
});
- it('fires clickedTreeRow when the link is clicked', () => {
+ it('fires clickFile when the link is clicked', () => {
vm = createComponent({
file: file(),
});
- spyOn(vm, 'clickedTreeRow');
+ spyOn(vm, 'clickFile');
vm.$el.click();
- expect(vm.clickedTreeRow).toHaveBeenCalledWith(vm.file);
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.file);
});
describe('submodule', () => {
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
index 031f2a9c0b2..18366fb89bc 100644
--- a/spec/javascripts/repo/components/repo_loading_file_spec.js
+++ b/spec/javascripts/repo/components/repo_loading_file_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoLoadingFile from '~/repo/components/repo_loading_file.vue';
+import store from '~/ide/stores';
+import repoLoadingFile from '~/ide/components/repo_loading_file.vue';
import { resetStore } from '../helpers';
describe('RepoLoadingFile', () => {
@@ -48,6 +48,7 @@ describe('RepoLoadingFile', () => {
it('renders 1 column of animated LoC if isMini', (done) => {
vm = createComponent();
+ vm.$store.state.leftPanelCollapsed = true;
vm.$store.state.openFiles.push('test');
vm.$nextTick(() => {
diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js
index 7f82ae36a64..ff26cab2262 100644
--- a/spec/javascripts/repo/components/repo_prev_directory_spec.js
+++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue';
+import store from '~/ide/stores';
+import repoPrevDirectory from '~/ide/components/repo_prev_directory.vue';
import { resetStore } from '../helpers';
describe('RepoPrevDirectory', () => {
diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js
index 8d1a87494cf..e90837e4cb2 100644
--- a/spec/javascripts/repo/components/repo_preview_spec.js
+++ b/spec/javascripts/repo/components/repo_preview_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoPreview from '~/repo/components/repo_preview.vue';
+import store from '~/ide/stores';
+import repoPreview from '~/ide/components/repo_preview.vue';
import { file, resetStore } from '../helpers';
describe('RepoPreview', () => {
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
index 7d2174196c9..507bca983df 100644
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoTab from '~/repo/components/repo_tab.vue';
+import store from '~/ide/stores';
+import repoTab from '~/ide/components/repo_tab.vue';
import { file, resetStore } from '../helpers';
describe('RepoTab', () => {
@@ -31,16 +31,16 @@ describe('RepoTab', () => {
expect(name.textContent.trim()).toEqual(vm.tab.name);
});
- it('calls setFileActive when clicking tab', () => {
+ it('fires clickFile when the link is clicked', () => {
vm = createComponent({
tab: file(),
});
- spyOn(vm, 'setFileActive');
+ spyOn(vm, 'clickFile');
vm.$el.click();
- expect(vm.setFileActive).toHaveBeenCalledWith(vm.tab);
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
});
it('calls closeFile when clicking close button', () => {
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
index 1fb2242c051..0beaf643793 100644
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoTabs from '~/repo/components/repo_tabs.vue';
+import store from '~/ide/stores';
+import repoTabs from '~/ide/components/repo_tabs.vue';
import { file, resetStore } from '../helpers';
describe('RepoTabs', () => {
diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js
index 820a44992b4..ac43d221198 100644
--- a/spec/javascripts/repo/helpers.js
+++ b/spec/javascripts/repo/helpers.js
@@ -1,5 +1,5 @@
-import { decorateData } from '~/repo/stores/utils';
-import state from '~/repo/stores/state';
+import { decorateData } from '~/ide/stores/utils';
+import state from '~/ide/stores/state';
export const resetStore = (store) => {
store.replaceState(state());
@@ -12,4 +12,5 @@ export const file = (name = 'name', id = name, type = '') => decorateData({
url: 'url',
name,
path: name,
+ lastCommit: {},
});
diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js
index 62c3913bf4d..af12ca15369 100644
--- a/spec/javascripts/repo/lib/common/disposable_spec.js
+++ b/spec/javascripts/repo/lib/common/disposable_spec.js
@@ -1,4 +1,4 @@
-import Disposable from '~/repo/lib/common/disposable';
+import Disposable from '~/ide/lib/common/disposable';
describe('Multi-file editor library disposable class', () => {
let instance;
diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js
index 8c134f178c0..563c2e33834 100644
--- a/spec/javascripts/repo/lib/common/model_manager_spec.js
+++ b/spec/javascripts/repo/lib/common/model_manager_spec.js
@@ -1,6 +1,6 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import ModelManager from '~/repo/lib/common/model_manager';
+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', () => {
diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js
index d41ade237ca..878a4a3f3fe 100644
--- a/spec/javascripts/repo/lib/common/model_spec.js
+++ b/spec/javascripts/repo/lib/common/model_spec.js
@@ -1,6 +1,6 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import Model from '~/repo/lib/common/model';
+import monacoLoader from '~/ide/monaco_loader';
+import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library model', () => {
diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js
index 2e32e8fa0bd..fea12d74dca 100644
--- a/spec/javascripts/repo/lib/decorations/controller_spec.js
+++ b/spec/javascripts/repo/lib/decorations/controller_spec.js
@@ -1,8 +1,8 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import editor from '~/repo/lib/editor';
-import DecorationsController from '~/repo/lib/decorations/controller';
-import Model from '~/repo/lib/common/model';
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library decorations controller', () => {
diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js
index ed62e28d3a3..1d55c165260 100644
--- a/spec/javascripts/repo/lib/diff/controller_spec.js
+++ b/spec/javascripts/repo/lib/diff/controller_spec.js
@@ -1,10 +1,10 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import editor from '~/repo/lib/editor';
-import ModelManager from '~/repo/lib/common/model_manager';
-import DecorationsController from '~/repo/lib/decorations/controller';
-import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/repo/lib/diff/controller';
-import { computeDiff } from '~/repo/lib/diff/diff';
+import monacoLoader from '~/ide/monaco_loader';
+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';
+import { computeDiff } from '~/ide/lib/diff/diff';
import { file } from '../../helpers';
describe('Multi-file editor library dirty diff controller', () => {
diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js
index 3269ec5d2c9..57f3ac3d365 100644
--- a/spec/javascripts/repo/lib/diff/diff_spec.js
+++ b/spec/javascripts/repo/lib/diff/diff_spec.js
@@ -1,4 +1,4 @@
-import { computeDiff } from '~/repo/lib/diff/diff';
+import { computeDiff } from '~/ide/lib/diff/diff';
describe('Multi-file editor library diff calculator', () => {
describe('computeDiff', () => {
diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js
index b4887d063ed..edbf5450dce 100644
--- a/spec/javascripts/repo/lib/editor_options_spec.js
+++ b/spec/javascripts/repo/lib/editor_options_spec.js
@@ -1,4 +1,4 @@
-import editorOptions from '~/repo/lib/editor_options';
+import editorOptions from '~/ide/lib/editor_options';
describe('Multi-file editor library editor options', () => {
it('returns an array', () => {
diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js
index cd32832a232..8d51d48a782 100644
--- a/spec/javascripts/repo/lib/editor_spec.js
+++ b/spec/javascripts/repo/lib/editor_spec.js
@@ -1,6 +1,6 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import editor from '~/repo/lib/editor';
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
diff --git a/spec/javascripts/repo/monaco_loader_spec.js b/spec/javascripts/repo/monaco_loader_spec.js
index 887a80160fc..b8ac36972aa 100644
--- a/spec/javascripts/repo/monaco_loader_spec.js
+++ b/spec/javascripts/repo/monaco_loader_spec.js
@@ -1,5 +1,5 @@
import monacoContext from 'monaco-editor/dev/vs/loader';
-import monacoLoader from '~/repo/monaco_loader';
+import monacoLoader from '~/ide/monaco_loader';
describe('MonacoLoader', () => {
it('calls require.config and exports require', () => {
diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js
index af9d6835a67..00d16fd790d 100644
--- a/spec/javascripts/repo/stores/actions/branch_spec.js
+++ b/spec/javascripts/repo/stores/actions/branch_spec.js
@@ -1,5 +1,5 @@
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('Multi-file store branch actions', () => {
@@ -16,19 +16,25 @@ describe('Multi-file store branch actions', () => {
}));
spyOn(history, 'pushState');
- store.state.project.id = 2;
- store.state.currentBranch = 'testing';
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'testing';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
});
it('creates new branch', (done) => {
store.dispatch('createNewBranch', 'master')
.then(() => {
- expect(store.state.currentBranch).toBe('testing');
- expect(service.createBranch).toHaveBeenCalledWith(2, {
+ expect(store.state.currentBranchId).toBe('testing');
+ expect(service.createBranch).toHaveBeenCalledWith('abcproject', {
branch: 'master',
ref: 'testing',
});
- expect(history.pushState).toHaveBeenCalled();
done();
})
diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js
index 099c0556e71..8ce01d3bf12 100644
--- a/spec/javascripts/repo/stores/actions/file_spec.js
+++ b/spec/javascripts/repo/stores/actions/file_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => {
@@ -24,8 +24,6 @@ describe('Multi-file store file actions', () => {
localFile.parentTreeUrl = 'parentTreeUrl';
store.state.openFiles.push(localFile);
-
- spyOn(history, 'pushState');
});
afterEach(() => {
@@ -82,15 +80,6 @@ describe('Multi-file store file actions', () => {
}).catch(done.fail);
});
- it('calls pushState when no open files are left', (done) => {
- store.dispatch('closeFile', { file: localFile })
- .then(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl');
-
- done();
- }).catch(done.fail);
- });
-
it('sets next file as active', (done) => {
const f = file();
store.state.openFiles.push(f);
@@ -322,8 +311,26 @@ describe('Multi-file store file actions', () => {
});
describe('createTempFile', () => {
+ let projectTree;
+
beforeEach(() => {
document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ projectTree = store.state.trees['abcproject/mybranch'];
});
afterEach(() => {
@@ -332,11 +339,13 @@ describe('Multi-file store file actions', () => {
it('creates temp file', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(f.tempFile).toBeTruthy();
- expect(store.state.tree.length).toBe(1);
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
done();
}).catch(done.fail);
@@ -344,8 +353,10 @@ describe('Multi-file store file actions', () => {
it('adds tmp file to open files', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(f.name);
@@ -356,8 +367,10 @@ describe('Multi-file store file actions', () => {
it('sets tmp file as active', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(f.active).toBeTruthy();
@@ -367,8 +380,10 @@ describe('Multi-file store file actions', () => {
it('enters edit mode if file is not base64', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then(() => {
expect(store.state.editMode).toBeTruthy();
@@ -376,24 +391,14 @@ describe('Multi-file store file actions', () => {
}).catch(done.fail);
});
- it('does not enter edit mode if file is base64', (done) => {
- store.dispatch('createTempFile', {
- tree: store.state,
- name: 'test',
- base64: true,
- }).then(() => {
- expect(store.state.editMode).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
it('creates flash message is file already exists', (done) => {
- store.state.tree.push(file('test', '1', 'blob'));
+ store.state.trees['abcproject/mybranch'].tree.push(file('test', '1', 'blob'));
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then(() => {
expect(document.querySelector('.flash-alert')).not.toBeNull();
@@ -402,11 +407,13 @@ describe('Multi-file store file actions', () => {
});
it('increases level of file', (done) => {
- store.state.level = 1;
+ store.state.trees['abcproject/mybranch'].level = 1;
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(f.level).toBe(2);
diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js
index 393a797c6a3..65351dbb7d9 100644
--- a/spec/javascripts/repo/stores/actions/tree_spec.js
+++ b/spec/javascripts/repo/stores/actions/tree_spec.js
@@ -1,9 +1,30 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store tree actions', () => {
+ let projectTree;
+
+ const basicCallParameters = {
+ endpoint: 'rootEndpoint',
+ projectId: 'abcproject',
+ branch: 'master',
+ };
+
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
afterEach(() => {
resetStore(store);
});
@@ -23,38 +44,32 @@ describe('Multi-file store tree actions', () => {
submodules: [{ name: 'submodule' }],
}),
}));
- spyOn(history, 'pushState');
-
- Object.assign(store.state.endpoints, {
- rootEndpoint: 'rootEndpoint',
- });
});
it('calls service getTreeData', (done) => {
- store.dispatch('getTreeData')
- .then(() => {
- expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
+ store.dispatch('getTreeData', basicCallParameters)
+ .then(() => {
+ expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
it('adds data into tree', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
- expect(store.state.tree.length).toBe(3);
- expect(store.state.tree[0].type).toBe('tree');
- expect(store.state.tree[1].type).toBe('submodule');
- expect(store.state.tree[2].type).toBe('blob');
+ projectTree = store.state.trees['abcproject/master'];
+ expect(projectTree.tree.length).toBe(3);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[1].type).toBe('submodule');
+ expect(projectTree.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
it('sets parent tree URL', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.parentTreeUrl).toBe('parent_tree_url');
@@ -63,10 +78,9 @@ describe('Multi-file store tree actions', () => {
});
it('sets last commit path', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
- expect(store.state.lastCommitPath).toBe('last_commit_path');
+ expect(store.state.trees['abcproject/master'].lastCommitPath).toBe('last_commit_path');
done();
}).catch(done.fail);
@@ -75,8 +89,7 @@ describe('Multi-file store tree actions', () => {
it('sets root if not currently at root', (done) => {
store.state.isInitialRoot = false;
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.isInitialRoot).toBeTruthy();
expect(store.state.isRoot).toBeTruthy();
@@ -86,7 +99,7 @@ describe('Multi-file store tree actions', () => {
});
it('sets page title', (done) => {
- store.dispatch('getTreeData')
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(document.title).toBe('test');
@@ -94,40 +107,15 @@ describe('Multi-file store tree actions', () => {
}).catch(done.fail);
});
- it('toggles loading', (done) => {
- store.dispatch('getTreeData')
- .then(() => {
- expect(store.state.loading).toBeTruthy();
-
- return Vue.nextTick();
- })
- .then(() => {
- expect(store.state.loading).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
- it('calls pushState with endpoint', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
- .then(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint');
-
- done();
- }).catch(done.fail);
- });
-
it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
store.state.prevLastCommitPath = 'test';
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
- expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state);
+ expect(getLastCommitDataSpy).toHaveBeenCalledWith(projectTree);
store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
@@ -148,6 +136,8 @@ describe('Multi-file store tree actions', () => {
store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
tree = {
+ projectId: 'abcproject',
+ branchId: 'master',
opened: false,
tree: [],
};
@@ -174,10 +164,11 @@ describe('Multi-file store tree actions', () => {
tree,
}).then(() => {
expect(getTreeDataSpy).toHaveBeenCalledWith({
+ projectId: 'abcproject',
+ branch: 'master',
endpoint: 'test',
tree,
});
- expect(store.state.previousUrl).toBe('test');
done();
}).catch(done.fail);
@@ -198,155 +189,29 @@ describe('Multi-file store tree actions', () => {
done();
}).catch(done.fail);
});
-
- it('pushes new state', (done) => {
- spyOn(history, 'pushState');
- Object.assign(tree, {
- opened: true,
- parentTreeUrl: 'testing',
- });
-
- store.dispatch('toggleTreeOpen', {
- endpoint: 'test',
- tree,
- }).then(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('clickedTreeRow', () => {
- describe('tree', () => {
- let toggleTreeOpenSpy;
- let oldToggleTreeOpen;
-
- beforeEach(() => {
- toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen');
-
- oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line
- store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line
- });
-
- afterEach(() => {
- store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line
- });
-
- it('opens tree', (done) => {
- const tree = {
- url: 'a',
- type: 'tree',
- };
-
- store.dispatch('clickedTreeRow', tree)
- .then(() => {
- expect(toggleTreeOpenSpy).toHaveBeenCalledWith({
- endpoint: tree.url,
- tree,
- });
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('submodule', () => {
- let row;
-
- beforeEach(() => {
- spyOn(gl.utils, 'visitUrl');
-
- row = {
- url: 'submoduleurl',
- type: 'submodule',
- loading: false,
- };
- });
-
- it('toggles loading for row', (done) => {
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(row.loading).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('opens submodule URL', (done) => {
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('blob', () => {
- let row;
-
- beforeEach(() => {
- row = {
- type: 'blob',
- opened: false,
- };
- });
-
- it('calls getFileData', (done) => {
- const getFileDataSpy = jasmine.createSpy('getFileData');
- const oldGetFileData = store._actions.getFileData; // eslint-disable-line
- store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line
-
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(getFileDataSpy).toHaveBeenCalledWith(row);
-
- store._actions.getFileData = oldGetFileData; // eslint-disable-line
-
- done();
- }).catch(done.fail);
- });
-
- it('calls setFileActive when file is opened', (done) => {
- const setFileActiveSpy = jasmine.createSpy('setFileActive');
- const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line
- store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line
-
- row.opened = true;
-
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(setFileActiveSpy).toHaveBeenCalledWith(row);
-
- store._actions.setFileActive = oldSetFileActive; // eslint-disable-line
-
- done();
- }).catch(done.fail);
- });
- });
});
describe('createTempTree', () => {
- it('creates temp tree', (done) => {
- store.dispatch('createTempTree', 'test')
- .then(() => {
- expect(store.state.tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].name).toBe('test');
- expect(store.state.tree[0].type).toBe('tree');
-
- done();
- }).catch(done.fail);
+ beforeEach(() => {
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ projectTree = store.state.trees['abcproject/mybranch'];
});
- it('creates .gitkeep file in temp tree', (done) => {
- store.dispatch('createTempTree', 'test')
- .then(() => {
- expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].tree[0].name).toBe('.gitkeep');
+ it('creates temp tree', (done) => {
+ store.dispatch('createTempTree', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ name: 'test',
+ parent: projectTree,
+ })
+ .then(() => {
+ expect(projectTree.tree[0].name).toBe('test');
+ expect(projectTree.tree[0].type).toBe('tree');
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
it('creates new folder inside another tree', (done) => {
@@ -356,35 +221,46 @@ describe('Multi-file store tree actions', () => {
tree: [],
};
- store.state.tree.push(tree);
+ projectTree.tree.push(tree);
- store.dispatch('createTempTree', 'testing/test')
- .then(() => {
- expect(store.state.tree[0].name).toBe('testing');
- expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].tree[0].name).toBe('test');
- expect(store.state.tree[0].tree[0].type).toBe('tree');
+ store.dispatch('createTempTree', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ name: 'testing/test',
+ parent: projectTree,
+ })
+ .then(() => {
+ expect(projectTree.tree[0].name).toBe('testing');
+ expect(projectTree.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(projectTree.tree[0].tree[0].name).toBe('test');
+ expect(projectTree.tree[0].tree[0].type).toBe('tree');
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
it('does not create new tree if already exists', (done) => {
const tree = {
type: 'tree',
name: 'testing',
+ endpoint: 'test',
tree: [],
};
- store.state.tree.push(tree);
+ projectTree.tree.push(tree);
- store.dispatch('createTempTree', 'testing/test')
- .then(() => {
- expect(store.state.tree[0].name).toBe('testing');
- expect(store.state.tree[0].tempFile).toBeUndefined();
+ store.dispatch('createTempTree', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ name: 'testing/test',
+ parent: projectTree,
+ })
+ .then(() => {
+ expect(projectTree.tree[0].name).toBe('testing');
+ expect(projectTree.tree[0].tempFile).toBeUndefined();
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
});
@@ -404,12 +280,17 @@ describe('Multi-file store tree actions', () => {
}]),
}));
- store.state.tree.push(file('testing', '1', 'tree'));
- store.state.lastCommitPath = 'lastcommitpath';
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ projectTree = store.state.trees['abcproject/mybranch'];
+ projectTree.tree.push(file('testing', '1', 'tree'));
+ projectTree.lastCommitPath = 'lastcommitpath';
});
it('calls service with lastCommitPath', (done) => {
- store.dispatch('getLastCommitData')
+ store.dispatch('getLastCommitData', projectTree)
.then(() => {
expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
@@ -418,22 +299,22 @@ describe('Multi-file store tree actions', () => {
});
it('updates trees last commit data', (done) => {
- store.dispatch('getLastCommitData')
- .then(Vue.nextTick)
+ store.dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
.then(() => {
- expect(store.state.tree[0].lastCommit.message).toBe('commit message');
+ expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
done();
}).catch(done.fail);
});
it('does not update entry if not found', (done) => {
- store.state.tree[0].name = 'a';
+ projectTree.tree[0].name = 'a';
- store.dispatch('getLastCommitData')
+ store.dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick)
.then(() => {
- expect(store.state.tree[0].lastCommit.message).not.toBe('commit message');
+ expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
done();
}).catch(done.fail);
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
index f2a7a698912..0b0d34f072a 100644
--- a/spec/javascripts/repo/stores/actions_spec.js
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import * as urlUtils from '~/lib/utils/url_utility';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { resetStore, file } from '../helpers';
describe('Multi-file store actions', () => {
@@ -10,11 +11,11 @@ describe('Multi-file store actions', () => {
describe('redirectToUrl', () => {
it('calls visitUrl', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
store.dispatch('redirectToUrl', 'test')
.then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('test');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
done();
})
@@ -109,6 +110,7 @@ describe('Multi-file store actions', () => {
it('can force closed if there are changed files', (done) => {
store.state.editMode = true;
+
store.state.openFiles.push(file());
store.state.openFiles[0].changed = true;
@@ -124,7 +126,6 @@ describe('Multi-file store actions', () => {
it('discards file changes', (done) => {
const f = file();
store.state.editMode = true;
- store.state.tree.push(f);
store.state.openFiles.push(f);
f.changed = true;
@@ -140,8 +141,6 @@ describe('Multi-file store actions', () => {
describe('toggleBlobView', () => {
it('sets edit mode view if in edit mode', (done) => {
- store.state.editMode = true;
-
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-editor');
@@ -152,6 +151,8 @@ describe('Multi-file store actions', () => {
});
it('sets preview mode view if not in edit mode', (done) => {
+ store.state.editMode = false;
+
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-preview');
@@ -164,9 +165,15 @@ describe('Multi-file store actions', () => {
describe('checkCommitStatus', () => {
beforeEach(() => {
- store.state.project.id = 2;
- store.state.currentBranch = 'master';
- store.state.currentRef = '1';
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
});
it('calls service', (done) => {
@@ -176,7 +183,7 @@ describe('Multi-file store actions', () => {
store.dispatch('checkCommitStatus')
.then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith(2, 'master');
+ expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
done();
})
@@ -220,7 +227,17 @@ describe('Multi-file store actions', () => {
document.body.innerHTML += '<div class="flash-container"></div>';
- store.state.project.id = 123;
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: 'webUrl',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
payload = {
branch: 'master',
};
@@ -247,7 +264,7 @@ describe('Multi-file store actions', () => {
it('calls service', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
- expect(service.commit).toHaveBeenCalledWith(123, payload);
+ expect(service.commit).toHaveBeenCalledWith('abcproject', payload);
done();
}).catch(done.fail);
@@ -283,17 +300,6 @@ describe('Multi-file store actions', () => {
}).catch(done.fail);
});
- it('toggles edit mode', (done) => {
- store.state.editMode = true;
-
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- expect(store.state.editMode).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
it('closes all files', (done) => {
store.state.openFiles.push(file());
store.state.openFiles[0].opened = true;
@@ -316,23 +322,12 @@ describe('Multi-file store actions', () => {
}).catch(done.fail);
});
- it('updates commit ref', (done) => {
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- expect(store.state.currentRef).toBe('123456');
-
- done();
- }).catch(done.fail);
- });
-
it('redirects to new merge request page', (done) => {
- spyOn(gl.utils, 'visitUrl');
-
- store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch=';
+ spyOn(urlUtils, 'visitUrl');
store.dispatch('commitChanges', { payload, newMr: true })
.then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('webUrl/merge_requests/new?merge_request%5Bsource_branch%5D=master');
done();
}).catch(done.fail);
@@ -362,15 +357,30 @@ describe('Multi-file store actions', () => {
});
describe('createTempEntry', () => {
+ beforeEach(() => {
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ });
+
it('creates a temp tree', (done) => {
+ const projectTree = store.state.trees['abcproject/mybranch'];
+
store.dispatch('createTempEntry', {
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
name: 'test',
type: 'tree',
})
.then(() => {
- expect(store.state.tree.length).toBe(1);
- expect(store.state.tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].type).toBe('tree');
+ const baseTree = projectTree.tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].tempFile).toBeTruthy();
+ expect(baseTree[0].type).toBe('tree');
done();
})
@@ -378,14 +388,20 @@ describe('Multi-file store actions', () => {
});
it('creates temp file', (done) => {
+ const projectTree = store.state.trees['abcproject/mybranch'];
+
store.dispatch('createTempEntry', {
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
name: 'test',
type: 'blob',
})
.then(() => {
- expect(store.state.tree.length).toBe(1);
- expect(store.state.tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].type).toBe('blob');
+ const baseTree = projectTree.tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].tempFile).toBeTruthy();
+ expect(baseTree[0].type).toBe('blob');
done();
})
diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js
index 952b8ec3a59..d0d5934f29a 100644
--- a/spec/javascripts/repo/stores/getters_spec.js
+++ b/spec/javascripts/repo/stores/getters_spec.js
@@ -1,5 +1,5 @@
-import * as getters from '~/repo/stores/getters';
-import state from '~/repo/stores/state';
+import * as getters from '~/ide/stores/getters';
+import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store getters', () => {
@@ -9,20 +9,6 @@ describe('Multi-file store getters', () => {
localState = state();
});
- describe('treeList', () => {
- it('returns flat tree list', () => {
- localState.tree.push(file('1'));
- localState.tree[0].tree.push(file('2'));
- localState.tree[0].tree[0].tree.push(file('3'));
-
- const treeList = getters.treeList(localState);
-
- expect(treeList.length).toBe(3);
- expect(treeList[1].name).toBe(localState.tree[0].tree[0].name);
- expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name);
- });
- });
-
describe('changedFiles', () => {
it('returns a list of changed opened files', () => {
localState.openFiles.push(file());
@@ -49,7 +35,7 @@ describe('Multi-file store getters', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
- expect(getters.activeFile(localState)).toBeUndefined();
+ expect(getters.activeFile(localState)).toBeNull();
});
});
@@ -67,18 +53,6 @@ describe('Multi-file store getters', () => {
});
});
- describe('isCollapsed', () => {
- it('returns true if state has open files', () => {
- localState.openFiles.push(file());
-
- expect(getters.isCollapsed(localState)).toBeTruthy();
- });
-
- it('returns false if state has no open files', () => {
- expect(getters.isCollapsed(localState)).toBeFalsy();
- });
- });
-
describe('canEditFile', () => {
beforeEach(() => {
localState.onTopOfBranch = true;
@@ -109,12 +83,6 @@ describe('Multi-file store getters', () => {
expect(getters.canEditFile(localState)).toBeFalsy();
});
-
- it('returns false if user can commit but on a branch', () => {
- localState.onTopOfBranch = false;
-
- expect(getters.canEditFile(localState)).toBeFalsy();
- });
});
describe('modifiedFiles', () => {
diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js
index 3c06794d5e3..a7167537ef2 100644
--- a/spec/javascripts/repo/stores/mutations/branch_spec.js
+++ b/spec/javascripts/repo/stores/mutations/branch_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations/branch';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations/branch';
+import state from '~/ide/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
@@ -12,7 +12,7 @@ describe('Multi-file store branch mutations', () => {
it('sets currentBranch', () => {
mutations.SET_CURRENT_BRANCH(localState, 'master');
- expect(localState.currentBranch).toBe('master');
+ expect(localState.currentBranchId).toBe('master');
});
});
});
diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js
index 2f2835dde1f..947a60587df 100644
--- a/spec/javascripts/repo/stores/mutations/file_spec.js
+++ b/spec/javascripts/repo/stores/mutations/file_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations/file';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations/file';
+import state from '~/ide/stores/state';
import { file } from '../../helpers';
describe('Multi-file store file mutations', () => {
diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js
index 1c76cfed9c8..cf1248ba28b 100644
--- a/spec/javascripts/repo/stores/mutations/tree_spec.js
+++ b/spec/javascripts/repo/stores/mutations/tree_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations/tree';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations/tree';
+import state from '~/ide/stores/state';
import { file } from '../../helpers';
describe('Multi-file store tree mutations', () => {
diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js
index d1c9885e01d..5fd8ad94972 100644
--- a/spec/javascripts/repo/stores/mutations_spec.js
+++ b/spec/javascripts/repo/stores/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations';
+import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store mutations', () => {
@@ -65,11 +65,11 @@ describe('Multi-file store mutations', () => {
it('toggles editMode', () => {
mutations.TOGGLE_EDIT_MODE(localState);
- expect(localState.editMode).toBeTruthy();
+ expect(localState.editMode).toBeFalsy();
mutations.TOGGLE_EDIT_MODE(localState);
- expect(localState.editMode).toBeFalsy();
+ expect(localState.editMode).toBeTruthy();
});
});
@@ -85,14 +85,6 @@ describe('Multi-file store mutations', () => {
});
});
- describe('SET_COMMIT_REF', () => {
- it('sets currentRef', () => {
- mutations.SET_COMMIT_REF(localState, '123');
-
- expect(localState.currentRef).toBe('123');
- });
- });
-
describe('SET_ROOT', () => {
it('sets isRoot & initialRoot', () => {
mutations.SET_ROOT(localState, true);
@@ -107,11 +99,27 @@ describe('Multi-file store mutations', () => {
});
});
- describe('SET_PREVIOUS_URL', () => {
- it('sets previousUrl', () => {
- mutations.SET_PREVIOUS_URL(localState, 'testing');
+ describe('SET_LEFT_PANEL_COLLAPSED', () => {
+ it('sets left panel collapsed', () => {
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.leftPanelCollapsed).toBeTruthy();
+
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.leftPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('SET_RIGHT_PANEL_COLLAPSED', () => {
+ it('sets right panel collapsed', () => {
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.rightPanelCollapsed).toBeTruthy();
+
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false);
- expect(localState.previousUrl).toBe('testing');
+ expect(localState.rightPanelCollapsed).toBeFalsy();
});
});
});
diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js
index 37287c587d7..89745a2029e 100644
--- a/spec/javascripts/repo/stores/utils_spec.js
+++ b/spec/javascripts/repo/stores/utils_spec.js
@@ -1,4 +1,6 @@
-import * as utils from '~/repo/stores/utils';
+import * as utils from '~/ide/stores/utils';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
describe('Multi-file store utils', () => {
describe('setPageTitle', () => {
@@ -9,13 +11,28 @@ describe('Multi-file store utils', () => {
});
});
- describe('pushState', () => {
- it('calls history.pushState', () => {
- spyOn(history, 'pushState');
+ describe('treeList', () => {
+ let localState;
- utils.pushState('test');
+ beforeEach(() => {
+ localState = state();
+ });
+
+ it('returns flat tree list', () => {
+ localState.trees = [];
+ localState.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ const baseTree = localState.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('1'));
+ baseTree[0].tree.push(file('2'));
+ baseTree[0].tree[0].tree.push(file('3'));
+
+ const treeList = utils.treeList(localState, 'abcproject/mybranch');
- expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test');
+ expect(treeList.length).toBe(3);
+ expect(treeList[1].name).toBe(baseTree[0].tree[0].name);
+ expect(treeList[2].name).toBe(baseTree[0].tree[0].tree[0].name);
});
});
@@ -52,10 +69,10 @@ describe('Multi-file store utils', () => {
});
describe('findIndexOfFile', () => {
- let state;
+ let localState;
beforeEach(() => {
- state = [{
+ localState = [{
path: '1',
}, {
path: '2',
@@ -63,7 +80,7 @@ describe('Multi-file store utils', () => {
});
it('finds in the index of an entry by path', () => {
- const index = utils.findIndexOfFile(state, {
+ const index = utils.findIndexOfFile(localState, {
path: '2',
});
@@ -72,10 +89,10 @@ describe('Multi-file store utils', () => {
});
describe('findEntry', () => {
- let state;
+ let localState;
beforeEach(() => {
- state = {
+ localState = {
tree: [{
type: 'tree',
name: 'test',
@@ -87,14 +104,14 @@ describe('Multi-file store utils', () => {
});
it('returns an entry found by name', () => {
- const foundEntry = utils.findEntry(state, 'tree', 'test');
+ const foundEntry = utils.findEntry(localState.tree, 'tree', 'test');
expect(foundEntry.type).toBe('tree');
expect(foundEntry.name).toBe('test');
});
it('returns undefined when no entry found', () => {
- const foundEntry = utils.findEntry(state, 'blob', 'test');
+ const foundEntry = utils.findEntry(localState.tree, 'blob', 'test');
expect(foundEntry).toBeUndefined();
});
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 5505f983d71..3267e29585b 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,8 +1,7 @@
/* 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 */
-/* global Sidebar */
import '~/commons/bootstrap';
-import '~/right_sidebar';
+import Sidebar from '~/right_sidebar';
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
@@ -41,7 +40,7 @@ import '~/right_sidebar';
loadFixtures(fixtureName);
this.sidebar = new Sidebar;
$aside = $('.right-sidebar');
- $page = $('.page-with-sidebar');
+ $page = $('.layout-page');
$icon = $aside.find('i');
$toggle = $aside.find('.js-sidebar-toggle');
return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index fdfc59a6f12..206f95abc1a 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,8 +1,9 @@
/* 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 */
import '~/gl_dropdown';
-import '~/search_autocomplete';
+import SearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
(function() {
var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -121,13 +122,13 @@ import '~/lib/utils/common_utils';
loadFixtures('static/search_autocomplete.html.raw');
// Prevent turbolinks from triggering within gl_dropdown
- spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
+ spyOn(urlUtils, 'visitUrl').and.returnValue(true);
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
- return widget = new gl.SearchAutocomplete;
+ return widget = new SearchAutocomplete();
});
afterEach(function() {
diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js
index 929ba75e67d..b97e24d9dcf 100644
--- a/spec/javascripts/sidebar/sidebar_assignees_spec.js
+++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js
@@ -4,20 +4,29 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('sidebar assignees', () => {
- let component;
- let SidebarAssigneeComponent;
+ let vm;
+ let mediator;
+ let sidebarAssigneesEl;
preloadFixtures('issues/open-issue.html.raw');
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- SidebarAssigneeComponent = Vue.extend(SidebarAssignees);
- spyOn(SidebarMediator.prototype, 'saveAssignees').and.callThrough();
- spyOn(SidebarMediator.prototype, 'assignYourself').and.callThrough();
- this.mediator = new SidebarMediator(Mock.mediator);
+
loadFixtures('issues/open-issue.html.raw');
- this.sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
+
+ mediator = new SidebarMediator(Mock.mediator);
+ spyOn(mediator, 'saveAssignees').and.callThrough();
+ spyOn(mediator, 'assignYourself').and.callThrough();
+
+ const SidebarAssigneeComponent = Vue.extend(SidebarAssignees);
+ sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
+ vm = mountComponent(SidebarAssigneeComponent, {
+ mediator,
+ field: sidebarAssigneesEl.dataset.field,
+ }, sidebarAssigneesEl);
});
afterEach(() => {
@@ -28,30 +37,24 @@ describe('sidebar assignees', () => {
});
it('calls the mediator when saves the assignees', () => {
- component = new SidebarAssigneeComponent()
- .$mount(this.sidebarAssigneesEl);
- component.saveAssignees();
-
- expect(SidebarMediator.prototype.saveAssignees).toHaveBeenCalled();
+ vm.saveAssignees();
+ expect(mediator.saveAssignees).toHaveBeenCalled();
});
it('calls the mediator when "assignSelf" method is called', () => {
- component = new SidebarAssigneeComponent()
- .$mount(this.sidebarAssigneesEl);
- component.assignSelf();
+ vm.assignSelf();
- expect(SidebarMediator.prototype.assignYourself).toHaveBeenCalled();
- expect(this.mediator.store.assignees.length).toEqual(1);
+ expect(mediator.assignYourself).toHaveBeenCalled();
+ expect(mediator.store.assignees.length).toEqual(1);
});
it('hides assignees until fetched', (done) => {
- component = new SidebarAssigneeComponent().$mount(this.sidebarAssigneesEl);
- const currentAssignee = this.sidebarAssigneesEl.querySelector('.value');
+ const currentAssignee = sidebarAssigneesEl.querySelector('.value');
expect(currentAssignee).toBe(null);
- component.store.isFetching.assignees = false;
+ vm.store.isFetching.assignees = false;
Vue.nextTick(() => {
- expect(component.$el.querySelector('.value')).toBeVisible();
+ expect(vm.$el.querySelector('.value')).toBeVisible();
done();
});
});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 14c34d5a78c..9efd109b996 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
@@ -85,12 +86,12 @@ describe('Sidebar mediator', () => {
const moveToProjectId = 7;
this.mediator.store.setMoveToProjectId(moveToProjectId);
spyOn(this.mediator.service, 'moveIssue').and.callThrough();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
this.mediator.moveIssue()
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
index 7adf22b0f1f..a6113cb0bae 100644
--- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -26,11 +26,14 @@ describe('Sidebar Subscriptions', function () {
});
it('calls the mediator toggleSubscription on event', () => {
- spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve());
- vm = mountComponent(SidebarSubscriptions, {});
+ const mediator = new SidebarMediator();
+ spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve());
+ vm = mountComponent(SidebarSubscriptions, {
+ mediator,
+ });
eventHub.$emit('toggleSubscription');
- expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled();
+ expect(mediator.toggleSubscription).toHaveBeenCalled();
});
});
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 946f98379ce..763a15e710b 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,44 +1,42 @@
/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
-import '~/syntax_highlight';
+import syntaxHighlight from '~/syntax_highlight';
-(function() {
- describe('Syntax Highlighter', function() {
- var stubUserColorScheme;
- stubUserColorScheme = function(value) {
- if (window.gon == null) {
- window.gon = {};
- }
- return window.gon.user_color_scheme = value;
- };
- describe('on a js-syntax-highlight element', function() {
- beforeEach(function() {
- return setFixtures('<div class="js-syntax-highlight"></div>');
- });
- return it('applies syntax highlighting', function() {
- stubUserColorScheme('monokai');
- $('.js-syntax-highlight').syntaxHighlight();
- return expect($('.js-syntax-highlight')).toHaveClass('monokai');
- });
+describe('Syntax Highlighter', function() {
+ var stubUserColorScheme;
+ stubUserColorScheme = function(value) {
+ if (window.gon == null) {
+ window.gon = {};
+ }
+ return window.gon.user_color_scheme = value;
+ };
+ describe('on a js-syntax-highlight element', function() {
+ beforeEach(function() {
+ return setFixtures('<div class="js-syntax-highlight"></div>');
});
- return describe('on a parent element', function() {
- beforeEach(function() {
- return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
- });
- it('applies highlighting to all applicable children', function() {
- stubUserColorScheme('monokai');
- $('.parent').syntaxHighlight();
- expect($('.parent, .foo')).not.toHaveClass('monokai');
- return expect($('.monokai').length).toBe(2);
- });
- return it('prevents an infinite loop when no matches exist', function() {
- var highlight;
- setFixtures('<div></div>');
- highlight = function() {
- return $('div').syntaxHighlight();
- };
- return expect(highlight).not.toThrow();
- });
+ return it('applies syntax highlighting', function() {
+ stubUserColorScheme('monokai');
+ syntaxHighlight($('.js-syntax-highlight'));
+ return expect($('.js-syntax-highlight')).toHaveClass('monokai');
});
});
-}).call(window);
+ return describe('on a parent element', function() {
+ beforeEach(function() {
+ return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
+ });
+ it('applies highlighting to all applicable children', function() {
+ stubUserColorScheme('monokai');
+ syntaxHighlight($('.parent'));
+ expect($('.parent, .foo')).not.toHaveClass('monokai');
+ return expect($('.monokai').length).toBe(2);
+ });
+ return it('prevents an infinite loop when no matches exist', function() {
+ var highlight;
+ setFixtures('<div></div>');
+ highlight = function() {
+ return syntaxHighlight($('div'));
+ };
+ return expect(highlight).not.toThrow();
+ });
+ });
+});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index fd7aa332d17..6897c991066 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -17,6 +17,12 @@ Vue.config.warnHandler = (msg, vm, trace) => {
fail(`${msg}${trace}`);
};
+let hasVueErrors = false;
+Vue.config.errorHandler = function (err) {
+ hasVueErrors = true;
+ fail(err);
+};
+
Vue.use(VueResource);
// enable test fixtures
@@ -72,7 +78,7 @@ testsContext.keys().forEach(function (path) {
describe('test errors', () => {
beforeAll((done) => {
- if (hasUnhandledPromiseRejections || hasVueWarnings) {
+ if (hasUnhandledPromiseRejections || hasVueWarnings || hasVueErrors) {
setTimeout(done, 1000);
} else {
done();
@@ -86,6 +92,10 @@ describe('test errors', () => {
it('has no Vue warnings', () => {
expect(hasVueWarnings).toBe(false);
});
+
+ it('has no Vue error', () => {
+ expect(hasVueErrors).toBe(false);
+ });
});
// if we're generating coverage reports, make sure to include all files so
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index 7d3c9319a11..59e16f0786e 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -1,3 +1,4 @@
+import * as urlUtils from '~/lib/utils/url_utility';
import Todos from '~/todos';
import '~/lib/utils/common_utils';
@@ -16,7 +17,7 @@ describe('Todos', () => {
it('opens the todo url', (done) => {
const todoLink = todoItem.dataset.url;
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(todoLink);
done();
});
@@ -31,7 +32,7 @@ describe('Todos', () => {
beforeEach(() => {
metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
- visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {});
+ visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {});
windowOpenSpy = spyOn(window, 'open').and.callFake(() => {});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
index 7ee998c8fce..6a59dc3c87e 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+import { getTimeago } from '~/lib/utils/datetime_utility';
const deploymentMockData = [
{
@@ -48,7 +50,7 @@ describe('MRWidgetDeployment', () => {
describe('formatDate', () => {
it('should work', () => {
- const readable = gl.utils.getTimeago().format(deployment.deployed_at);
+ const readable = getTimeago().format(deployment.deployed_at);
expect(vm.formatDate(deployment.deployed_at)).toEqual(readable);
});
});
@@ -93,10 +95,8 @@ describe('MRWidgetDeployment', () => {
const url = '/foo/bar';
const returnPromise = () => new Promise((resolve) => {
resolve({
- json() {
- return {
- redirect_url: url,
- };
+ data: {
+ redirect_url: url,
},
});
});
@@ -108,13 +108,13 @@ describe('MRWidgetDeployment', () => {
it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
- spyOn(gl.utils, 'visitUrl').and.returnValue(true);
+ spyOn(urlUtils, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(url);
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 2ae3adc1f93..07ed7f7f532 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -155,9 +155,7 @@ describe('MemoryUsage', () => {
describe('loadMetrics', () => {
const returnServicePromise = () => new Promise((resolve) => {
resolve({
- json() {
- return metricsMockData;
- },
+ data: metricsMockData,
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
new file mode 100644
index 00000000000..66ecaa316c8
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -0,0 +1,115 @@
+import Vue from 'vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Merge request widget rebase component', () => {
+ let Component;
+ let vm;
+ beforeEach(() => {
+ Component = Vue.extend(component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('While rebasing', () => {
+ it('should show progress message', () => {
+ vm = mountComponent(Component, {
+ mr: { rebaseInProgress: true },
+ service: {},
+ });
+
+ expect(
+ vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(),
+ ).toContain('Rebase in progress');
+ });
+ });
+
+ describe('With permissions', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {},
+ });
+ });
+
+ it('it should render rebase button and warning message', () => {
+ const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim();
+ expect(text).toContain('Fast-forward merge is not possible.');
+ expect(text).toContain('Rebase the source branch onto the target branch or merge target');
+ expect(text).toContain('branch into source branch to allow this merge request to be merged.');
+ });
+
+ it('it should render error message when it fails', (done) => {
+ vm.rebasingError = 'Something went wrong!';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(),
+ ).toContain('Something went wrong!');
+ done();
+ });
+ });
+ });
+
+ describe('Without permissions', () => {
+ it('should render a message explaining user does not have permissions', () => {
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: 'foo',
+ },
+ service: {},
+ });
+
+ const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim();
+
+ expect(text).toContain('Fast-forward merge is not possible.');
+ expect(text).toContain('Rebase the source branch onto');
+ expect(text).toContain('foo');
+ expect(text).toContain('to allow this merge request to be merged.');
+ });
+ });
+
+ describe('methods', () => {
+ it('checkRebaseStatus', (done) => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(Component, {
+ mr: {},
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ merge_error: null,
+ },
+ });
+ },
+ },
+ });
+
+ vm.rebase();
+
+ // Wait for the rebase request
+ vm.$nextTick()
+ // Wait for the polling request
+ .then(vm.$nextTick())
+ // Wait for the eventHub to be called
+ .then(vm.$nextTick())
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
index d23b558f4ea..1bf97bbf093 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -4,13 +4,16 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid
const mr = {
targetBranch: 'good-branch',
targetBranchPath: '/good-branch',
- closedEvent: {
- author: {
+ metrics: {
+ mergedBy: {},
+ mergedAt: 'mergedUpdatedAt',
+ closedBy: {
name: 'Fatih Acet',
username: 'fatihacet',
},
- updatedAt: 'closedEventUpdatedAt',
- formattedUpdatedAt: '',
+ closedAt: 'closedEventUpdatedAt',
+ readableMergedAt: '',
+ readableClosedAt: '',
},
updatedAt: 'mrUpdatedAt',
closedAt: '1 day ago',
@@ -56,7 +59,7 @@ describe('MRWidgetClosed', () => {
it('should have correct elements', () => {
expect(el.querySelector('h4').textContent).toContain('Closed by');
- expect(el.querySelector('h4').textContent).toContain(mr.closedEvent.author.name);
+ expect(el.querySelector('h4').textContent).toContain(mr.metrics.closedBy.name);
expect(el.textContent).toContain('The changes were not merged into');
expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
index 9a71d0b47d7..5f4df15bcd6 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
@@ -108,9 +108,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(new Promise((resolve) => {
resolve({
- json() {
- return mrObj;
- },
+ data: mrObj,
});
}));
@@ -129,10 +127,8 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
spyOn(eventHub, '$emit');
spyOn(vm.service.mergeResource, 'save').and.returnValue(new Promise((resolve) => {
resolve({
- json() {
- return {
- status: 'merge_when_pipeline_succeeds',
- };
+ data: {
+ status: 'merge_when_pipeline_succeeds',
},
});
}));
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 2714e8294fa..2dc3b72ea40 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -14,10 +14,13 @@ const createComponent = () => {
canRevertInCurrentMR: true,
canRemoveSourceBranch: true,
sourceBranchRemoved: true,
- mergedEvent: {
- author: {},
- updatedAt: 'mergedUpdatedAt',
- formattedUpdatedAt: '',
+ metrics: {
+ mergedBy: {},
+ mergedAt: 'mergedUpdatedAt',
+ readableMergedAt: '',
+ closedBy: {},
+ closedAt: 'mergedUpdatedAt',
+ readableClosedAt: '',
},
updatedAt: 'mrUpdatedAt',
targetBranch,
@@ -111,10 +114,8 @@ describe('MRWidgetMerged', () => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => {
resolve({
- json() {
- return {
- message: 'Branch was removed',
- };
+ data: {
+ message: 'Branch was removed',
},
});
}));
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index df3d29ee1f9..1127576617b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -292,8 +292,8 @@ describe('MRWidgetReadyToMerge', () => {
describe('handleMergeButtonClick', () => {
const returnPromise = status => new Promise((resolve) => {
resolve({
- json() {
- return { status };
+ data: {
+ status,
},
});
});
@@ -364,8 +364,9 @@ describe('MRWidgetReadyToMerge', () => {
describe('handleMergePolling', () => {
const returnPromise = state => new Promise((resolve) => {
resolve({
- json() {
- return { state, source_branch_exists: true };
+ data: {
+ state,
+ source_branch_exists: true,
},
});
});
@@ -422,8 +423,8 @@ describe('MRWidgetReadyToMerge', () => {
describe('handleRemoveBranchPolling', () => {
const returnPromise = state => new Promise((resolve) => {
resolve({
- json() {
- return { source_branch_exists: state };
+ data: {
+ source_branch_exists: state,
},
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 2cb3aaa6951..98ab61a0367 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -50,9 +50,7 @@ describe('MRWidgetWIP', () => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeWIP').and.returnValue(new Promise((resolve) => {
resolve({
- json() {
- return mrObj;
- },
+ data: mrObj,
});
}));
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 1ad7c2d8efa..ca29c9fee32 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -33,8 +33,8 @@ export default {
"source_project_id": 19,
"target_branch": "master",
"target_project_id": 19,
- "merge_event": {
- "author": {
+ "metrics": {
+ "merged_by": {
"name": "Administrator",
"username": "root",
"id": 1,
@@ -42,9 +42,10 @@ export default {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
- "updated_at": "2017-04-07T15:39:25.696Z"
+ "merged_at": "2017-04-07T15:39:25.696Z",
+ "closed_by": null,
+ "closed_at": null
},
- "closed_event": null,
"author": {
"name": "Administrator",
"username": "root",
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 9e6d0aa472c..cd00d0a39a3 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -2,15 +2,13 @@ import Vue from 'vue';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
+import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
const returnPromise = data => new Promise((resolve) => {
resolve({
- json() {
- return data;
- },
- body: data,
+ data,
});
});
@@ -344,4 +342,31 @@ describe('mrWidgetOptions', () => {
expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined();
});
});
+
+ describe('rendering relatedLinks', () => {
+ beforeEach((done) => {
+ vm.mr.relatedLinks = {
+ assignToMe: null,
+ closing: `
+ <a class="close-related-link" href="#'>
+ Close
+ </a>
+ `,
+ mentioned: '',
+ };
+ Vue.nextTick(done);
+ });
+
+ it('renders if there are relatedLinks', () => {
+ expect(vm.$el.querySelector('.close-related-link')).toBeDefined();
+ });
+
+ it('does not render if state is nothingToMerge', (done) => {
+ vm.mr.state = stateKey.nothingToMerge;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.close-related-link')).toBeNull();
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index 8e5614b20f0..33d052aceb2 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -1,4 +1,5 @@
import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
+import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from '../mock_data';
describe('MergeRequestStore', () => {
@@ -52,5 +53,17 @@ describe('MergeRequestStore', () => {
expect(store.isPipelineSkipped).toBe(false);
});
});
+
+ describe('isNothingToMergeState', () => {
+ it('returns true when nothingToMerge', () => {
+ store.state = stateKey.nothingToMerge;
+ expect(store.isNothingToMergeState).toEqual(true);
+ });
+
+ it('returns false when not nothingToMerge', () => {
+ store.state = 'state';
+ expect(store.isNothingToMergeState).toEqual(false);
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index d5754aaa9e7..fdead874209 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -10,7 +10,7 @@ describe('Commit component', () => {
CommitComponent = Vue.extend(commitComp);
});
- it('should render a code-fork icon if it does not represent a tag', () => {
+ it('should render a fork icon if it does not represent a tag', () => {
component = new CommitComponent({
propsData: {
tag: false,
@@ -30,7 +30,7 @@ describe('Commit component', () => {
},
}).$mount();
- expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
+ expect(component.$el.querySelector('.icon-container').children).toContain('svg');
});
describe('Given all the props', () => {
diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js
new file mode 100644
index 00000000000..a33ab689dd1
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/expand_button_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import expandButton from '~/vue_shared/components/expand_button.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('expand button', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(expandButton);
+ vm = mountComponent(Component, {
+ slots: {
+ expanded: '<p>Expanded!</p>',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a collpased button', () => {
+ expect(vm.$el.textContent.trim()).toEqual('...');
+ });
+
+ it('hides expander on click', (done) => {
+ vm.$el.querySelector('button').click();
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;');
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js
new file mode 100644
index 00000000000..d99b17bdc79
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/file_icon_spec.js
@@ -0,0 +1,83 @@
+import Vue from 'vue';
+import fileIcon from '~/vue_shared/components/file_icon.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('File Icon component', () => {
+ let vm;
+ let FileIcon;
+
+ beforeEach(() => {
+ FileIcon = Vue.extend(fileIcon);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render a span element with an svg', () => {
+ vm = mountComponent(FileIcon, {
+ fileName: 'test.js',
+ });
+
+ expect(vm.$el.tagName).toEqual('SPAN');
+ expect(vm.$el.querySelector('span > svg')).toBeDefined();
+ });
+
+ it('should render a javascript icon based on file ending', () => {
+ vm = mountComponent(FileIcon, {
+ fileName: 'test.js',
+ });
+
+ expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#javascript`);
+ });
+
+ it('should render a image icon based on file ending', () => {
+ vm = mountComponent(FileIcon, {
+ fileName: 'test.png',
+ });
+
+ expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#image`);
+ });
+
+ it('should render a webpack icon based on file namer', () => {
+ vm = mountComponent(FileIcon, {
+ fileName: 'webpack.js',
+ });
+
+ expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#webpack`);
+ });
+
+ it('should render a standard folder icon', () => {
+ vm = mountComponent(FileIcon, {
+ fileName: 'js',
+ folder: true,
+ });
+
+ expect(vm.$el.querySelector('span > svg > use').getAttribute('xlink:href')).toBe(`${gon.sprite_file_icons}#folder`);
+ });
+
+ it('should render a loading icon', () => {
+ vm = mountComponent(FileIcon, {
+ fileName: 'test.js',
+ loading: true,
+ });
+
+ expect(
+ vm.$el.querySelector('i').getAttribute('class'),
+ ).toEqual('fa fa-spin fa-spinner fa-1x');
+ });
+
+ it('should add a special class and a size class', () => {
+ vm = mountComponent(FileIcon, {
+ fileName: 'test.js',
+ cssClasses: 'extraclasses',
+ size: 120,
+ });
+
+ const classList = vm.$el.firstChild.classList;
+ const containsSizeClass = classList.contains('s120');
+ const containsCustomClass = classList.contains('extraclasses');
+ expect(containsSizeClass).toBe(true);
+ expect(containsCustomClass).toBe(true);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index b4553acb341..b378a0bd896 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Header CI Component', () => {
let HeaderCi;
@@ -8,7 +9,6 @@ describe('Header CI Component', () => {
beforeEach(() => {
HeaderCi = Vue.extend(headerCi);
-
props = {
status: {
group: 'failed',
@@ -45,54 +45,65 @@ describe('Header CI Component', () => {
],
hasSidebarButton: true,
};
-
- vm = new HeaderCi({
- propsData: props,
- }).$mount();
});
afterEach(() => {
vm.$destroy();
});
- it('should render status badge', () => {
- expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
- expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
- expect(
- vm.$el.querySelector('.ci-failed').getAttribute('href'),
- ).toEqual(props.status.details_path);
- });
+ describe('render', () => {
+ beforeEach(() => {
+ vm = mountComponent(HeaderCi, props);
+ });
- it('should render item name and id', () => {
- expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
- });
+ it('should render status badge', () => {
+ expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
+ expect(
+ vm.$el.querySelector('.ci-failed').getAttribute('href'),
+ ).toEqual(props.status.details_path);
+ });
- it('should render timeago date', () => {
- expect(vm.$el.querySelector('time')).toBeDefined();
- });
+ it('should render item name and id', () => {
+ expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
+ });
- it('should render user icon and name', () => {
- expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
- });
+ it('should render timeago date', () => {
+ expect(vm.$el.querySelector('time')).toBeDefined();
+ });
- it('should render provided actions', () => {
- expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
- expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
- expect(vm.$el.querySelector('.link').tagName).toEqual('A');
- expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
- expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
- });
+ it('should render user icon and name', () => {
+ expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+ });
+
+ it('should render provided actions', () => {
+ expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
+ expect(vm.$el.querySelector('.link').tagName).toEqual('A');
+ expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
+ expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
+ });
- it('should show loading icon', (done) => {
- vm.actions[0].isLoading = true;
+ it('should show loading icon', (done) => {
+ vm.actions[0].isLoading = true;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy();
- done();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy();
+ done();
+ });
+ });
+
+ it('should render sidebar toggle button', () => {
+ expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined();
});
});
- it('should render sidebar toggle button', () => {
- expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined();
+ describe('shouldRenderTriggeredLabel', () => {
+ it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => {
+ vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false });
+
+ expect(vm.$el.textContent).toContain('created');
+ expect(vm.$el.textContent).not.toContain('triggered');
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js
new file mode 100644
index 00000000000..fe75a86cac8
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/modal_spec.js
@@ -0,0 +1,66 @@
+import Vue from 'vue';
+import modal from '~/vue_shared/components/modal.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const modalComponent = Vue.extend(modal);
+
+describe('Modal', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('props', () => {
+ describe('without primaryButtonLabel', () => {
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, {
+ primaryButtonLabel: null,
+ });
+ });
+
+ it('does not render a primary button', () => {
+ expect(vm.$el.querySelector('.js-primary-button')).toBeNull();
+ });
+ });
+
+ describe('with id', () => {
+ it('does not render a primary button', () => {
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, {
+ id: 'my-modal',
+ });
+ });
+
+ it('assigns the id to the modal', () => {
+ expect(vm.$el.querySelector('#my-modal.modal')).not.toBeNull();
+ });
+
+ it('does not show the modal immediately', () => {
+ expect(vm.$el.querySelector('#my-modal.modal')).not.toHaveClass('show');
+ });
+
+ it('does not show a backdrop', () => {
+ expect(vm.$el.querySelector('modal-backdrop')).toBeNull();
+ });
+ });
+ });
+
+ it('works with data-toggle="modal"', (done) => {
+ setFixtures(`
+ <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
+ <div id="modal-container"></div>
+ `);
+
+ const modalContainer = document.getElementById('modal-container');
+ const modalButton = document.getElementById('modal-button');
+ vm = mountComponent(modalComponent, {
+ id: 'my-modal',
+ }, modalContainer);
+ const modalElement = vm.$el.querySelector('#my-modal');
+ $(modalElement).on('shown.bs.modal', () => done());
+
+ modalButton.click();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
new file mode 100644
index 00000000000..70ce3dffaba
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
@@ -0,0 +1,59 @@
+import Vue from 'vue';
+import panelResizer from '~/vue_shared/components/panel_resizer.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Panel Resizer component', () => {
+ let vm;
+ let PanelResizer;
+
+ 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);
+ };
+
+ beforeEach(() => {
+ PanelResizer = Vue.extend(panelResizer);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render a div element with the correct classes and styles', () => {
+ vm = mountComponent(PanelResizer, {
+ startSize: 100,
+ side: 'left',
+ });
+
+ expect(vm.$el.tagName).toEqual('DIV');
+ expect(vm.$el.getAttribute('class')).toBe('dragHandle dragleft');
+ expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;');
+ });
+
+ it('should render a div element with the correct classes for a right side panel', () => {
+ vm = mountComponent(PanelResizer, {
+ startSize: 100,
+ side: 'right',
+ });
+
+ expect(vm.$el.tagName).toEqual('DIV');
+ expect(vm.$el.getAttribute('class')).toBe('dragHandle dragright');
+ });
+
+ it('drag the resizer', () => {
+ vm = mountComponent(PanelResizer, {
+ startSize: 100,
+ side: 'left',
+ });
+
+ spyOn(vm, '$emit');
+ triggerEvent('mousedown', vm.$el);
+ triggerEvent('mousemove', document);
+ triggerEvent('mouseup', document);
+ expect(vm.$emit.calls.allArgs()).toEqual([['resize-start', 100], ['update:size', 100], ['resize-end', 100]]);
+ expect(vm.size).toBe(100);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index b0b78e34e0f..1465ef5855f 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -19,6 +19,22 @@ describe('Pagination component', () => {
});
describe('render', () => {
+ it('should not render anything', () => {
+ component = mountComponet({
+ pageInfo: {
+ nextPage: 1,
+ page: 1,
+ perPage: 20,
+ previousPage: null,
+ total: 15,
+ totalPages: 1,
+ },
+ change: spy,
+ });
+
+ expect(component.$el.innerHTML).not.toBeDefined();
+ });
+
describe('prev button', () => {
it('should be disabled and non clickable', () => {
component = mountComponet({
diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
index b4c1f70ed1e..b4fb568f1d4 100644
--- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import '~/lib/utils/datetime_utility';
+import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
describe('Time ago with tooltip component', () => {
let TimeagoTooltip;
@@ -24,10 +24,10 @@ describe('Time ago with tooltip component', () => {
expect(vm.$el.tagName).toEqual('TIME');
expect(
vm.$el.getAttribute('data-original-title'),
- ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z'));
+ ).toEqual(formatDate('2017-05-08T14:57:39.781Z'));
expect(vm.$el.getAttribute('data-placement')).toEqual('top');
- const timeago = gl.utils.getTimeago();
+ const timeago = getTimeago();
expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z'));
});
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js
index 447d74d4e08..859995d33fa 100644
--- a/spec/javascripts/vue_shared/components/toggle_button_spec.js
+++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js
@@ -30,9 +30,9 @@ describe('Toggle Button', () => {
expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
});
- it('renders Enabled and Disabled text data attributes', () => {
- expect(vm.$el.querySelector('button').getAttribute('data-enabled-text')).toEqual('Enabled');
- expect(vm.$el.querySelector('button').getAttribute('data-disabled-text')).toEqual('Disabled');
+ it('renders input status icon', () => {
+ expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1);
+ expect(vm.$el.querySelectorAll('svg.s16.toggle-icon-svg').length).toEqual(1);
});
});
@@ -49,6 +49,14 @@ describe('Toggle Button', () => {
expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
});
+ it('sets aria-label representing toggle state', () => {
+ vm.value = true;
+ expect(vm.ariaLabel).toEqual('Toggle Status: ON');
+
+ vm.value = false;
+ expect(vm.ariaLabel).toEqual('Toggle Status: OFF');
+ });
+
it('emits change event when clicked', () => {
vm.$el.querySelector('button').click();
diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb
index 532d25e121d..f6474c8936d 100644
--- a/spec/lib/banzai/filter/mermaid_filter_spec.rb
+++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
describe Banzai::Filter::MermaidFilter do
include FilterSpecHelper
- it 'adds `js-render-mermaid` class to the `pre` tag' do
+ it 'adds `js-render-mermaid` class to the `code` tag' do
doc = filter("<pre class='code highlight js-syntax-highlight mermaid' lang='mermaid' v-pre='true'><code>graph TD;\n A--&gt;B;\n</code></pre>")
- result = doc.xpath('descendant-or-self::pre').first
+ result = doc.css('code').first
expect(result[:class]).to include('js-render-mermaid')
end
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index 68643effb66..5a7858e77f3 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.team << [user, :master]
+ project.add_master(user)
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
@@ -94,7 +94,7 @@ describe Banzai::Filter::RedactorFilter do
it 'removes references for project members with guest role' do
member = create(:user)
project = create(:project, :public)
- project.team << [member, :guest]
+ project.add_guest(member)
issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
@@ -128,7 +128,7 @@ describe Banzai::Filter::RedactorFilter do
it 'allows references for project members' do
member = create(:user)
project = create(:project, :public)
- project.team << [member, :developer]
+ project.add_developer(member)
issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 08beede62db..f38f0776303 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -5,6 +5,7 @@ describe Banzai::Filter::RelativeLinkFilter do
contexts.reverse_merge!({
commit: commit,
project: project,
+ group: group,
project_wiki: project_wiki,
ref: ref,
requested_path: requested_path
@@ -25,7 +26,12 @@ describe Banzai::Filter::RelativeLinkFilter do
%(<a href="#{path}">#{path}</a>)
end
+ def nested(element)
+ %(<div>#{element}</div>)
+ end
+
let(:project) { create(:project, :repository) }
+ let(:group) { nil }
let(:project_path) { project.full_path }
let(:ref) { 'markdown' }
let(:commit) { project.commit(ref) }
@@ -70,6 +76,11 @@ describe Banzai::Filter::RelativeLinkFilter do
expect { filter(act) }.not_to raise_error
end
+ it 'does not raise an exception with a garbled path' do
+ act = link("open(/var/tmp/):%20/location%0Afrom:%20/test")
+ expect { filter(act) }.not_to raise_error
+ end
+
it 'ignores ref if commit is passed' do
doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') )
expect(doc.at_css('a')['href'])
@@ -223,4 +234,79 @@ describe Banzai::Filter::RelativeLinkFilter do
let(:commit) { nil } # force filter to use ref instead of commit
include_examples :valid_repository
end
+
+ context 'with a /upload/ URL' do
+ # not needed
+ let(:commit) { nil }
+ let(:ref) { nil }
+ let(:requested_path) { nil }
+
+ context 'to a project upload' do
+ it 'rebuilds relative URL for a link' do
+ doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('a')['href'])
+ .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = filter(nested(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')))
+ expect(doc.at_css('a')['href'])
+ .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ end
+
+ it 'rebuilds relative URL for an image' do
+ doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('img')['src'])
+ .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = filter(nested(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')))
+ expect(doc.at_css('img')['src'])
+ .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+
+ it 'supports Unicode filenames' do
+ path = '/uploads/한글.png'
+ escaped = Addressable::URI.escape(path)
+
+ # Stub these methods so the file doesn't actually need to be in the repo
+ allow_any_instance_of(described_class)
+ .to receive(:file_exists?).and_return(true)
+ allow_any_instance_of(described_class)
+ .to receive(:image?).with(path).and_return(true)
+
+ doc = filter(image(escaped))
+ expect(doc.at_css('img')['src']).to match "/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png"
+ end
+ end
+
+ context 'to a group upload' do
+ let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
+ let(:group) { create(:group) }
+ let(:project) { nil }
+ let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" }
+
+ it 'rewrites the link correctly' do
+ doc = filter(upload_link)
+
+ expect(doc.at_css('a')['href']).to eq(relative_path)
+ end
+
+ it 'rewrites the link correctly for subgroup' do
+ group.update!(parent: create(:group))
+
+ doc = filter(upload_link)
+
+ expect(doc.at_css('a')['href']).to eq(relative_path)
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 85eddde732e..0cfef4ff5bf 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -65,6 +65,13 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(doc.css('h2 a').first.attr('href')).to eq '#one-1'
end
+ it 'prepends a prefix to digits-only ids' do
+ doc = filter(header(1, "123") + header(2, "1.0"))
+
+ expect(doc.css('h1 a').first.attr('href')).to eq '#anchor-123'
+ expect(doc.css('h2 a').first.attr('href')).to eq '#anchor-10'
+ end
+
it 'supports Unicode' do
doc = filter(header(1, '한글'))
expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글'
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
deleted file mode 100644
index 76bc0c36ab7..00000000000
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ /dev/null
@@ -1,133 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::UploadLinkFilter do
- def filter(doc, contexts = {})
- contexts.reverse_merge!({
- project: project
- })
-
- raw_filter(doc, contexts)
- end
-
- def raw_filter(doc, contexts = {})
- described_class.call(doc, contexts)
- end
-
- def image(path)
- %(<img src="#{path}" />)
- end
-
- def link(path)
- %(<a href="#{path}">#{path}</a>)
- end
-
- def nested_image(path)
- %(<div><img src="#{path}" /></div>)
- end
-
- def nested_link(path)
- %(<div><a href="#{path}">#{path}</a></div>)
- end
-
- let(:project) { create(:project) }
-
- shared_examples :preserve_unchanged do
- it 'does not modify any relative URL in anchor' do
- doc = filter(link('README.md'))
- expect(doc.at_css('a')['href']).to eq 'README.md'
- end
-
- it 'does not modify any relative URL in image' do
- doc = filter(image('files/images/logo-black.png'))
- expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
- end
- end
-
- it 'does not raise an exception on invalid URIs' do
- act = link("://foo")
- expect { filter(act) }.not_to raise_error
- end
-
- context 'with a valid repository' do
- it 'rebuilds relative URL for a link' do
- doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('a')['href'])
- .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
-
- doc = filter(nested_link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('a')['href'])
- .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
- end
-
- it 'rebuilds relative URL for an image' do
- doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('img')['src'])
- .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
-
- doc = filter(nested_image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('img')['src'])
- .to eq "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
- end
-
- it 'does not modify absolute URL' do
- doc = filter(link('http://example.com'))
- expect(doc.at_css('a')['href']).to eq 'http://example.com'
- end
-
- it 'supports Unicode filenames' do
- path = '/uploads/한글.png'
- escaped = Addressable::URI.escape(path)
-
- # Stub these methods so the file doesn't actually need to be in the repo
- allow_any_instance_of(described_class)
- .to receive(:file_exists?).and_return(true)
- allow_any_instance_of(described_class)
- .to receive(:image?).with(path).and_return(true)
-
- doc = filter(image(escaped))
- expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png"
- end
- end
-
- context 'in group context' do
- let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
- let(:group) { create(:group) }
- let(:filter_context) { { project: nil, group: group } }
- let(:relative_path) { "groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" }
-
- it 'rewrites the link correctly' do
- doc = raw_filter(upload_link, filter_context)
-
- expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}")
- end
-
- it 'rewrites the link correctly for subgroup' do
- subgroup = create(:group, parent: group)
- relative_path = "groups/#{subgroup.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
-
- doc = raw_filter(upload_link, { project: nil, group: subgroup })
-
- expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}")
- end
-
- it 'does not modify absolute URL' do
- doc = filter(link('http://example.com'), filter_context)
-
- expect(doc.at_css('a')['href']).to eq 'http://example.com'
- end
- end
-
- context 'when project or group context does not exist' do
- let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
-
- it 'does not raise error' do
- expect { raw_filter(upload_link, project: nil) }.not_to raise_error
- end
-
- it 'does not rewrite link' do
- doc = raw_filter(upload_link, project: nil)
-
- expect(doc.to_html).to eq upload_link
- end
- end
-end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index fc03741976e..c76adc262fc 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -34,11 +34,11 @@ describe Banzai::Filter::UserReferenceFilter do
let(:reference) { User.reference_prefix + 'all' }
before do
- project.team << [project.creator, :developer]
+ project.add_developer(project.creator)
end
it 'supports a special @all mention' do
- project.team << [user, :developer]
+ project.add_developer(user)
doc = reference_filter("Hey #{reference}", author: user)
expect(doc.css('a').length).to eq 1
@@ -47,7 +47,7 @@ describe Banzai::Filter::UserReferenceFilter do
end
it 'includes a data-author attribute when there is an author' do
- project.team << [user, :developer]
+ project.add_developer(user)
doc = reference_filter(reference, author: user)
expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index e49726aca6c..b079a3be029 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -63,8 +63,8 @@ describe Banzai::ReferenceParser::UserParser do
let(:contributor) { create(:user) }
before do
- project.team << [user, :developer]
- project.team << [contributor, :developer]
+ project.add_developer(user)
+ project.add_developer(contributor)
end
it 'returns the members of a project' do
@@ -162,7 +162,7 @@ describe Banzai::ReferenceParser::UserParser do
context 'when the link has a data-author attribute' do
it 'returns the nodes when the user is a member of the project' do
other_project = create(:project)
- other_project.team << [user, :developer]
+ other_project.add_developer(user)
link['data-project'] = other_project.id.to_s
link['data-author'] = user.id.to_s
diff --git a/spec/lib/gitlab/action_rate_limiter_spec.rb b/spec/lib/gitlab/action_rate_limiter_spec.rb
new file mode 100644
index 00000000000..542fc03e555
--- /dev/null
+++ b/spec/lib/gitlab/action_rate_limiter_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::ActionRateLimiter do
+ let(:redis) { double('redis') }
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:key) { [user, project] }
+ let(:cache_key) { "action_rate_limiter:test_action:user:#{user.id}:project:#{project.id}" }
+
+ subject { described_class.new(action: :test_action, expiry_time: 100) }
+
+ before do
+ allow(Gitlab::Redis::Cache).to receive(:with).and_yield(redis)
+ end
+
+ it 'increases the throttle count and sets the expire time' do
+ expect(redis).to receive(:incr).with(cache_key).and_return(1)
+ expect(redis).to receive(:expire).with(cache_key, 100)
+
+ expect(subject.throttled?(key, 1)).to be false
+ end
+
+ it 'returns true if the key is throttled' do
+ expect(redis).to receive(:incr).with(cache_key).and_return(2)
+ expect(redis).not_to receive(:expire)
+
+ expect(subject.throttled?(key, 1)).to be true
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
index 79d2c071446..e1c4f9cfea7 100644
--- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -2,19 +2,20 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do
let(:migration) { described_class.new }
+ let(:projects) { table(:projects) }
- let(:base1) { create(:project) }
- let(:base1_fork1) { create(:project) }
- let(:base1_fork2) { create(:project) }
+ let(:base1) { projects.create }
+ let(:base1_fork1) { projects.create }
+ let(:base1_fork2) { projects.create }
- let(:base2) { create(:project) }
- let(:base2_fork1) { create(:project) }
- let(:base2_fork2) { create(:project) }
+ let(:base2) { projects.create }
+ let(:base2_fork1) { projects.create }
+ let(:base2_fork2) { projects.create }
- let(:fork_of_fork) { create(:project) }
- let(:fork_of_fork2) { create(:project) }
- let(:second_level_fork) { create(:project) }
- let(:third_level_fork) { create(:project) }
+ let(:fork_of_fork) { projects.create }
+ let(:fork_of_fork2) { projects.create }
+ let(:second_level_fork) { projects.create }
+ let(:third_level_fork) { projects.create }
let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) }
let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) }
@@ -97,7 +98,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
end
it 'does not miss members for forks of forks for which the root was deleted' do
- forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id)
+ forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: projects.create.id)
base1.destroy
expect(migration.missing_members?(7, 10)).to be_falsy
@@ -105,8 +106,8 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
context 'with more forks' do
before do
- forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
- forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
+ forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id)
+ forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id)
end
it 'only processes a single batch of links at a time' do
diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
index 5c471cbdeda..9bae7e53b71 100644
--- a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
@@ -24,17 +24,12 @@ describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :mig
redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
end
- it 'deletes the conflicting redirect_routes in the range' do
+ # No-op. See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252
+ it 'NO-OP: does not delete any redirect_routes' do
expect(redirect_routes.count).to eq(8)
- expect do
- described_class.new.perform(1, 3)
- end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2)
+ described_class.new.perform(1, 5)
- expect do
- described_class.new.perform(4, 5)
- end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0)
-
- expect(redirect_routes.count).to eq(3)
+ expect(redirect_routes.count).to eq(8)
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
index cb52d971047..5432d270555 100644
--- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
@@ -225,9 +225,10 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati
let(:user_class) { table(:users) }
let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) }
let(:namespace) { create(:namespace, owner: author) }
- let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) }
+ let(:projects) { table(:projects) }
+ let(:project) { projects.create(namespace_id: namespace.id, creator_id: author.id) }
- # We can not rely on FactoryGirl as the state of Event may change in ways that
+ # We can not rely on FactoryBot as the state of Event may change in ways that
# the background migration does not expect, hence we use the Event class of
# the migration itself.
def create_push_event(project, author, data = nil)
@@ -280,6 +281,17 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati
migration.process_event(event)
end
+
+ it 'handles an error gracefully' do
+ event1 = create_push_event(project, author, { commits: [] })
+
+ expect(migration).to receive(:replicate_event).and_call_original
+ expect(migration).to receive(:create_push_event_payload).and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ migration.process_event(event1)
+
+ expect(described_class::EventForMigration.all.count).to eq(0)
+ end
end
describe '#replicate_event' do
@@ -334,9 +346,8 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati
it 'does not create push event payloads for removed events' do
allow(event).to receive(:id).and_return(-1)
- payload = migration.create_push_event_payload(event)
+ expect { migration.create_push_event_payload(event) }.to raise_error(ActiveRecord::InvalidForeignKey)
- expect(payload).to be_nil
expect(PushEventPayload.count).to eq(0)
end
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
index e52baf8dde7..8582af96199 100644
--- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do
let(:migration) { described_class.new }
- let(:base1) { create(:project) }
+ let(:projects) { table(:projects) }
+ let(:base1) { projects.create }
- let(:base2) { create(:project) }
- let(:base2_fork1) { create(:project) }
+ let(:base2) { projects.create }
+ let(:base2_fork1) { projects.create }
let!(:forked_project_links) { table(:forked_project_links) }
let!(:fork_networks) { table(:fork_networks) }
@@ -18,10 +19,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
# A normal fork link
forked_project_links.create(id: 1,
forked_from_project_id: base1.id,
- forked_to_project_id: create(:project).id)
+ forked_to_project_id: projects.create.id)
forked_project_links.create(id: 2,
forked_from_project_id: base1.id,
- forked_to_project_id: create(:project).id)
+ forked_to_project_id: projects.create.id)
forked_project_links.create(id: 3,
forked_from_project_id: base2.id,
forked_to_project_id: base2_fork1.id)
@@ -29,10 +30,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
# create a fork of a fork
forked_project_links.create(id: 4,
forked_from_project_id: base2_fork1.id,
- forked_to_project_id: create(:project).id)
+ forked_to_project_id: projects.create.id)
forked_project_links.create(id: 5,
- forked_from_project_id: create(:project).id,
- forked_to_project_id: create(:project).id)
+ forked_from_project_id: projects.create.id,
+ forked_to_project_id: projects.create.id)
# Stub out the calls to the other migrations
allow(BackgroundMigrationWorker).to receive(:perform_in)
@@ -63,7 +64,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
end
it 'creates a fork network for the fork of which the source was deleted' do
- fork = create(:project)
+ fork = projects.create
forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: fork.id)
migration.perform(5, 8)
diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb
new file mode 100644
index 00000000000..dfe3b31f1c0
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb
@@ -0,0 +1,124 @@
+require 'rails_helper'
+
+describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, :migration, schema: 20171128214150 do
+ describe '#perform' do
+ let(:mr_with_event) { create(:merge_request) }
+ let!(:merged_event) { create(:event, :merged, target: mr_with_event) }
+ let!(:closed_event) { create(:event, :closed, target: mr_with_event) }
+
+ before do
+ # Make sure no metrics are created and kept through after_* callbacks.
+ mr_with_event.metrics.destroy!
+ end
+
+ it 'inserts metrics and updates closed and merged events' do
+ subject.perform(mr_with_event.id, mr_with_event.id)
+
+ mr_with_event.reload
+
+ expect(mr_with_event.metrics).to have_attributes(latest_closed_by_id: closed_event.author_id,
+ merged_by_id: merged_event.author_id)
+ expect(mr_with_event.metrics.latest_closed_at.to_s).to eq(closed_event.updated_at.to_s)
+ end
+ end
+
+ describe '#insert_metrics_for_range' do
+ let!(:mrs_without_metrics) { create_list(:merge_request, 3) }
+ let!(:mrs_with_metrics) { create_list(:merge_request, 2) }
+
+ before do
+ # Make sure no metrics are created and kept through after_* callbacks.
+ mrs_without_metrics.each { |m| m.metrics.destroy! }
+ end
+
+ it 'inserts merge_request_metrics for merge_requests without one' do
+ expect { subject.insert_metrics_for_range(MergeRequest.first.id, MergeRequest.last.id) }
+ .to change(MergeRequest::Metrics, :count).from(2).to(5)
+
+ mrs_without_metrics.each do |mr_without_metrics|
+ expect(mr_without_metrics.reload.metrics).to be_present
+ end
+ end
+
+ it 'does not inserts merge_request_metrics for MRs out of given range' do
+ expect { subject.insert_metrics_for_range(mrs_with_metrics.first.id, mrs_with_metrics.last.id) }
+ .not_to change(MergeRequest::Metrics, :count).from(2)
+ end
+ end
+
+ describe '#update_metrics_with_events_data' do
+ context 'closed events data update' do
+ let(:users) { create_list(:user, 3) }
+ let(:mrs_with_event) { create_list(:merge_request, 3) }
+
+ before do
+ create_list(:event, 2, :closed, author: users.first, target: mrs_with_event.first)
+ create_list(:event, 3, :closed, author: users.second, target: mrs_with_event.second)
+ create(:event, :closed, author: users.third, target: mrs_with_event.third)
+ end
+
+ it 'migrates multiple MR metrics with closed event data' do
+ mr_without_event = create(:merge_request)
+ create(:event, :merged)
+
+ subject.update_metrics_with_events_data(mrs_with_event.first.id, mrs_with_event.last.id)
+
+ mrs_with_event.each do |mr_with_event|
+ latest_event = Event.where(action: 3, target: mr_with_event).last
+
+ mr_with_event.metrics.reload
+
+ expect(mr_with_event.metrics.latest_closed_by).to eq(latest_event.author)
+ expect(mr_with_event.metrics.latest_closed_at.to_s).to eq(latest_event.updated_at.to_s)
+ end
+
+ expect(mr_without_event.metrics.reload).to have_attributes(latest_closed_by_id: nil,
+ latest_closed_at: nil)
+ end
+
+ it 'does not updates metrics out of given range' do
+ out_of_range_mr = create(:merge_request)
+ create(:event, :closed, author: users.last, target: out_of_range_mr)
+
+ expect { subject.perform(mrs_with_event.first.id, mrs_with_event.second.id) }
+ .not_to change { out_of_range_mr.metrics.reload.merged_by }
+ .from(nil)
+ end
+ end
+
+ context 'merged events data update' do
+ let(:users) { create_list(:user, 3) }
+ let(:mrs_with_event) { create_list(:merge_request, 3) }
+
+ before do
+ create_list(:event, 2, :merged, author: users.first, target: mrs_with_event.first)
+ create_list(:event, 3, :merged, author: users.second, target: mrs_with_event.second)
+ create(:event, :merged, author: users.third, target: mrs_with_event.third)
+ end
+
+ it 'migrates multiple MR metrics with merged event data' do
+ mr_without_event = create(:merge_request)
+ create(:event, :merged)
+
+ subject.update_metrics_with_events_data(mrs_with_event.first.id, mrs_with_event.last.id)
+
+ mrs_with_event.each do |mr_with_event|
+ latest_event = Event.where(action: Event::MERGED, target: mr_with_event).last
+
+ expect(mr_with_event.metrics.reload.merged_by).to eq(latest_event.author)
+ end
+
+ expect(mr_without_event.metrics.reload).to have_attributes(merged_by_id: nil)
+ end
+
+ it 'does not updates metrics out of given range' do
+ out_of_range_mr = create(:merge_request)
+ create(:event, :merged, author: users.last, target: out_of_range_mr)
+
+ expect { subject.perform(mrs_with_event.first.id, mrs_with_event.second.id) }
+ .not_to change { out_of_range_mr.metrics.reload.merged_by }
+ .from(nil)
+ end
+ end
+ end
+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 b80df6956b0..be45c00dfe6 100644
--- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
@@ -182,13 +182,22 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do
end
context 'for a pre-Markdown Note attachment file path' do
- class Note < ActiveRecord::Base
- has_many :uploads, as: :model, dependent: :destroy
+ let(:model) { create(:note, :with_attachment) }
+ let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') }
+ let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) }
+
+ before do
+ Upload.where(model_type: 'Note', model_id: model.id).delete_all
end
- let(:model) { create(:note, :with_attachment) }
+ # Can't use the shared example because Note doesn't have an `uploads` association
+ it 'creates an Upload record' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1)
- it_behaves_like 'non_markdown_file'
+ expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs)
+ end
end
context 'for a user avatar file path' do
diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
index cd3f1a45270..8bb9ebe0419 100644
--- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
+++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
@@ -2,21 +2,10 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
include TrackUntrackedUploadsHelpers
+ include MigrationsHelpers
let!(:untracked_files_for_uploads) { described_class::UntrackedFile }
- matcher :be_scheduled_migration do |*expected|
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration, expected]
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
before do
DatabaseCleaner.clean
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index 7f3bf5fc41c..b5d86df09d2 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -68,8 +68,14 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
expect(Project.find_by_full_path(project_path)).not_to be_nil
end
+ it 'does not schedule an import' do
+ expect_any_instance_of(Project).not_to receive(:import_schedule)
+
+ importer.create_project_if_needed
+ end
+
it 'creates the Git repo in disk' do
- FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+ create_bare_repository("#{project_path}.git")
importer.create_project_if_needed
@@ -124,13 +130,31 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
end
it 'creates the Git repo in disk' do
- FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+ create_bare_repository("#{project_path}.git")
importer.create_project_if_needed
project = Project.find_by_full_path("#{admin.full_path}/#{project_path}")
expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
+ expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ end
+
+ it 'moves an existing project to the correct path' do
+ # This is a quick way to get a valid repository instead of copying an
+ # existing one. Since it's not persisted, the importer will try to
+ # create the project.
+ project = build(:project, :repository)
+ original_commit_count = project.repository.commit_count
+
+ bare_repo = Gitlab::BareRepositoryImport::Repository.new(project.repository_storage_path, project.repository.path)
+ gitlab_importer = described_class.new(admin, bare_repo)
+
+ expect(gitlab_importer).to receive(:create_project).and_call_original
+
+ new_project = gitlab_importer.create_project_if_needed
+
+ expect(new_project.repository.commit_count).to eq(original_commit_count)
end
end
@@ -141,8 +165,11 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
it_behaves_like 'importing a repository'
it 'creates the Wiki git repo in disk' do
- FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
- FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.wiki.git"))
+ create_bare_repository("#{project_path}.git")
+ create_bare_repository("#{project_path}.wiki.git")
+
+ expect(Projects::CreateService).to receive(:new).with(admin, hash_including(skip_wiki: true,
+ import_type: 'bare_repository')).and_call_original
importer.create_project_if_needed
@@ -165,4 +192,9 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
end
end
end
+
+ def create_bare_repository(project_path)
+ repo_path = File.join(base_dir, project_path)
+ Gitlab::Git::Repository.create(repo_path, bare: true)
+ end
end
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index 2db737f5fb6..9f42cf1dfca 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -1,51 +1,122 @@
require 'spec_helper'
describe ::Gitlab::BareRepositoryImport::Repository do
- let(:project_repo_path) { described_class.new('/full/path/', '/full/path/to/repo.git') }
+ context 'legacy storage' do
+ subject { described_class.new('/full/path/', '/full/path/to/repo.git') }
- it 'stores the repo path' do
- expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git')
- end
+ it 'stores the repo path' do
+ expect(subject.repo_path).to eq('/full/path/to/repo.git')
+ end
- it 'stores the group path' do
- expect(project_repo_path.group_path).to eq('to')
- end
+ it 'stores the group path' do
+ expect(subject.group_path).to eq('to')
+ end
- it 'stores the project name' do
- expect(project_repo_path.project_name).to eq('repo')
- end
+ it 'stores the project name' do
+ expect(subject.project_name).to eq('repo')
+ end
- it 'stores the wiki path' do
- expect(project_repo_path.wiki_path).to eq('/full/path/to/repo.wiki.git')
- end
+ it 'stores the wiki path' do
+ expect(subject.wiki_path).to eq('/full/path/to/repo.wiki.git')
+ end
- describe '#wiki?' do
- it 'returns true if it is a wiki' do
- wiki_path = described_class.new('/full/path/', '/full/path/to/a/b/my.wiki.git')
+ describe '#processable?' do
+ it 'returns false if it is a wiki' do
+ subject = described_class.new('/full/path/', '/full/path/to/a/b/my.wiki.git')
- expect(wiki_path.wiki?).to eq(true)
+ expect(subject).not_to be_processable
+ end
+
+ it 'returns true if group path is missing' do
+ subject = described_class.new('/full/path/', '/full/path/repo.git')
+
+ expect(subject).to be_processable
+ end
+
+ it 'returns true when group path and project name are present' do
+ expect(subject).to be_processable
+ end
end
- it 'returns false if it is not a wiki' do
- expect(project_repo_path.wiki?).to eq(false)
+ describe '#project_full_path' do
+ it 'returns the project full path with trailing slash in the root path' do
+ expect(subject.project_full_path).to eq('to/repo')
+ end
+
+ it 'returns the project full path with no trailing slash in the root path' do
+ subject = described_class.new('/full/path', '/full/path/to/repo.git')
+
+ expect(subject.project_full_path).to eq('to/repo')
+ end
end
end
- describe '#hashed?' do
- it 'returns true if it is a hashed folder' do
- path = described_class.new('/full/path/', '/full/path/@hashed/my.repo.git')
+ context 'hashed storage' do
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:repository_storage) { 'default' }
+ let(:root_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
+ let(:hashed_path) { "@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" }
+ let(:repo_path) { File.join(root_path, "#{hashed_path}.git") }
+ let(:wiki_path) { File.join(root_path, "#{hashed_path}.wiki.git") }
- expect(path.hashed?).to eq(true)
+ before do
+ gitlab_shell.add_repository(repository_storage, hashed_path)
+ repository = Rugged::Repository.new(repo_path)
+ repository.config['gitlab.fullpath'] = 'to/repo'
end
- it 'returns false if it is not a hashed folder' do
- expect(project_repo_path.hashed?).to eq(false)
+ after do
+ gitlab_shell.remove_repository(root_path, hashed_path)
end
- end
- describe '#project_full_path' do
- it 'returns the project full path' do
- expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git')
+ subject { described_class.new(root_path, repo_path) }
+
+ it 'stores the repo path' do
+ expect(subject.repo_path).to eq(repo_path)
+ end
+
+ it 'stores the wiki path' do
+ expect(subject.wiki_path).to eq(wiki_path)
+ end
+
+ it 'reads the group path from .git/config' do
+ expect(subject.group_path).to eq('to')
+ end
+
+ it 'reads the project name from .git/config' do
+ expect(subject.project_name).to eq('repo')
+ end
+
+ describe '#processable?' do
+ it 'returns false if it is a wiki' do
+ subject = described_class.new(root_path, wiki_path)
+
+ expect(subject).not_to be_processable
+ end
+
+ it 'returns false when group and project name are missing' do
+ repository = Rugged::Repository.new(repo_path)
+ repository.config.delete('gitlab.fullpath')
+
+ expect(subject).not_to be_processable
+ end
+
+ it 'returns true when group path and project name are present' do
+ expect(subject).to be_processable
+ end
+ end
+
+ describe '#project_full_path' do
+ it 'returns the project full path with trailing slash in the root path' do
+ expect(subject.project_full_path).to eq('to/repo')
+ end
+
+ it 'returns the project full path with no trailing slash in the root path' do
+ subject = described_class.new(root_path[0...-1], repo_path)
+
+ expect(subject.project_full_path).to eq('to/repo')
+ end
end
end
end
diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb
new file mode 100644
index 00000000000..f90c2d6aded
--- /dev/null
+++ b/spec/lib/gitlab/checks/project_moved_spec.rb
@@ -0,0 +1,87 @@
+require 'rails_helper'
+
+describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ describe '.fetch_redirct_message' do
+ context 'with a redirect message queue' do
+ it 'should return the redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ project_moved.add_redirect_message
+
+ expect(described_class.fetch_redirect_message(user.id, project.id)).to eq(project_moved.redirect_message)
+ end
+
+ it 'should delete the redirect message from redis' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ project_moved.add_redirect_message
+
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil
+ described_class.fetch_redirect_message(user.id, project.id)
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil
+ end
+ end
+
+ context 'with no redirect message queue' do
+ it 'should return nil' do
+ expect(described_class.fetch_redirect_message(1, 2)).to be_nil
+ end
+ end
+ end
+
+ describe '#add_redirect_message' do
+ it 'should queue a redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.add_redirect_message).to eq("OK")
+ end
+
+ it 'should handle anonymous clones' do
+ project_moved = described_class.new(project, nil, 'foo/bar', 'http')
+
+ expect(project_moved.add_redirect_message).to eq(nil)
+ end
+ end
+
+ describe '#redirect_message' do
+ context 'when the push is rejected' do
+ it 'should return a redirect message telling the user to try again' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
+ "\n\nPlease update your Git remote:" +
+ "\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n"
+
+ expect(project_moved.redirect_message(rejected: true)).to eq(message)
+ end
+ end
+
+ context 'when the push is not rejected' do
+ it 'should return a redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
+ "\n\nPlease update your Git remote:" +
+ "\n\n git remote set-url origin #{project.http_url_to_repo}\n"
+
+ expect(project_moved.redirect_message).to eq(message)
+ end
+ end
+ end
+
+ describe '#permanent_redirect?' do
+ context 'with a permanent RedirectRoute' do
+ it 'should return true' do
+ project.route.create_redirect('foo/bar', permanent: true)
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.permanent_redirect?).to be_truthy
+ end
+ end
+
+ context 'without a permanent RedirectRoute' do
+ it 'should return false' do
+ project.route.create_redirect('foo/bar')
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.permanent_redirect?).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index 33540eab5d6..05e2d94cbd6 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -120,6 +120,10 @@ describe Gitlab::Ci::Ansi2html do
expect(convert_html("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>')
end
+ it "can print 256 xterm fg bold colors" do
+ expect(convert_html("\e[38;5;16;1mHello")).to eq('<span class="xterm-fg-16 term-bold">Hello</span>')
+ end
+
it "can print 256 xterm bg colors on normal magenta foreground" do
expect(convert_html("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 0f1d72080c5..3ae7053a995 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -6,46 +6,81 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
let(:pipeline) { Ci::Pipeline.new }
let(:command) do
- double('command', source: :push,
- origin_ref: 'master',
- checkout_sha: project.commit.id,
- after_sha: nil,
- before_sha: nil,
- trigger_request: nil,
- schedule: nil,
- project: project,
- current_user: user)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :push,
+ origin_ref: 'master',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ trigger_request: nil,
+ schedule: nil,
+ project: project,
+ current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
before do
stub_repository_ci_yaml_file(sha: anything)
-
- step.perform!
end
it 'never breaks the chain' do
+ step.perform!
+
expect(step.break?).to be false
end
it 'fills pipeline object with data' do
+ step.perform!
+
expect(pipeline.sha).not_to be_empty
expect(pipeline.sha).to eq project.commit.id
expect(pipeline.ref).to eq 'master'
+ expect(pipeline.tag).to be false
expect(pipeline.user).to eq user
expect(pipeline.project).to eq project
end
it 'sets a valid config source' do
+ step.perform!
+
expect(pipeline.repository_source?).to be true
end
it 'returns a valid pipeline' do
+ step.perform!
+
expect(pipeline).to be_valid
end
it 'does not persist a pipeline' do
+ step.perform!
+
expect(pipeline).not_to be_persisted
end
+
+ context 'when pipeline is running for a tag' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :push,
+ origin_ref: 'mytag',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ trigger_request: nil,
+ schedule: nil,
+ project: project,
+ current_user: user)
+ end
+
+ before do
+ allow_any_instance_of(Repository).to receive(:tag_exists?).with('mytag').and_return(true)
+
+ step.perform!
+ end
+
+ it 'correctly indicated that this is a tagged pipeline' do
+ expect(pipeline).to be_tag
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
new file mode 100644
index 00000000000..75a177d2d1f
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Command do
+ set(:project) { create(:project, :repository) }
+
+ describe '#initialize' do
+ subject do
+ described_class.new(origin_ref: 'master')
+ end
+
+ it 'properly initialises object from hash' do
+ expect(subject.origin_ref).to eq('master')
+ end
+ end
+
+ context 'handling of origin_ref' do
+ let(:command) { described_class.new(project: project, origin_ref: origin_ref) }
+
+ describe '#branch_exists?' do
+ subject { command.branch_exists? }
+
+ context 'for existing branch' do
+ let(:origin_ref) { 'master' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'for invalid branch' do
+ let(:origin_ref) { 'something' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#tag_exists?' do
+ subject { command.tag_exists? }
+
+ context 'for existing ref' do
+ let(:origin_ref) { 'v1.0.0' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'for invalid ref' do
+ let(:origin_ref) { 'something' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#ref' do
+ subject { command.ref }
+
+ context 'for regular ref' do
+ let(:origin_ref) { 'master' }
+
+ it { is_expected.to eq('master') }
+ end
+
+ context 'for branch ref' do
+ let(:origin_ref) { 'refs/heads/master' }
+
+ it { is_expected.to eq('master') }
+ end
+
+ context 'for tag ref' do
+ let(:origin_ref) { 'refs/tags/1.0.0' }
+
+ it { is_expected.to eq('1.0.0') }
+ end
+
+ context 'for other refs' do
+ let(:origin_ref) { 'refs/merge-requests/11/head' }
+
+ it { is_expected.to eq('refs/merge-requests/11/head') }
+ end
+ end
+ end
+
+ describe '#sha' do
+ subject { command.sha }
+
+ context 'when invalid checkout_sha is specified' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
+
+ it 'returns empty value' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when a valid checkout_sha is specified' do
+ let(:command) { described_class.new(project: project, checkout_sha: project.commit.id) }
+
+ it 'returns checkout_sha' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+
+ context 'when a valid after_sha is specified' do
+ let(:command) { described_class.new(project: project, after_sha: project.commit.id) }
+
+ it 'returns after_sha' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+
+ context 'when a valid origin_ref is specified' do
+ let(:command) { described_class.new(project: project, origin_ref: 'HEAD') }
+
+ it 'returns SHA for given ref' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+ end
+
+ describe '#origin_sha' do
+ subject { command.origin_sha }
+
+ context 'when using checkout_sha and after_sha' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa', after_sha: 'bbb') }
+
+ it 'uses checkout_sha' do
+ is_expected.to eq('aaa')
+ end
+ end
+
+ context 'when using after_sha only' do
+ let(:command) { described_class.new(project: project, after_sha: 'bbb') }
+
+ it 'uses after_sha' do
+ is_expected.to eq('bbb')
+ end
+ end
+ end
+
+ describe '#before_sha' do
+ subject { command.before_sha }
+
+ context 'when using checkout_sha and before_sha' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa', before_sha: 'bbb') }
+
+ it 'uses before_sha' do
+ is_expected.to eq('bbb')
+ end
+ end
+
+ context 'when using checkout_sha only' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
+
+ it 'uses checkout_sha' do
+ is_expected.to eq('aaa')
+ end
+ end
+
+ context 'when checkout_sha and before_sha are empty' do
+ let(:command) { described_class.new(project: project) }
+
+ it 'uses BLANK_SHA' do
+ is_expected.to eq(Gitlab::Git::BLANK_SHA)
+ end
+ end
+ end
+
+ describe '#protected_ref?' do
+ let(:command) { described_class.new(project: project, origin_ref: 'my-branch') }
+
+ subject { command.protected_ref? }
+
+ context 'when a ref is protected' do
+ before do
+ expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when a ref is unprotected' do
+ before do
+ expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index f54e2326b06..1b03227d67b 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -10,9 +10,9 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
end
let(:command) do
- double('command', project: project,
- current_user: user,
- seeds_block: nil)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user, seeds_block: nil)
end
let(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index e165e0fac2a..eca23694a2b 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do
set(:user) { create(:user) }
let(:pipeline) { build_stubbed(:ci_pipeline) }
- let(:command) { double('command' ) }
+ let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new }
let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
index 32bd5de829b..dc13cae961c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
@@ -6,10 +6,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:command) do
- double('command', project: project,
- current_user: user,
- ignore_skip_ci: false,
- save_incompleted: true)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ ignore_skip_ci: false,
+ save_incompleted: true)
end
let(:step) { described_class.new(pipeline, command) }
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 0bbdd23f4d6..a973ccda8de 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -5,11 +5,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
set(:user) { create(:user) }
let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: ref, project: project)
+ build_stubbed(:ci_pipeline, project: project)
end
let(:command) do
- double('command', project: project, current_user: user)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: ref)
end
let(:step) { described_class.new(pipeline, command) }
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 8357af38f92..5c12c6e6392 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -5,9 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
set(:user) { create(:user) }
let(:command) do
- double('command', project: project,
- current_user: user,
- save_incompleted: true)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ save_incompleted: true)
end
let!(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
index bb356efe9ad..fb1b53fc55c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
@@ -3,10 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
-
- let(:command) do
- double('command', project: project, current_user: user)
- end
+ let(:pipeline) { build_stubbed(:ci_pipeline) }
let!(:step) { described_class.new(pipeline, command) }
@@ -14,9 +11,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
step.perform!
end
- context 'when pipeline ref and sha exists' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project)
+ context 'when ref and sha exists' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'master', checkout_sha: project.commit.id)
end
it 'does not break the chain' do
@@ -28,9 +26,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end
end
- context 'when pipeline ref does not exist' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'something', project: project)
+ context 'when ref does not exist' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'something')
end
it 'breaks the chain' do
@@ -43,9 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end
end
- context 'when pipeline does not have SHA set' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project)
+ context 'when does not have existing SHA set' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'master', checkout_sha: 'something')
end
it 'breaks the chain' do
diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb
index 03d1f46b517..2cce7a23ea7 100644
--- a/spec/lib/gitlab/ci/status/build/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/common_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::Common do
describe '#has_details?' do
context 'when user has access to read build' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it { is_expected.to have_details }
diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb
index b38fbee2486..40871f86568 100644
--- a/spec/lib/gitlab/ci/status/external/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/external/common_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Status::External::Common do
describe '#has_details?' do
context 'when user has access to read commit status' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it { is_expected.to have_details }
diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb
index c96fd53e730..529d02a3e39 100644
--- a/spec/lib/gitlab/ci/status/external/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Ci::Status::External::Factory do
let(:external_url) { 'http://gitlab.com/status' }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when external status has a simple core status' do
diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
index 4a5b45e7cae..57df8325635 100644
--- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Pipeline::Common do
describe '#has_details?' do
context 'when user has access to read pipeline' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it { is_expected.to have_details }
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index dd754b849b2..defb3fdc0df 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
let(:factory) { described_class.new(pipeline, user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when pipeline has a core status' do
diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb
index f5f03ac0395..6ec35f8da7e 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.team << [user, :master]
+ project.add_master(user)
end
it 'has details' do
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 432b07e4902..dee4f4efd1b 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Stage::Factory do
end
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when stage has a core status' do
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index ef7d766a13d..8c79ef54c6c 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -13,8 +13,8 @@ describe Gitlab::ClosingIssueExtractor do
subject { described_class.new(project, project.creator) }
before do
- project.team << [project.creator, :developer]
- project2.team << [project.creator, :master]
+ project.add_developer(project.creator)
+ project2.add_master(project.creator)
end
describe "#closed_by_message" do
@@ -297,7 +297,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.team << [jira_project.creator, :master]
+ jira_project.add_master(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/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
index 0560c47f03f..3fe0493ed9b 100644
--- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
@@ -23,6 +23,8 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do
allow_any_instance_of(described_class).to receive(:serialize) do |event|
event
end
+ allow_any_instance_of(described_class)
+ .to receive(:allowed_ids).and_return(nil)
stub_const('Gitlab::CycleAnalytics::BaseEventFetcher::MAX_EVENTS', max_events)
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index 28ea7d4c303..38a47a159e1 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -122,17 +122,18 @@ describe 'cycle analytics events' do
let(:stage) { :test }
let(:merge_request) { MergeRequest.first }
+
let!(:pipeline) do
create(:ci_pipeline,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
- project: context.project,
+ project: project,
head_pipeline_of: merge_request)
end
before do
- create(:ci_build, pipeline: pipeline, status: :success, author: user)
- create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, :success, pipeline: pipeline, author: user)
+ create(:ci_build, :success, pipeline: pipeline, author: user)
pipeline.run!
pipeline.succeed!
@@ -219,17 +220,18 @@ describe 'cycle analytics events' do
describe '#staging_events' do
let(:stage) { :staging }
let(:merge_request) { MergeRequest.first }
+
let!(:pipeline) do
create(:ci_pipeline,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
- project: context.project,
+ project: project,
head_pipeline_of: merge_request)
end
before do
- create(:ci_build, pipeline: pipeline, status: :success, author: user)
- create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, :success, pipeline: pipeline, author: user)
+ create(:ci_build, :success, pipeline: pipeline, author: user)
pipeline.run!
pipeline.succeed!
diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
index 2a0dd7be439..6de4bd3dc7c 100644
--- a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
@@ -38,7 +38,7 @@ describe Gitlab::CycleAnalytics::Permissions do
context 'user is master' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'has permissions to issue stage' do
@@ -72,7 +72,7 @@ describe Gitlab::CycleAnalytics::Permissions do
context 'user has no build permissions' do
before do
- project.team << [user, :guest]
+ project.add_guest(user)
end
it 'has permissions to issue stage' do
@@ -90,7 +90,7 @@ describe Gitlab::CycleAnalytics::Permissions do
context 'user has no merge request permissions' do
before do
- project.team << [user, :guest]
+ project.add_guest(user)
end
it 'has permissions to issue stage' do
@@ -108,7 +108,7 @@ describe Gitlab::CycleAnalytics::Permissions do
context 'user has no issue permissions' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 664ba0f7234..43761c2fe0c 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -902,7 +902,7 @@ describe Gitlab::Database::MigrationHelpers do
describe '#check_trigger_permissions!' do
it 'does nothing when the user has the correct permissions' do
expect { model.check_trigger_permissions!('users') }
- .not_to raise_error(RuntimeError)
+ .not_to raise_error
end
it 'raises RuntimeError when the user does not have the correct permissions' do
@@ -1006,12 +1006,12 @@ describe Gitlab::Database::MigrationHelpers do
context 'with batch_size option' do
it 'queues jobs correctly' do
Sidekiq::Testing.fake! do
- model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2)
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2)
expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.minutes.from_now.to_f)
end
end
end
@@ -1019,10 +1019,10 @@ describe Gitlab::Database::MigrationHelpers do
context 'without batch_size option' do
it 'queues jobs correctly' do
Sidekiq::Testing.fake! do
- model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds)
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes)
expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
end
end
end
@@ -1036,4 +1036,93 @@ describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ describe '#change_column_type_using_background_migration' do
+ let!(:issue) { create(:issue) }
+
+ let(:issue_model) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'issues'
+ include EachBatch
+ end
+ end
+
+ it 'changes the type of a column using a background migration' do
+ expect(model)
+ .to receive(:add_column)
+ .with('issues', 'closed_at_for_type_change', :datetime_with_timezone)
+
+ expect(model)
+ .to receive(:install_rename_triggers)
+ .with('issues', :closed_at, 'closed_at_for_type_change')
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 10.minutes,
+ 'CopyColumn',
+ ['issues', :closed_at, 'closed_at_for_type_change', issue.id, issue.id]
+ )
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 1.hour + 10.minutes,
+ 'CleanupConcurrentTypeChange',
+ ['issues', :closed_at, 'closed_at_for_type_change']
+ )
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CopyColumn')
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CleanupConcurrentTypeChange')
+
+ model.change_column_type_using_background_migration(
+ issue_model.all,
+ :closed_at,
+ :datetime_with_timezone
+ )
+ end
+ end
+
+ describe '#perform_background_migration_inline?' do
+ it 'returns true in a test environment' do
+ allow(Rails.env)
+ .to receive(:test?)
+ .and_return(true)
+
+ expect(model.perform_background_migration_inline?).to eq(true)
+ end
+
+ it 'returns true in a development environment' do
+ allow(Rails.env)
+ .to receive(:test?)
+ .and_return(false)
+
+ allow(Rails.env)
+ .to receive(:development?)
+ .and_return(true)
+
+ expect(model.perform_background_migration_inline?).to eq(true)
+ end
+
+ it 'returns false in a production environment' do
+ allow(Rails.env)
+ .to receive(:test?)
+ .and_return(false)
+
+ allow(Rails.env)
+ .to receive(:development?)
+ .and_return(false)
+
+ expect(model.perform_background_migration_inline?).to eq(false)
+ end
+ end
end
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 d81774c8b8f..a067c42b75b 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
@@ -19,4 +19,18 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
diff_files
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)
+ end
+ end
+
+ context 'with Gitaly disabled', :disable_gitaly do
+ it_behaves_like 'initializes a DiffCollection'
+ end
+
+ context 'with Gitaly enabled' do
+ it_behaves_like 'initializes a DiffCollection'
+ end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index ff9acfd08b9..9204ea37963 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -431,4 +431,29 @@ describe Gitlab::Diff::File do
end
end
end
+
+ context 'when neither blob exists' do
+ let(:blank_diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: Gitlab::Git::BLANK_SHA, head_sha: Gitlab::Git::BLANK_SHA) }
+ let(:diff_file) { described_class.new(diff, diff_refs: blank_diff_refs, repository: project.repository) }
+
+ describe '#blob' do
+ it 'returns a concrete nil so it can be used in boolean expressions' do
+ actual = diff_file.blob && true
+
+ expect(actual).to be_nil
+ end
+ end
+
+ describe '#binary?' do
+ it 'returns false' do
+ expect(diff_file).not_to be_binary
+ end
+ end
+
+ describe '#size' do
+ it 'returns zero' do
+ expect(diff_file.size).to be_zero
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
index 51ce3116880..dc1a93367a4 100644
--- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -49,6 +49,7 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do
expect(merge_request.author).to eq(user)
expect(merge_request.source_branch).to eq('feature')
expect(merge_request.title).to eq('Feature added')
+ expect(merge_request.description).to eq('Merge request description')
expect(merge_request.target_branch).to eq(project.default_branch)
end
end
@@ -79,6 +80,17 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do
expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidMergeRequestError)
end
end
+
+ context "when the message body is blank" do
+ let(:email_raw) { fixture_file("emails/valid_new_merge_request_no_description.eml") }
+
+ it "creates a new merge request with description set from the last commit" do
+ expect { receiver.execute }.to change { project.merge_requests.count }.by(1)
+ merge_request = project.merge_requests.last
+
+ expect(merge_request.description).to eq('Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>')
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index d0fa16ce4d1..031efcf1291 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -66,7 +66,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
context 'and current user can update noteable' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'does not raise an error' do
@@ -99,7 +99,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
context 'and current user can update noteable' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'post a note and updates the noteable' do
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index f6e5c55240f..4e9367323cb 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -120,6 +120,24 @@ describe Gitlab::EncodingHelper do
it 'returns empty string on conversion errors' do
expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError)
end
+
+ context 'with strings that can be forcefully encoded into utf8' do
+ let(:test_string) do
+ "refs/heads/FixSymbolsTitleDropdown".encode("ASCII-8BIT")
+ end
+ let(:expected_string) do
+ "refs/heads/FixSymbolsTitleDropdown".encode("UTF-8")
+ end
+
+ subject { ext_class.encode_utf8(test_string) }
+
+ it "doesn't use CharlockHolmes if the encoding can be forced into utf_8" do
+ expect(CharlockHolmes::EncodingDetector).not_to receive(:detect)
+
+ expect(subject).to eq(expected_string)
+ expect(subject.encoding.name).to eq('UTF-8')
+ end
+ end
end
describe '#clean' do
@@ -145,4 +163,18 @@ describe Gitlab::EncodingHelper do
end
end
end
+
+ describe 'encode_binary' do
+ [
+ [nil, ""],
+ ["", ""],
+ [" ", " "],
+ %w(a1 a1),
+ ["ç¼–ç ", "\xE7\xBC\x96\xE7\xA0\x81".b]
+ ].each do |input, result|
+ it "encodes #{input.inspect} to #{result.inspect}" do
+ expect(ext_class.encode_binary(input)).to eq(result)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index 7322a326b01..6193e177668 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -73,4 +73,19 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do
described_class.new(key, timeout: 3600).try_obtain
end
end
+
+ describe '#ttl' do
+ it 'returns the TTL of the Redis key' do
+ lease = described_class.new('kittens', timeout: 100)
+ lease.try_obtain
+
+ expect(lease.ttl <= 100).to eq(true)
+ end
+
+ it 'returns nil when the lease does not exist' do
+ lease = described_class.new('kittens', timeout: 10)
+
+ expect(lease.ttl).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 7dc06c90078..4d2f08f95fc 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'some text' }
before do
- old_project.team << [user, :reporter]
+ old_project.add_reporter(user)
end
describe '#rewrite' do
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index c04a9688503..07eb5b82d5f 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -146,7 +146,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
context 'when sha references a tree' do
it 'returns nil' do
- tree = Gitlab::Git::Commit.find(repository, 'master').tree
+ tree = repository.rugged.rev_parse('master^{tree}')
blob = Gitlab::Git::Blob.raw(repository, tree.oid)
@@ -202,16 +202,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
context 'limiting' do
subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) }
- context 'default' do
- let(:blob_size_limit) { nil }
-
- it 'limits to MAX_DATA_DISPLAY_SIZE' do
- stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100)
-
- expect(subject.first.data.size).to eq(100)
- end
- end
-
context 'positive' do
let(:blob_size_limit) { 10 }
@@ -221,7 +211,10 @@ describe Gitlab::Git::Blob, seed_helper: true do
context 'zero' do
let(:blob_size_limit) { 0 }
- it { expect(subject.first.data).to eq('') }
+ 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
@@ -237,7 +230,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
describe '.batch_lfs_pointers' do
- let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree }
+ let(:tree_object) { repository.rugged.rev_parse('master^{tree}') }
let(:non_lfs_blob) do
Gitlab::Git::Blob.find(
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 5ed639543e0..6a07a3ca8b8 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -55,7 +55,6 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(@commit.parents).to eq(@gitlab_parents) }
it { expect(@commit.parent_id).to eq(@parents.first.oid) }
it { expect(@commit.no_commit_message).to eq("--no commit message") }
- it { expect(@commit.tree).to eq(@tree) }
after do
# Erase the new commit so other tests get the original repo
@@ -428,6 +427,11 @@ describe Gitlab::Git::Commit, seed_helper: true do
subject { super().deletions }
it { is_expected.to eq(6) }
end
+
+ describe '#total' do
+ subject { super().total }
+ it { is_expected.to eq(17) }
+ end
end
describe '#stats with gitaly on' do
diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb
new file mode 100644
index 00000000000..a798b188a0d
--- /dev/null
+++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb
@@ -0,0 +1,337 @@
+require 'spec_helper'
+
+describe Gitlab::Git::GitlabProjects do
+ after do
+ TestEnv.clean_test_path
+ end
+
+ let(:project) { create(:project, :repository) }
+
+ if $VERBOSE
+ let(:logger) { Logger.new(STDOUT) }
+ else
+ let(:logger) { double('logger').as_null_object }
+ end
+
+ let(:tmp_repos_path) { TestEnv.repos_path }
+ let(:repo_name) { project.disk_path + '.git' }
+ let(:tmp_repo_path) { File.join(tmp_repos_path, repo_name) }
+ let(:gl_projects) { build_gitlab_projects(tmp_repos_path, repo_name) }
+
+ describe '#initialize' do
+ it { expect(gl_projects.shard_path).to eq(tmp_repos_path) }
+ it { expect(gl_projects.repository_relative_path).to eq(repo_name) }
+ it { expect(gl_projects.repository_absolute_path).to eq(File.join(tmp_repos_path, repo_name)) }
+ it { expect(gl_projects.logger).to eq(logger) }
+ end
+
+ describe '#mv_project' do
+ let(:new_repo_path) { File.join(tmp_repos_path, 'repo.git') }
+
+ it 'moves a repo directory' do
+ expect(File.exist?(tmp_repo_path)).to be_truthy
+
+ message = "Moving repository from <#{tmp_repo_path}> to <#{new_repo_path}>."
+ expect(logger).to receive(:info).with(message)
+
+ expect(gl_projects.mv_project('repo.git')).to be_truthy
+
+ expect(File.exist?(tmp_repo_path)).to be_falsy
+ expect(File.exist?(new_repo_path)).to be_truthy
+ end
+
+ it "fails if the source path doesn't exist" do
+ expected_source_path = File.join(tmp_repos_path, 'bad-src.git')
+ expect(logger).to receive(:error).with("mv-project failed: source path <#{expected_source_path}> does not exist.")
+
+ result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git')
+ expect(result).to be_falsy
+ end
+
+ it 'fails if the destination path already exists' do
+ FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git'))
+
+ expected_distination_path = File.join(tmp_repos_path, 'already-exists.git')
+ message = "mv-project failed: destination path <#{expected_distination_path}> already exists."
+ expect(logger).to receive(:error).with(message)
+
+ expect(gl_projects.mv_project('already-exists.git')).to be_falsy
+ end
+ end
+
+ describe '#rm_project' do
+ it 'removes a repo directory' do
+ expect(File.exist?(tmp_repo_path)).to be_truthy
+ expect(logger).to receive(:info).with("Removing repository <#{tmp_repo_path}>.")
+
+ expect(gl_projects.rm_project).to be_truthy
+
+ expect(File.exist?(tmp_repo_path)).to be_falsy
+ end
+ end
+
+ describe '#push_branches' do
+ let(:remote_name) { 'remote-name' }
+ let(:branch_name) { 'master' }
+ let(:cmd) { %W(git push -- #{remote_name} #{branch_name}) }
+ let(:force) { false }
+
+ subject { gl_projects.push_branches(remote_name, 600, force, [branch_name]) }
+
+ it 'executes the command' do
+ stub_spawn(cmd, 600, tmp_repo_path, success: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'fails' do
+ stub_spawn(cmd, 600, tmp_repo_path, success: false)
+
+ is_expected.to be_falsy
+ end
+
+ context 'with --force' do
+ let(:cmd) { %W(git push --force -- #{remote_name} #{branch_name}) }
+ let(:force) { true }
+
+ it 'executes the command' do
+ stub_spawn(cmd, 600, tmp_repo_path, success: true)
+
+ is_expected.to be_truthy
+ end
+ end
+ end
+
+ describe '#fetch_remote' do
+ let(:remote_name) { 'remote-name' }
+ let(:branch_name) { 'master' }
+ let(:force) { false }
+ let(:tags) { true }
+ let(:args) { { force: force, tags: tags }.merge(extra_args) }
+ let(:extra_args) { {} }
+ let(:cmd) { %W(git fetch #{remote_name} --prune --quiet --tags) }
+
+ subject { gl_projects.fetch_remote(remote_name, 600, args) }
+
+ def stub_tempfile(name, filename, opts = {})
+ chmod = opts.delete(:chmod)
+ file = StringIO.new
+
+ allow(file).to receive(:close!)
+ allow(file).to receive(:path).and_return(name)
+
+ expect(Tempfile).to receive(:new).with(filename).and_return(file)
+ expect(file).to receive(:chmod).with(chmod) if chmod
+
+ file
+ end
+
+ context 'with default args' do
+ it 'executes the command' do
+ stub_spawn(cmd, 600, tmp_repo_path, {}, success: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'fails' do
+ stub_spawn(cmd, 600, tmp_repo_path, {}, success: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'with --force' do
+ let(:force) { true }
+ let(:cmd) { %W(git fetch #{remote_name} --prune --quiet --force --tags) }
+
+ it 'executes the command with forced option' do
+ stub_spawn(cmd, 600, tmp_repo_path, {}, success: true)
+
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'with --no-tags' do
+ let(:tags) { false }
+ let(:cmd) { %W(git fetch #{remote_name} --prune --quiet --no-tags) }
+
+ it 'executes the command' do
+ stub_spawn(cmd, 600, tmp_repo_path, {}, success: true)
+
+ is_expected.to be_truthy
+ end
+ end
+
+ describe 'with an SSH key' do
+ let(:extra_args) { { ssh_key: 'SSH KEY' } }
+
+ it 'sets GIT_SSH to a custom script' do
+ script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755)
+ key = stub_tempfile('/tmp files/keyFile', 'gitlab-shell-key-file', chmod: 0o400)
+
+ stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true)
+
+ is_expected.to be_truthy
+
+ expect(script.string).to eq("#!/bin/sh\nexec ssh '-oIdentityFile=\"/tmp files/keyFile\"' '-oIdentitiesOnly=\"yes\"' \"$@\"")
+ expect(key.string).to eq('SSH KEY')
+ end
+ end
+
+ describe 'with known_hosts data' do
+ let(:extra_args) { { known_hosts: 'KNOWN HOSTS' } }
+
+ it 'sets GIT_SSH to a custom script' do
+ script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755)
+ key = stub_tempfile('/tmp files/knownHosts', 'gitlab-shell-known-hosts', chmod: 0o400)
+
+ stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true)
+
+ is_expected.to be_truthy
+
+ expect(script.string).to eq("#!/bin/sh\nexec ssh '-oStrictHostKeyChecking=\"yes\"' '-oUserKnownHostsFile=\"/tmp files/knownHosts\"' \"$@\"")
+ expect(key.string).to eq('KNOWN HOSTS')
+ end
+ end
+ end
+
+ describe '#import_project' do
+ let(:project) { create(:project) }
+ let(:import_url) { TestEnv.factory_repo_path_bare }
+ let(:cmd) { %W(git clone --bare -- #{import_url} #{tmp_repo_path}) }
+ let(:timeout) { 600 }
+
+ subject { gl_projects.import_project(import_url, timeout) }
+
+ context 'success import' do
+ it 'imports a repo' do
+ expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy
+
+ message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>."
+ expect(logger).to receive(:info).with(message)
+
+ is_expected.to be_truthy
+
+ expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_truthy
+ end
+ end
+
+ context 'already exists' do
+ it "doesn't import" do
+ FileUtils.mkdir_p(tmp_repo_path)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ 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)
+
+ 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
+ end
+ end
+
+ describe '#fork_repository' do
+ let(:dest_repos_path) { tmp_repos_path }
+ let(:dest_repo_name) { File.join('@hashed', 'aa', 'bb', 'xyz.git') }
+ let(:dest_repo) { File.join(dest_repos_path, dest_repo_name) }
+
+ subject { gl_projects.fork_repository(dest_repos_path, dest_repo_name) }
+
+ before do
+ FileUtils.mkdir_p(dest_repos_path)
+ end
+
+ after do
+ FileUtils.rm_rf(dest_repos_path)
+ end
+
+ shared_examples 'forking a repository' do
+ it 'forks the repository' 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
+
+ it 'does not fork if a project of the same name already exists' do
+ # create a fake project at the intended destination
+ FileUtils.mkdir_p(dest_repo)
+
+ is_expected.to be_falsy
+ 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'
+
+ # 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_path) { File.join(File.dirname(tmp_repos_path), 'alternative') }
+
+ 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
+ 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)
+
+ 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)
+
+ subject
+ end
+ end
+ end
+ end
+ end
+
+ def build_gitlab_projects(*args)
+ described_class.new(
+ *args,
+ global_hooks_path: Gitlab.config.gitlab_shell.hooks_path,
+ logger: logger
+ )
+ end
+
+ def stub_spawn(*args, success: true)
+ exitstatus = success ? 0 : nil
+ expect(gl_projects).to receive(:popen_with_timeout).with(*args)
+ .and_return(["output", exitstatus])
+ end
+
+ def stub_spawn_timeout(*args)
+ expect(gl_projects).to receive(:popen_with_timeout).with(*args)
+ .and_raise(Timeout::Error)
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index f19b65a5f71..f346a345f00 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -18,6 +18,52 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:storage_path) { TestEnv.repos_path }
+
+ describe '.create_hooks' do
+ let(:repo_path) { File.join(storage_path, 'hook-test.git') }
+ let(:hooks_dir) { File.join(repo_path, 'hooks') }
+ let(:target_hooks_dir) { Gitlab.config.gitlab_shell.hooks_path }
+ let(:existing_target) { File.join(repo_path, 'foobar') }
+
+ before do
+ FileUtils.rm_rf(repo_path)
+ FileUtils.mkdir_p(repo_path)
+ end
+
+ context 'hooks is a directory' do
+ let(:existing_file) { File.join(hooks_dir, 'my-file') }
+
+ before do
+ FileUtils.mkdir_p(hooks_dir)
+ FileUtils.touch(existing_file)
+ described_class.create_hooks(repo_path, target_hooks_dir)
+ end
+
+ it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
+ it { expect(Dir[File.join(repo_path, "hooks.old.*/my-file")].count).to eq(1) }
+ end
+
+ context 'hooks is a valid symlink' do
+ before do
+ FileUtils.mkdir_p existing_target
+ File.symlink(existing_target, hooks_dir)
+ described_class.create_hooks(repo_path, target_hooks_dir)
+ end
+
+ it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
+ end
+
+ context 'hooks is a broken symlink' do
+ before do
+ FileUtils.rm_f(existing_target)
+ File.symlink(existing_target, hooks_dir)
+ described_class.create_hooks(repo_path, target_hooks_dir)
+ end
+
+ it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
+ end
+ end
describe "Respond to" do
subject { repository }
@@ -588,44 +634,54 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#fetch_as_mirror_without_shell' do
+ describe '#fetch_repository_as_mirror' do
let(:new_repository) do
Gitlab::Git::Repository.new('default', 'my_project.git', '')
end
- subject { new_repository.fetch_as_mirror_without_shell(repository.path) }
+ subject { new_repository.fetch_repository_as_mirror(repository) }
before do
Gitlab::Shell.new.add_repository('default', 'my_project')
end
after do
- Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project')
+ Gitlab::Shell.new.remove_repository(storage_path, 'my_project')
end
- it 'fetches a url as a mirror remote' do
- subject
+ shared_examples 'repository mirror fecthing' do
+ it 'fetches a repository as a mirror remote' do
+ subject
- expect(refs(new_repository.path)).to eq(refs(repository.path))
- end
+ expect(refs(new_repository.path)).to eq(refs(repository.path))
+ end
- context 'with keep-around refs' do
- let(:sha) { SeedRepo::Commit::ID }
- let(:keep_around_ref) { "refs/keep-around/#{sha}" }
- let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
+ context 'with keep-around refs' do
+ let(:sha) { SeedRepo::Commit::ID }
+ let(:keep_around_ref) { "refs/keep-around/#{sha}" }
+ let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
- before do
- repository.rugged.references.create(keep_around_ref, sha, force: true)
- repository.rugged.references.create(tmp_ref, sha, force: true)
- end
+ before do
+ repository.rugged.references.create(keep_around_ref, sha, force: true)
+ repository.rugged.references.create(tmp_ref, sha, force: true)
+ end
- it 'includes the temporary and keep-around refs' do
- subject
+ 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
+
+ context 'with gitaly enabled' do
+ it_behaves_like 'repository mirror fecthing'
+ end
+
+ context 'with gitaly enabled', :skip_gitaly_mock do
+ it_behaves_like 'repository mirror fecthing'
+ end
end
describe '#remote_tags' do
@@ -656,21 +712,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#remote_exists?' do
- before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.add_remote("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
- end
-
- it 'returns true for an existing remote' do
- expect(@repo.remote_exists?('new_remote')).to eq(true)
- end
-
- it 'returns false for a non-existing remote' do
- expect(@repo.remote_exists?('foo')).to eq(false)
- end
- end
-
describe "#log" do
let(:commit_with_old_name) do
Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id)
@@ -985,7 +1026,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples 'extended commit counting' do
context 'with after timestamp' do
it 'returns the number of commits after timestamp' do
- options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') }
+ options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') }
expect(repository.count_commits(options)).to eq(25)
end
@@ -993,19 +1034,65 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with before timestamp' do
it 'returns the number of commits before timestamp' do
- options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') }
+ options = { ref: 'feature', before: Time.iso8601('2015-03-03T20:15:01+00:00') }
expect(repository.count_commits(options)).to eq(9)
end
end
+ context 'with max_count' do
+ it 'returns the number of commits with path ' do
+ options = { ref: 'master', max_count: 5 }
+
+ expect(repository.count_commits(options)).to eq(5)
+ end
+ end
+
context 'with path' do
it 'returns the number of commits with path ' do
- options = { ref: 'master', limit: nil, path: "encoding" }
+ options = { ref: 'master', path: 'encoding' }
expect(repository.count_commits(options)).to eq(2)
end
end
+
+ context 'with option :from and option :to' do
+ it 'returns the number of commits ahead for fix-mode..fix-blob-path' do
+ options = { from: 'fix-mode', to: 'fix-blob-path' }
+
+ expect(repository.count_commits(options)).to eq(2)
+ end
+
+ it 'returns the number of commits ahead for fix-blob-path..fix-mode' do
+ options = { from: 'fix-blob-path', to: 'fix-mode' }
+
+ expect(repository.count_commits(options)).to eq(1)
+ end
+
+ context 'with option :left_right' do
+ it 'returns the number of commits for fix-mode...fix-blob-path' do
+ options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true }
+
+ expect(repository.count_commits(options)).to eq([1, 2])
+ end
+
+ context 'with max_count' do
+ it 'returns the number of commits with path ' do
+ options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 }
+
+ expect(repository.count_commits(options)).to eq([1, 1])
+ end
+ end
+ end
+ end
+
+ context 'with max_count' do
+ it 'returns the number of commits up to the passed limit' do
+ options = { ref: 'master', max_count: 10, after: Time.iso8601('2013-03-03T20:15:01+00:00') }
+
+ expect(repository.count_commits(options)).to eq(10)
+ end
+ end
end
context 'when Gitaly count_commits feature is enabled' do
@@ -1625,7 +1712,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:branch_name) { "to-be-deleted-soon" }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
repository.create_branch(branch_name)
end
@@ -1662,21 +1749,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#fetch_remote_without_shell' do
- let(:git_path) { Gitlab.config.git.bin_path }
- let(:remote_name) { 'my_remote' }
-
- subject { repository.fetch_remote_without_shell(remote_name) }
-
- it 'fetches the remote and returns true if the command was successful' do
- expect(repository).to receive(:popen)
- .with(%W(#{git_path} fetch #{remote_name}), repository.path, {})
- .and_return(['', 0])
-
- expect(subject).to be(true)
- end
- end
-
describe '#merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@@ -1704,6 +1776,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(result.repo_created).to eq(false)
expect(result.branch_created).to eq(false)
end
+
+ it 'returns nil if there was a concurrent branch update' do
+ concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
+ result = repository.merge(user, source_sha, target_branch, 'Test merge') do
+ # This ref update should make the merge fail
+ repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id)
+ end
+
+ # This 'nil' signals that the merge was not applied
+ expect(result).to be_nil
+
+ # Our concurrent ref update should not have been undone
+ expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id)
+ end
end
context 'with gitaly' do
@@ -1813,6 +1899,166 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe 'remotes' do
+ let(:repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+ let(:remote_name) { 'my-remote' }
+
+ after do
+ ensure_seeds
+ end
+
+ describe '#add_remote' do
+ let(:url) { 'http://my-repo.git' }
+ let(:mirror_refmap) { '+refs/*:refs/*' }
+
+ it 'creates a new remote via Gitaly' do
+ expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
+ .to receive(:add_remote).with(remote_name, url, mirror_refmap)
+
+ repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
+ end
+
+ context 'with Gitaly disabled', :skip_gitaly_mock do
+ it 'creates a new remote via Rugged' do
+ expect_any_instance_of(Rugged::RemoteCollection).to receive(:create)
+ .with(remote_name, url)
+ expect_any_instance_of(Rugged::Config).to receive(:[]=)
+ .with("remote.#{remote_name}.mirror", true)
+ expect_any_instance_of(Rugged::Config).to receive(:[]=)
+ .with("remote.#{remote_name}.prune", true)
+ expect_any_instance_of(Rugged::Config).to receive(:[]=)
+ .with("remote.#{remote_name}.fetch", mirror_refmap)
+
+ repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
+ end
+ end
+ end
+
+ describe '#remove_remote' do
+ it 'removes the remote via Gitaly' do
+ expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
+ .to receive(:remove_remote).with(remote_name)
+
+ repository.remove_remote(remote_name)
+ end
+
+ context 'with Gitaly disabled', :skip_gitaly_mock do
+ it 'removes the remote via Rugged' do
+ expect_any_instance_of(Rugged::RemoteCollection).to receive(:delete)
+ .with(remote_name)
+
+ repository.remove_remote(remote_name)
+ end
+ end
+ end
+ end
+
+ describe '#gitlab_projects' do
+ subject { repository.gitlab_projects }
+
+ it { expect(subject.shard_path).to eq(storage_path) }
+ it { expect(subject.repository_relative_path).to eq(repository.relative_path) }
+ end
+
+ context 'gitlab_projects commands' do
+ let(:gitlab_projects) { repository.gitlab_projects }
+ let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
+
+ describe '#push_remote_branches' do
+ subject do
+ repository.push_remote_branches('downstream-remote', ['master'])
+ end
+
+ it 'executes the command' do
+ expect(gitlab_projects).to receive(:push_branches)
+ .with('downstream-remote', timeout, true, ['master'])
+ .and_return(true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'raises an error if the command fails' do
+ allow(gitlab_projects).to receive(:output) { 'error' }
+ expect(gitlab_projects).to receive(:push_branches)
+ .with('downstream-remote', timeout, true, ['master'])
+ .and_return(false)
+
+ expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
+ end
+ end
+
+ describe '#delete_remote_branches' do
+ subject do
+ repository.delete_remote_branches('downstream-remote', ['master'])
+ end
+
+ it 'executes the command' do
+ expect(gitlab_projects).to receive(:delete_remote_branches)
+ .with('downstream-remote', ['master'])
+ .and_return(true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'raises an error if the command fails' do
+ allow(gitlab_projects).to receive(:output) { 'error' }
+ expect(gitlab_projects).to receive(:delete_remote_branches)
+ .with('downstream-remote', ['master'])
+ .and_return(false)
+
+ expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
+ end
+ end
+
+ describe '#delete_remote_branches' do
+ subject do
+ repository.delete_remote_branches('downstream-remote', ['master'])
+ end
+
+ it 'executes the command' do
+ expect(gitlab_projects).to receive(:delete_remote_branches)
+ .with('downstream-remote', ['master'])
+ .and_return(true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'raises an error if the command fails' do
+ allow(gitlab_projects).to receive(:output) { 'error' }
+ expect(gitlab_projects).to receive(:delete_remote_branches)
+ .with('downstream-remote', ['master'])
+ .and_return(false)
+
+ expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
+ end
+ end
+
+ describe '#delete_remote_branches' do
+ subject do
+ repository.delete_remote_branches('downstream-remote', ['master'])
+ end
+
+ it 'executes the command' do
+ expect(gitlab_projects).to receive(:delete_remote_branches)
+ .with('downstream-remote', ['master'])
+ .and_return(true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'raises an error if the command fails' do
+ allow(gitlab_projects).to receive(:output) { 'error' }
+ expect(gitlab_projects).to receive(:delete_remote_branches)
+ .with('downstream-remote', ['master'])
+ .and_return(false)
+
+ expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
+ end
+ end
+ end
+
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c9643c5da47..4290fbb0087 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -193,7 +193,15 @@ describe Gitlab::GitAccess do
let(:actor) { build(:rsa_deploy_key_2048, user: user) }
end
- describe '#check_project_moved!' do
+ shared_examples 'check_project_moved' do
+ it 'enqueues a redirected message' do
+ push_access_check
+
+ expect(Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)).not_to be_nil
+ end
+ end
+
+ describe '#check_project_moved!', :clean_gitlab_redis_shared_state do
before do
project.add_master(user)
end
@@ -207,7 +215,40 @@ describe Gitlab::GitAccess do
end
end
- context 'when a redirect was followed to find the project' do
+ context 'when a permanent redirect and ssh protocol' do
+ let(:redirected_path) { 'some/other-path' }
+
+ before do
+ allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
+ end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'check_project_moved'
+ end
+
+ context 'with a permanent redirect and http protocol' do
+ let(:redirected_path) { 'some/other-path' }
+ let(:protocol) { 'http' }
+
+ before do
+ allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
+ end
+
+ it 'allows_push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'check_project_moved'
+ end
+
+ context 'with a temporal redirect and ssh protocol' do
let(:redirected_path) { 'some/other-path' }
it 'blocks push and pull access' do
@@ -219,23 +260,22 @@ describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
end
end
+ end
- context 'http protocol' do
- let(:protocol) { 'http' }
+ context 'with a temporal redirect and http protocol' do
+ let(:redirected_path) { 'some/other-path' }
+ let(:protocol) { 'http' }
- it 'includes the path to the project using HTTP' do
- aggregate_failures do
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- end
- end
+ it 'does not allow to push and pull access' do
+ expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
+ expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
end
end
end
describe '#check_command_disabled!' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
context 'over http' do
@@ -364,7 +404,7 @@ describe Gitlab::GitAccess do
describe 'reporter user' do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
context 'pull code' do
@@ -377,7 +417,7 @@ describe Gitlab::GitAccess do
context 'when member of the project' do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
context 'pull code' do
@@ -457,7 +497,7 @@ describe Gitlab::GitAccess do
if role == :admin
user.update_attribute(:admin, true)
else
- project.team << [user, role]
+ project.add_role(user, role)
end
aggregate_failures do
@@ -618,7 +658,7 @@ describe Gitlab::GitAccess do
context 'when project is authorized' do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) }
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 1056074264a..186b2d9279d 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::GitAccessWiki do
context 'when user can :create_wiki' do
before do
create(:protected_branch, name: 'master', project: project)
- project.team << [user, :developer]
+ project.add_developer(user)
end
subject { access.check('git-receive-pack', changes) }
@@ -41,7 +41,7 @@ describe Gitlab::GitAccessWiki do
subject { access.check('git-upload-pack', '_any') }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when wiki feature is enabled' do
diff --git a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
new file mode 100644
index 00000000000..b9641de7eda
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::ConflictsService do
+ let(:project) { create(:project, :repository) }
+ let(:target_project) { create(:project, :repository) }
+ let(:source_repository) { project.repository.raw }
+ let(:target_repository) { target_project.repository.raw }
+ let(:target_gitaly_repository) { target_repository.gitaly_repository }
+ let(:our_commit_oid) { 'f00' }
+ let(:their_commit_oid) { 'f44' }
+ let(:client) do
+ described_class.new(target_repository, our_commit_oid, their_commit_oid)
+ end
+
+ describe '#list_conflict_files' do
+ let(:request) do
+ Gitaly::ListConflictFilesRequest.new(
+ repository: target_gitaly_repository, our_commit_oid: our_commit_oid,
+ their_commit_oid: their_commit_oid
+ )
+ end
+ let(:our_path) { 'our/path' }
+ let(:their_path) { 'their/path' }
+ let(:our_mode) { 0744 }
+ let(:header) do
+ double(repository: target_gitaly_repository, commit_oid: our_commit_oid,
+ our_path: our_path, our_mode: 0744, their_path: their_path)
+ end
+ let(:response) do
+ [
+ double(files: [double(header: header), double(content: 'foo', header: nil)]),
+ double(files: [double(content: 'bar', header: nil)])
+ ]
+ end
+ let(:file) { subject[0] }
+
+ subject { client.list_conflict_files }
+
+ it 'sends an RPC request' do
+ expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:list_conflict_files)
+ .with(request, kind_of(Hash)).and_return([])
+
+ subject
+ end
+
+ it 'forms a Gitlab::Git::ConflictFile collection from the response' do
+ allow_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:list_conflict_files)
+ .with(request, kind_of(Hash)).and_return(response)
+
+ expect(subject.size).to be(1)
+ expect(file.content).to eq('foobar')
+ expect(file.their_path).to eq(their_path)
+ expect(file.our_path).to eq(our_path)
+ expect(file.our_mode).to be(our_mode)
+ expect(file.repository).to eq(target_repository)
+ expect(file.commit_oid).to eq(our_commit_oid)
+ end
+ end
+
+ describe '#resolve_conflicts' do
+ let(:user) { create(:user) }
+ let(:files) do
+ [{ old_path: 'some/path', new_path: 'some/path', content: '' }]
+ end
+ let(:source_branch) { 'master' }
+ let(:target_branch) { 'feature' }
+ let(:commit_message) { 'Solving conflicts' }
+ let(:resolution) do
+ Gitlab::Git::Conflict::Resolution.new(user, files, commit_message)
+ end
+
+ subject do
+ client.resolve_conflicts(source_repository, resolution, source_branch, target_branch)
+ end
+
+ it 'sends an RPC request' do
+ expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:resolve_conflicts)
+ .with(kind_of(Enumerator), kind_of(Hash)).and_return(double(resolution_error: ""))
+
+ subject
+ end
+
+ it 'raises a relevant exception if resolution_error is present' do
+ expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:resolve_conflicts)
+ .with(kind_of(Enumerator), kind_of(Hash)).and_return(double(resolution_error: "something happened"))
+
+ expect { subject }.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
new file mode 100644
index 00000000000..9d540446532
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::RemoteService do
+ let(:project) { create(:project) }
+ let(:storage_name) { project.repository_storage }
+ let(:relative_path) { project.disk_path + '.git' }
+ let(:remote_name) { 'my-remote' }
+ let(:client) { described_class.new(project.repository) }
+
+ describe '#add_remote' do
+ let(:url) { 'http://my-repo.git' }
+ let(:mirror_refmap) { :all_refs }
+
+ it 'sends an add_remote message' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:add_remote)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(:add_remote_response))
+
+ client.add_remote(remote_name, url, mirror_refmap)
+ end
+ end
+
+ describe '#remove_remote' do
+ it 'sends an remove_remote message and returns the result value' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:remove_remote)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(result: true))
+
+ expect(client.remove_remote(remote_name)).to be(true)
+ end
+ end
+
+ describe '#fetch_internal_remote' do
+ let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+
+ it 'sends an fetch_internal_remote message and returns the result value' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:fetch_internal_remote)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(result: true))
+
+ expect(client.fetch_internal_remote(remote_repository)).to be(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index a871ed0df0e..309b7338ef0 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -38,20 +38,6 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do
end
end
- describe 'encode' do
- [
- [nil, ""],
- ["", ""],
- [" ", " "],
- %w(a1 a1),
- ["ç¼–ç ", "\xE7\xBC\x96\xE7\xA0\x81".b]
- ].each do |input, result|
- it "encodes #{input.inspect} to #{result.inspect}" do
- expect(described_class.encode(input)).to eq result
- end
- end
- end
-
describe 'allow_n_plus_1_calls' do
context 'when RequestStore is enabled', :request_store do
it 'returns the result of the allow_n_plus_1_calls block' do
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 168e5d07504..46a57e08963 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -70,7 +70,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
describe '#execute' do
it 'imports the repository and wiki' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(true)
@@ -93,7 +93,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'does not import the repository if it already exists' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(false)
@@ -115,7 +115,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'does not import the wiki if it is disabled' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(true)
@@ -137,7 +137,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'does not import the wiki if the repository could not be imported' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(true)
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 798ea0bac58..017facd0f5e 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.team << [project.creator, :master]
+ project.add_master(project.creator)
project.create_import_data(data: import_data)
end
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index cfaeb1f0d4f..0385dd762c2 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -70,6 +70,10 @@ describe Gitlab::Identifier do
expect(identifier.identify_using_commit(project, '123')).to eq(user)
end
end
+
+ it 'returns nil if the project & ref are not present' do
+ expect(identifier.identify_using_commit(nil, nil)).to be_nil
+ end
end
describe '#identify_using_user' do
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index f0752649121..7580b62cfc0 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6465,78 +6465,100 @@
}
}
],
- "statuses": [
- {
- "id": 71,
- "project_id": 5,
- "status": "failed",
- "finished_at": "2016-03-29T06:28:12.630Z",
- "trace": null,
- "created_at": "2016-03-22T15:20:35.772Z",
- "updated_at": "2016-03-29T06:28:12.634Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 36,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 1",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": null
- },
- "artifacts_metadata": {
- "url": null
- },
- "erased_by_id": null,
- "erased_at": null,
- "type": "Ci::Build",
- "token": "abcd"
- },
- {
- "id": 72,
- "project_id": 5,
- "status": "success",
- "finished_at": null,
- "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.",
- "created_at": "2016-03-22T15:20:35.777Z",
- "updated_at": "2016-03-22T15:20:35.777Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 36,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 2",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
- },
- "artifacts_metadata": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
- },
- "erased_by_id": null,
- "erased_at": null
+ "stages": [
+ {
+ "id": 11,
+ "project_id": 5,
+ "pipeline_id": 36,
+ "name": "test",
+ "status": 1,
+ "created_at": "2016-03-22T15:44:44.772Z",
+ "updated_at": "2016-03-29T06:44:44.634Z",
+ "statuses": [
+ {
+ "id": 71,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": "2016-03-29T06:28:12.630Z",
+ "trace": null,
+ "created_at": "2016-03-22T15:20:35.772Z",
+ "updated_at": "2016-03-29T06:28:12.634Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 36,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "stage_id": 11,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null,
+ "type": "Ci::Build",
+ "token": "abcd"
+ },
+ {
+ "id": 72,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.",
+ "created_at": "2016-03-22T15:20:35.777Z",
+ "updated_at": "2016-03-22T15:20:35.777Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 36,
+ "commands": "$ deploy command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "deploy",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "stage_id": 12,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
+ },
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 12,
+ "project_id": 5,
+ "pipeline_id": 36,
+ "name": "deploy",
+ "status": 2,
+ "created_at": "2016-03-22T15:45:45.772Z",
+ "updated_at": "2016-03-29T06:45:45.634Z"
}
]
},
@@ -6556,76 +6578,87 @@
"started_at": null,
"finished_at": null,
"duration": null,
- "statuses": [
- {
- "id": 74,
- "project_id": 5,
- "status": "success",
- "finished_at": null,
- "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.",
- "created_at": "2016-03-22T15:20:35.846Z",
- "updated_at": "2016-03-22T15:20:35.846Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 37,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 2",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
- },
- "artifacts_metadata": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
- },
- "erased_by_id": null,
- "erased_at": null
- },
- {
- "id": 73,
- "project_id": 5,
- "status": "canceled",
- "finished_at": null,
- "trace": null,
- "created_at": "2016-03-22T15:20:35.842Z",
- "updated_at": "2016-03-22T15:20:35.842Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 37,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 1",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": null
- },
- "artifacts_metadata": {
- "url": null
- },
- "erased_by_id": null,
- "erased_at": null
+ "stages": [
+ {
+ "id": 21,
+ "project_id": 5,
+ "pipeline_id": 37,
+ "name": "test",
+ "status": 1,
+ "created_at": "2016-03-22T15:44:44.772Z",
+ "updated_at": "2016-03-29T06:44:44.634Z",
+ "statuses": [
+ {
+ "id": 74,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.",
+ "created_at": "2016-03-22T15:20:35.846Z",
+ "updated_at": "2016-03-22T15:20:35.846Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 37,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
+ },
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 73,
+ "project_id": 5,
+ "status": "canceled",
+ "finished_at": null,
+ "trace": null,
+ "created_at": "2016-03-22T15:20:35.842Z",
+ "updated_at": "2016-03-22T15:20:35.842Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 37,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
}
]
},
@@ -6645,76 +6678,87 @@
"started_at": null,
"finished_at": null,
"duration": null,
- "statuses": [
- {
- "id": 76,
- "project_id": 5,
- "status": "success",
- "finished_at": null,
- "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.",
- "created_at": "2016-03-22T15:20:35.882Z",
- "updated_at": "2016-03-22T15:20:35.882Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 38,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 2",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
- },
- "artifacts_metadata": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
- },
- "erased_by_id": null,
- "erased_at": null
- },
- {
- "id": 75,
- "project_id": 5,
- "status": "failed",
- "finished_at": null,
- "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.",
- "created_at": "2016-03-22T15:20:35.864Z",
- "updated_at": "2016-03-22T15:20:35.864Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 38,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 1",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
- },
- "artifacts_metadata": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
- },
- "erased_by_id": null,
- "erased_at": null
+ "stages": [
+ {
+ "id": 22,
+ "project_id": 5,
+ "pipeline_id": 38,
+ "name": "test",
+ "status": 1,
+ "created_at": "2016-03-22T15:44:44.772Z",
+ "updated_at": "2016-03-29T06:44:44.634Z",
+ "statuses": [
+ {
+ "id": 76,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.",
+ "created_at": "2016-03-22T15:20:35.882Z",
+ "updated_at": "2016-03-22T15:20:35.882Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 38,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
+ },
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 75,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": null,
+ "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.",
+ "created_at": "2016-03-22T15:20:35.864Z",
+ "updated_at": "2016-03-22T15:20:35.864Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 38,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
+ },
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
}
]
},
@@ -6734,76 +6778,87 @@
"started_at": null,
"finished_at": null,
"duration": null,
- "statuses": [
- {
- "id": 78,
- "project_id": 5,
- "status": "success",
- "finished_at": null,
- "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.",
- "created_at": "2016-03-22T15:20:35.927Z",
- "updated_at": "2016-03-22T15:20:35.927Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 39,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 2",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
- },
- "artifacts_metadata": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
- },
- "erased_by_id": null,
- "erased_at": null
- },
- {
- "id": 77,
- "project_id": 5,
- "status": "failed",
- "finished_at": null,
- "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.",
- "created_at": "2016-03-22T15:20:35.905Z",
- "updated_at": "2016-03-22T15:20:35.905Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 39,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 1",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
- },
- "artifacts_metadata": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
- },
- "erased_by_id": null,
- "erased_at": null
+ "stages": [
+ {
+ "id": 23,
+ "project_id": 5,
+ "pipeline_id": 39,
+ "name": "test",
+ "status": 1,
+ "created_at": "2016-03-22T15:44:44.772Z",
+ "updated_at": "2016-03-29T06:44:44.634Z",
+ "statuses": [
+ {
+ "id": 78,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.",
+ "created_at": "2016-03-22T15:20:35.927Z",
+ "updated_at": "2016-03-22T15:20:35.927Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 39,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
+ },
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 77,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": null,
+ "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.",
+ "created_at": "2016-03-22T15:20:35.905Z",
+ "updated_at": "2016-03-22T15:20:35.905Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 39,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
+ },
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
}
]
},
@@ -6823,76 +6878,87 @@
"started_at": null,
"finished_at": null,
"duration": null,
- "statuses": [
- {
- "id": 79,
- "project_id": 5,
- "status": "failed",
- "finished_at": "2016-03-29T06:28:12.695Z",
- "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.",
- "created_at": "2016-03-22T15:20:35.950Z",
- "updated_at": "2016-03-29T06:28:12.696Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 40,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 1",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": null
- },
- "artifacts_metadata": {
- "url": null
- },
- "erased_by_id": null,
- "erased_at": null
- },
- {
- "id": 80,
- "project_id": 5,
- "status": "success",
- "finished_at": null,
- "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.",
- "created_at": "2016-03-22T15:20:35.966Z",
- "updated_at": "2016-03-22T15:20:35.966Z",
- "started_at": null,
- "runner_id": null,
- "coverage": null,
- "commit_id": 40,
- "commands": "$ build command",
- "job_id": null,
- "name": "test build 2",
- "deploy": false,
- "options": null,
- "allow_failure": false,
- "stage": "test",
- "trigger_request_id": null,
- "stage_idx": 1,
- "tag": null,
- "ref": "master",
- "user_id": null,
- "target_url": null,
- "description": null,
- "artifacts_file": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
- },
- "artifacts_metadata": {
- "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
- },
- "erased_by_id": null,
- "erased_at": null
+ "stages": [
+ {
+ "id": 24,
+ "project_id": 5,
+ "pipeline_id": 40,
+ "name": "test",
+ "status": 1,
+ "created_at": "2016-03-22T15:44:44.772Z",
+ "updated_at": "2016-03-29T06:44:44.634Z",
+ "statuses": [
+ {
+ "id": 79,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": "2016-03-29T06:28:12.695Z",
+ "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.",
+ "created_at": "2016-03-22T15:20:35.950Z",
+ "updated_at": "2016-03-29T06:28:12.696Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 40,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 80,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.",
+ "created_at": "2016-03-22T15:20:35.966Z",
+ "updated_at": "2016-03-22T15:20:35.966Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 40,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
+ },
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
}
]
}
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 0ab3afd0074..9dfd879a1bc 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -179,6 +179,32 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
end
+
+ context 'when restoring hierarchy of pipeline, stages and jobs' do
+ it 'restores pipelines' do
+ expect(Ci::Pipeline.all.count).to be 5
+ end
+
+ it 'restores pipeline stages' do
+ expect(Ci::Stage.all.count).to be 6
+ end
+
+ it 'correctly restores association between stage and a pipeline' do
+ expect(Ci::Stage.all).to all(have_attributes(pipeline_id: a_value > 0))
+ end
+
+ it 'restores statuses' do
+ expect(CommitStatus.all.count).to be 10
+ end
+
+ it 'correctly restores association between a stage and a job' do
+ expect(CommitStatus.all).to all(have_attributes(stage_id: a_value > 0))
+ end
+
+ it 'correctly restores association between a pipeline and a job' do
+ expect(CommitStatus.all).to all(have_attributes(pipeline_id: a_value > 0))
+ 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 6243b6ac9f0..08e5bbbd400 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.team << [user, :master]
+ project.add_master(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')
@@ -109,12 +109,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty
end
+ it 'has pipeline stages' do
+ expect(saved_project_json.dig('pipelines', 0, 'stages')).not_to be_empty
+ end
+
it 'has pipeline statuses' do
- expect(saved_project_json['pipelines'].first['statuses']).not_to be_empty
+ expect(saved_project_json.dig('pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty
end
it 'has pipeline builds' do
- expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1)
+ builds_count = saved_project_json
+ .dig('pipelines', 0, 'stages', 0, 'statuses')
+ .count { |hash| hash['type'] == 'Ci::Build' }
+
+ expect(builds_count).to eq(1)
end
it 'has no when YML attributes but only the DB column' do
diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb
index e6ad516deef..44f972fe530 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.team << [user, :master]
+ project.add_master(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 1016a7ab591..1a554bd1a4e 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -461,6 +461,7 @@ Project:
- delete_error
- merge_requests_ff_only_enabled
- merge_requests_rebase_enabled
+- jobs_cache_index
Author:
- name
ProjectFeature:
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 0e55993c8ef..1d1e7e7f89a 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.team << [user, :master]
+ project.add_master(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/kubernetes/helm_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 15f99b0401f..69112fe90b1 100644
--- a/spec/lib/gitlab/kubernetes/helm_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -1,22 +1,23 @@
require 'spec_helper'
-describe Gitlab::Kubernetes::Helm do
+describe Gitlab::Kubernetes::Helm::Api do
let(:client) { double('kubernetes client') }
let(:helm) { described_class.new(client) }
- let(:namespace) { Gitlab::Kubernetes::Namespace.new(described_class::NAMESPACE, client) }
+ let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
+ let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) }
let(:install_helm) { true }
let(:chart) { 'stable/a_chart' }
let(:application_name) { 'app_name' }
- let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm, chart) }
+ let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm: install_helm, chart: chart) }
subject { helm }
before do
- allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client).and_return(namespace)
+ allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client).and_return(namespace)
end
describe '#initialize' do
it 'creates a namespace object' do
- expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client)
+ expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client)
subject
end
@@ -41,7 +42,7 @@ describe Gitlab::Kubernetes::Helm do
let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation
it 'fetches POD phase from kubernetes cluster' do
- expect(client).to receive(:get_pod).with(command.pod_name, described_class::NAMESPACE).once.and_return(pod)
+ expect(client).to receive(:get_pod).with(command.pod_name, gitlab_namespace).once.and_return(pod)
expect(subject.installation_status(command.pod_name)).to eq(phase)
end
@@ -52,7 +53,7 @@ describe Gitlab::Kubernetes::Helm do
let(:response) { RestClient::Response.new(log) }
it 'fetches POD phase from kubernetes cluster' do
- expect(client).to receive(:get_pod_log).with(command.pod_name, described_class::NAMESPACE).once.and_return(response)
+ expect(client).to receive(:get_pod_log).with(command.pod_name, gitlab_namespace).once.and_return(response)
expect(subject.installation_log(command.pod_name)).to eq(log)
end
@@ -60,41 +61,9 @@ describe Gitlab::Kubernetes::Helm do
describe '#delete_installation_pod!' do
it 'deletes the POD from kubernetes cluster' do
- expect(client).to receive(:delete_pod).with(command.pod_name, described_class::NAMESPACE).once
+ expect(client).to receive(:delete_pod).with(command.pod_name, gitlab_namespace).once
subject.delete_installation_pod!(command.pod_name)
end
end
-
- describe '#helm_init_command' do
- subject { helm.send(:helm_init_command, command) }
-
- context 'when command.install_helm is true' do
- let(:install_helm) { true }
-
- it { is_expected.to eq('helm init >/dev/null') }
- end
-
- context 'when command.install_helm is false' do
- let(:install_helm) { false }
-
- it { is_expected.to eq('helm init --client-only >/dev/null') }
- end
- end
-
- describe '#helm_install_command' do
- subject { helm.send(:helm_install_command, command) }
-
- context 'when command.chart is nil' do
- let(:chart) { nil }
-
- it { is_expected.to be_nil }
- end
-
- context 'when command.chart is set' do
- let(:chart) { 'stable/a_chart' }
-
- it { is_expected.to eq("helm install #{chart} --name #{application_name} --namespace #{namespace.name} >/dev/null")}
- end
- end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
new file mode 100644
index 00000000000..4afe48e72ad
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -0,0 +1,111 @@
+require 'rails_helper'
+
+describe Gitlab::Kubernetes::Helm::InstallCommand do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ describe "#initialize" do
+ context "With all the params" do
+ subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) }
+
+ it 'should assign all parameters' do
+ expect(subject.name).to eq(prometheus.name)
+ expect(subject.install_helm).to be_truthy
+ expect(subject.chart).to eq(prometheus.chart)
+ expect(subject.chart_values_file).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
+ end
+ end
+
+ context 'when install_helm is not set' do
+ subject { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: true) }
+
+ it 'should set install_helm as false' do
+ expect(subject.install_helm).to be_falsy
+ end
+ end
+
+ context 'when chart is not set' do
+ subject { described_class.new(prometheus.name, install_helm: true) }
+
+ it 'should set chart as nil' do
+ expect(subject.chart).to be_falsy
+ end
+ end
+
+ context 'when chart_values_file is not set' do
+ subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart) }
+
+ it 'should set chart_values_file as nil' do
+ expect(subject.chart_values_file).to be_falsy
+ end
+ end
+ end
+
+ describe "#generate_script" do
+ let(:install_command) { described_class.new(prometheus.name, install_helm: install_helm) }
+ let(:client) { double('kubernetes client') }
+ let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) }
+ subject { install_command.send(:generate_script, namespace.name) }
+
+ context 'when install helm is true' do
+ let(:install_helm) { true }
+ let(:command) do
+ <<~MSG
+ set -eo pipefail
+ apk add -U 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/
+
+ helm init >/dev/null
+ MSG
+ end
+
+ it 'should return appropriate command' do
+ is_expected.to eq(command)
+ end
+ end
+
+ context 'when install helm is false' do
+ let(:install_helm) { false }
+ let(:command) do
+ <<~MSG
+ set -eo pipefail
+ apk add -U 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/
+
+ helm init --client-only >/dev/null
+ MSG
+ end
+
+ it 'should return appropriate command' do
+ is_expected.to eq(command)
+ end
+ end
+
+ context 'when chart is present' do
+ let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart) }
+ let(:command) do
+ <<~MSG.chomp
+ set -eo pipefail
+ apk add -U 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/
+
+ helm init --client-only >/dev/null
+ helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} >/dev/null
+ MSG
+ end
+
+ it 'should return appropriate command' do
+ is_expected.to eq(command)
+ end
+ end
+ end
+
+ describe "#pod_name" do
+ let(:install_command) { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: true) }
+ subject { install_command.send(:pod_name) }
+
+ it { is_expected.to eq('install-prometheus') }
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
new file mode 100644
index 00000000000..906b10b96d4
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -0,0 +1,86 @@
+require 'rails_helper'
+
+describe Gitlab::Kubernetes::Helm::Pod do
+ describe '#generate' do
+ let(:cluster) { create(:cluster) }
+ let(:app) { create(:clusters_applications_prometheus, cluster: cluster) }
+ let(:command) { app.install_command }
+ let(:client) { double('kubernetes client') }
+ let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) }
+ subject { described_class.new(command, namespace.name, client) }
+
+ before do
+ allow(client).to receive(:create_config_map).and_return(nil)
+ end
+
+ shared_examples 'helm pod' do
+ it 'should generate a Kubeclient::Resource' do
+ expect(subject.generate).to be_a_kind_of(Kubeclient::Resource)
+ end
+
+ it 'should generate the appropriate metadata' do
+ metadata = subject.generate.metadata
+ expect(metadata.name).to eq("install-#{app.name}")
+ expect(metadata.namespace).to eq('gitlab-managed-apps')
+ expect(metadata.labels['gitlab.org/action']).to eq('install')
+ expect(metadata.labels['gitlab.org/application']).to eq(app.name)
+ end
+
+ it 'should generate a container spec' do
+ spec = subject.generate.spec
+ expect(spec.containers.count).to eq(1)
+ end
+
+ it 'should generate the appropriate specifications for the container' do
+ container = subject.generate.spec.containers.first
+ expect(container.name).to eq('helm')
+ expect(container.image).to eq('alpine:3.6')
+ expect(container.env.count).to eq(3)
+ expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
+ expect(container.command).to match_array(["/bin/sh"])
+ expect(container.args).to match_array(["-c", "$(COMMAND_SCRIPT)"])
+ end
+
+ it 'should include a never restart policy' do
+ spec = subject.generate.spec
+ expect(spec.restartPolicy).to eq('Never')
+ end
+ end
+
+ context 'with a configuration file' do
+ it_behaves_like 'helm pod'
+
+ it 'should include volumes for the container' do
+ container = subject.generate.spec.containers.first
+ expect(container.volumeMounts.first['name']).to eq('config-volume')
+ expect(container.volumeMounts.first['mountPath']).to eq('/etc/config')
+ end
+
+ it 'should include a volume inside the specification' do
+ spec = subject.generate.spec
+ expect(spec.volumes.first['name']).to eq('config-volume')
+ end
+
+ it 'should mount configMap specification in the volume' do
+ spec = subject.generate.spec
+ expect(spec.volumes.first.configMap['name']).to eq('values-config')
+ end
+ end
+
+ context 'without a configuration file' do
+ let(:app) { create(:clusters_applications_ingress, cluster: cluster) }
+
+ it_behaves_like 'helm pod'
+
+ it 'should not include volumeMounts inside the container' do
+ container = subject.generate.spec.containers.first
+ expect(container.volumeMounts).to be_nil
+ end
+
+ it 'should not a volume inside the specification' do
+ spec = subject.generate.spec
+ expect(spec.volumes).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb
index d9ddb4326be..6132abd9b35 100644
--- a/spec/lib/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/ldap/adapter_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::LDAP::Adapter do
expect(adapter).to receive(:ldap_search) do |arg|
expect(arg[:filter].to_s).to eq('(uid=johndoe)')
expect(arg[:base]).to eq('dc=example,dc=com')
- expect(arg[:attributes]).to match(%w{dn uid cn mail email userPrincipalName})
+ expect(arg[:attributes]).to match(ldap_attributes)
end.and_return({})
adapter.users('uid', 'johndoe')
@@ -26,7 +26,7 @@ describe Gitlab::LDAP::Adapter do
expect(adapter).to receive(:ldap_search).with(
base: 'uid=johndoe,ou=users,dc=example,dc=com',
scope: Net::LDAP::SearchScope_BaseObject,
- attributes: %w{dn uid cn mail email userPrincipalName},
+ attributes: ldap_attributes,
filter: nil
).and_return({})
@@ -63,7 +63,7 @@ describe Gitlab::LDAP::Adapter do
it 'uses the right uid attribute when non-default' do
stub_ldap_config(uid: 'sAMAccountName')
expect(adapter).to receive(:ldap_search).with(
- hash_including(attributes: %w{dn sAMAccountName cn mail email userPrincipalName})
+ hash_including(attributes: ldap_attributes)
).and_return({})
adapter.users('sAMAccountName', 'johndoe')
@@ -137,4 +137,8 @@ describe Gitlab::LDAP::Adapter do
end
end
end
+
+ def ldap_attributes
+ Gitlab::LDAP::Person.ldap_attributes(Gitlab::LDAP::Config.new('ldapmain'))
+ end
end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb
index d204050ef66..ff29d9aa5be 100644
--- a/spec/lib/gitlab/ldap/person_spec.rb
+++ b/spec/lib/gitlab/ldap/person_spec.rb
@@ -8,13 +8,16 @@ describe Gitlab::LDAP::Person do
before do
stub_ldap_config(
options: {
+ 'uid' => 'uid',
'attributes' => {
- 'name' => 'cn',
- 'email' => %w(mail email userPrincipalName)
+ 'name' => 'cn',
+ 'email' => %w(mail email userPrincipalName),
+ 'username' => username_attribute
}
}
)
end
+ let(:username_attribute) { %w(uid sAMAccountName userid) }
describe '.normalize_dn' do
subject { described_class.normalize_dn(given) }
@@ -44,6 +47,34 @@ describe Gitlab::LDAP::Person do
end
end
+ describe '.ldap_attributes' do
+ it 'returns a compact and unique array' do
+ stub_ldap_config(
+ options: {
+ 'uid' => nil,
+ 'attributes' => {
+ 'name' => 'cn',
+ 'email' => 'mail',
+ 'username' => %w(uid mail memberof)
+ }
+ }
+ )
+ config = Gitlab::LDAP::Config.new('ldapmain')
+ ldap_attributes = described_class.ldap_attributes(config)
+
+ expect(ldap_attributes).to match_array(%w(dn uid cn mail memberof))
+ end
+ end
+
+ describe '.validate_entry' do
+ it 'raises InvalidEntryError' do
+ entry['foo'] = 'bar'
+
+ expect { described_class.new(entry, 'ldapmain') }
+ .to raise_error(Gitlab::LDAP::Person::InvalidEntryError)
+ end
+ end
+
describe '#name' do
it 'uses the configured name attribute and handles values as an array' do
name = 'John Doe'
@@ -72,6 +103,44 @@ describe Gitlab::LDAP::Person do
end
end
+ describe '#username' do
+ context 'with default uid username attribute' do
+ let(:username_attribute) { 'uid' }
+
+ it 'returns the proper username value' do
+ attr_value = 'johndoe'
+ entry[username_attribute] = attr_value
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.username).to eq(attr_value)
+ end
+ end
+
+ context 'with a different username attribute' do
+ let(:username_attribute) { 'sAMAccountName' }
+
+ it 'returns the proper username value' do
+ attr_value = 'johndoe'
+ entry[username_attribute] = attr_value
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.username).to eq(attr_value)
+ end
+ end
+
+ context 'with a non-standard username attribute' do
+ let(:username_attribute) { 'mail' }
+
+ it 'returns the proper username value' do
+ attr_value = 'john.doe@example.com'
+ entry[username_attribute] = attr_value
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.username).to eq(attr_value)
+ end
+ end
+ end
+
def assert_generic_test(test_description, got, expected)
test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}"
expect(got).to eq(expected), test_failure_message
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 260df6e4dae..048caa38fcf 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -38,7 +38,6 @@ describe Gitlab::LDAP::User do
it "does not mark existing ldap user as changed" do
create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
- ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true)
expect(ldap_user.changed?).to be_falsey
end
end
@@ -144,11 +143,15 @@ describe Gitlab::LDAP::User do
expect(ldap_user.gl_user.email).to eq(info[:email])
end
- it "has user_synced_attributes_metadata email set to true" do
+ it "has email set as synced" do
expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
end
- it "has synced_attribute_provider set to ldapmain" do
+ it "has email set as read-only" do
+ expect(ldap_user.gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to ldapmain" do
expect(ldap_user.gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain'
end
end
@@ -162,9 +165,13 @@ describe Gitlab::LDAP::User do
expect(ldap_user.gl_user.temp_oauth_email?).to be_truthy
end
- it "has synced attribute email set to false" do
+ it "has email set as not synced" do
expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
end
+
+ it "does not have email set as read-only" do
+ expect(ldap_user.gl_user.read_only_attribute?(:email)).to be_falsey
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 5341addf911..41a9d1d9c90 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -20,9 +20,39 @@ describe Gitlab::Metrics::MethodCall do
context 'prometheus instrumentation is enabled' do
before do
+ allow(Feature.get(:prometheus_metrics_method_instrumentation)).to receive(:enabled?).and_call_original
+ described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1
Feature.get(:prometheus_metrics_method_instrumentation).enable
end
+ around do |example|
+ Timecop.freeze do
+ example.run
+ end
+ end
+
+ it 'caches subsequent invocations of feature check' do
+ 10.times do
+ method_call.measure { 'foo' }
+ end
+
+ expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).once
+ end
+
+ it 'expires feature check cache after 1 minute' do
+ method_call.measure { 'foo' }
+
+ Timecop.travel(1.minute.from_now) do
+ method_call.measure { 'foo' }
+ end
+
+ Timecop.travel(1.minute.from_now + 1.second) do
+ method_call.measure { 'foo' }
+ end
+
+ expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).twice
+ end
+
it 'observes the performance of the supplied block' do
expect(described_class.call_duration_histogram)
.to receive(:observe)
@@ -34,6 +64,8 @@ describe Gitlab::Metrics::MethodCall do
context 'prometheus instrumentation is disabled' do
before do
+ described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1
+
Feature.get(:prometheus_metrics_method_instrumentation).disable
end
@@ -64,14 +96,17 @@ describe Gitlab::Metrics::MethodCall do
describe '#to_metric' do
it 'returns a Metric instance' do
+ expect(method_call).to receive(:real_time).and_return(4.0001)
+ expect(method_call).to receive(:cpu_time).and_return(3.0001)
+
method_call.measure { 'foo' }
metric = method_call.to_metric
expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
expect(metric.series).to eq('rails_method_calls')
- expect(metric.values[:duration]).to be_a_kind_of(Numeric)
- expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
+ expect(metric.values[:duration]).to eq(4000)
+ expect(metric.values[:cpu_duration]).to eq(3000)
expect(metric.values[:call_count]).to be_an(Integer)
expect(metric.tags).to eq({ method: 'Foo#bar' })
@@ -84,13 +119,13 @@ describe Gitlab::Metrics::MethodCall do
end
it 'returns false when the total call time is not above the threshold' do
- expect(method_call).to receive(:real_time).and_return(9)
+ expect(method_call).to receive(:real_time).and_return(0.009)
expect(method_call.above_threshold?).to eq(false)
end
it 'returns true when the total call time is above the threshold' do
- expect(method_call).to receive(:real_time).and_return(9000)
+ expect(method_call).to receive(:real_time).and_return(9)
expect(method_call.above_threshold?).to eq(true)
end
diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
index 667e4747897..f66451c5188 100644
--- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
@@ -21,7 +21,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do
it 'samples various statistics' do
expect(sampler).to receive(:sample_memory_usage)
expect(sampler).to receive(:sample_file_descriptors)
- expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
expect(sampler).to receive(:flush)
@@ -72,28 +71,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do
end
end
- if Gitlab::Metrics.mri?
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler).to receive(:add_metric)
- .with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash))
- .at_least(:once)
- .and_call_original
-
- sampler.sample_objects
- end
-
- it 'ignores classes without a name' do
- expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
-
- expect(sampler).not_to receive(:add_metric)
- .with('object_counts', an_instance_of(Hash), type: nil)
-
- sampler.sample_objects
- end
- end
- end
-
describe '#sample_gc' do
it 'adds a metric containing garbage collection statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 53699327da1..375cbf8a9ca 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -11,7 +11,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do
it 'samples various statistics' do
expect(Gitlab::Metrics::System).to receive(:memory_usage)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
- expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
sampler.sample
@@ -65,26 +64,4 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
end
-
- if Gitlab::Metrics.mri?
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler.metrics[:objects_total]).to receive(:set)
- .with(include(class: anything), be > 0)
- .at_least(:once)
- .and_call_original
-
- sampler.sample
- end
-
- it 'ignores classes without a name' do
- expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
-
- expect(sampler.metrics[:objects_total]).not_to receive(:set)
- .with(include(class: 'object_counts'), anything)
-
- sampler.sample
- end
- end
- end
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 4d94d8705fb..14afcdf5daa 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -29,19 +29,19 @@ describe Gitlab::Metrics::System do
describe '.cpu_time' do
it 'returns a Fixnum' do
- expect(described_class.cpu_time).to be_an(Integer)
+ expect(described_class.cpu_time).to be_an(Float)
end
end
describe '.real_time' do
it 'returns a Fixnum' do
- expect(described_class.real_time).to be_an(Integer)
+ expect(described_class.real_time).to be_an(Float)
end
end
describe '.monotonic_time' do
- it 'returns a Fixnum' do
- expect(described_class.monotonic_time).to be_an(Integer)
+ it 'returns a Float' do
+ expect(described_class.monotonic_time).to be_an(Float)
end
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 2f19fb7312d..45fff4c5787 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -202,11 +202,13 @@ describe Gitlab::OAuth::User do
end
context "and no account for the LDAP user" do
- it "creates a user with dual LDAP and omniauth identities" do
+ before do
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
oauth_user.save
+ end
+ it "creates a user with dual LDAP and omniauth identities" do
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
expect(gl_user.email).to eql 'johndoe@example.com'
@@ -219,6 +221,18 @@ describe Gitlab::OAuth::User do
]
)
end
+
+ it "has email set as synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
+ end
+
+ it "has email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to ldapmain" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain'
+ end
end
context "and LDAP user has an account already" do
@@ -261,6 +275,26 @@ describe Gitlab::OAuth::User do
end
end
+ context 'and a corresponding LDAP person with a non-default username' do
+ before do
+ allow(ldap_user).to receive(:uid) { uid }
+ allow(ldap_user).to receive(:username) { 'johndoe@example.com' }
+ allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) }
+ allow(ldap_user).to receive(:dn) { dn }
+ end
+
+ context 'and no account for the LDAP user' do
+ it 'creates a user favoring the LDAP username and strips email domain' do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'johndoe'
+ end
+ end
+ end
+
context "and no corresponding LDAP person" do
before do
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil)
@@ -440,11 +474,15 @@ describe Gitlab::OAuth::User do
expect(gl_user.email).to eq(info_hash[:email])
end
- it "has external_attributes set to true" do
- expect(gl_user.user_synced_attributes_metadata).not_to be_nil
+ it "has email set as synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
end
- it "has attributes_provider set to my-provider" do
+ it "has email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to my-provider" do
expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
end
end
@@ -458,10 +496,13 @@ describe Gitlab::OAuth::User do
expect(gl_user.email).not_to eq(info_hash[:email])
end
- it "has user_synced_attributes_metadata set to nil" do
- expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
+ it "has email set as not synced" do
expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
end
+
+ it "does not have email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_falsey
+ end
end
end
@@ -508,11 +549,15 @@ describe Gitlab::OAuth::User do
expect(gl_user.email).to eq(info_hash[:email])
end
- it "has email_synced_attribute set to true" do
+ it "has email set as synced" do
expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true)
end
- it "has my-provider as attributes_provider" do
+ it "has email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to my-provider" do
expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
end
end
@@ -524,7 +569,14 @@ describe Gitlab::OAuth::User do
it "does not update the user email" do
expect(gl_user.email).not_to eq(info_hash[:email])
- expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false)
+ end
+
+ it "has email set as not synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
+ end
+
+ it "does not have email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_falsey
end
end
end
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
index 953cfbb8b88..f3cd6961e94 100644
--- a/spec/lib/gitlab/project_authorizations_spec.rb
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::ProjectAuthorizations do
end
before do
- other_project.team << [user, :reporter]
+ other_project.add_reporter(user)
group.add_developer(user)
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index a424f0f5cfe..17937726f2c 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -179,7 +179,7 @@ describe Gitlab::ProjectSearchResults do
end
it 'does not list project confidential issues for project members with guest role' do
- project.team << [member, :guest]
+ project.add_guest(member)
results = described_class.new(member, project, query)
issues = results.objects('issues')
@@ -211,7 +211,7 @@ describe Gitlab::ProjectSearchResults do
end
it 'lists project confidential issues for project members' do
- project.team << [member, :developer]
+ project.add_developer(member)
results = described_class.new(member, project, query)
issues = results.objects('issues')
@@ -290,12 +290,12 @@ 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.team << [user, :master]
+ private_project.add_master(user)
user
end
let(:team_reporter) do
user = create(:user, username: 'private-project-reporter')
- private_project.team << [user, :reporter]
+ private_project.add_reporter(user)
user
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index ef874368077..4139d1c650c 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ReferenceExtractor do
let(:project) { create(:project) }
before do
- project.team << [project.creator, :developer]
+ project.add_developer(project.creator)
end
subject { described_class.new(project, project.creator) }
@@ -14,8 +14,8 @@ describe Gitlab::ReferenceExtractor do
@u_bar = create(:user, username: 'bar')
@u_offteam = create(:user, username: 'offteam')
- project.team << [@u_foo, :reporter]
- project.team << [@u_bar, :guest]
+ project.add_guest(@u_foo)
+ project.add_guest(@u_bar)
subject.analyze('@foo, @baduser, @bar, and @offteam')
expect(subject.users).to match_array([@u_foo, @u_bar, @u_offteam])
@@ -26,8 +26,8 @@ describe Gitlab::ReferenceExtractor do
@u_bar = create(:user, username: 'bar')
@u_offteam = create(:user, username: 'offteam')
- project.team << [@u_foo, :reporter]
- project.team << [@u_bar, :guest]
+ project.add_reporter(@u_foo)
+ project.add_reporter(@u_bar)
subject.analyze(%Q{
Inline code: `@foo`
@@ -115,6 +115,15 @@ describe Gitlab::ReferenceExtractor do
end
end
+ it 'does not include anchors from table of contents in issue references' do
+ issue1 = create(:issue, project: project)
+ issue2 = create(:issue, project: project)
+
+ subject.analyze("not real issue <h4>#{issue1.iid}</h4>, real issue #{issue2.to_reference}")
+
+ expect(subject.issues).to match_array([issue2])
+ end
+
it 'accesses valid issue objects' do
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
@@ -219,7 +228,7 @@ describe Gitlab::ReferenceExtractor do
let(:issue) { create(:issue, project: other_project) }
before do
- other_project.team << [project.creator, :developer]
+ other_project.add_developer(project.creator)
end
it 'handles project issue references' do
@@ -237,7 +246,7 @@ describe Gitlab::ReferenceExtractor do
let(:text) { "Ref. #{issue.to_reference} and #{label.to_reference}" }
before do
- project.team << [project.creator, :developer]
+ project.add_developer(project.creator)
subject.analyze(text)
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index e44a7c23452..b5a9ac570e6 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::SearchResults do
context 'as a user with access' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe '#projects_count' do
@@ -51,6 +51,38 @@ describe Gitlab::SearchResults do
expect(results.objects('merge_requests')).to include merge_request_2
end
+
+ describe '#merge_requests' do
+ it 'includes project filter by default' do
+ expect(results).to receive(:project_ids_relation).and_call_original
+
+ results.objects('merge_requests')
+ end
+
+ it 'it skips project filter if default project context is used' do
+ allow(results).to receive(:default_project_filter).and_return(true)
+
+ expect(results).not_to receive(:project_ids_relation)
+
+ results.objects('merge_requests')
+ end
+ end
+
+ describe '#issues' do
+ it 'includes project filter by default' do
+ expect(results).to receive(:project_ids_relation).and_call_original
+
+ results.objects('issues')
+ end
+
+ it 'it skips project filter if default project context is used' do
+ allow(results).to receive(:default_project_filter).and_return(true)
+
+ expect(results).not_to receive(:project_ids_relation)
+
+ results.objects('issues')
+ end
+ end
end
it 'does not list issues on private projects' do
@@ -93,8 +125,8 @@ describe Gitlab::SearchResults do
end
it 'does not list confidential issues for project members with guest role' do
- project_1.team << [member, :guest]
- project_2.team << [member, :guest]
+ project_1.add_guest(member)
+ project_2.add_guest(member)
results = described_class.new(member, limit_projects, query)
issues = results.objects('issues')
@@ -135,8 +167,8 @@ describe Gitlab::SearchResults do
end
it 'lists confidential issues for project members' do
- project_1.team << [member, :developer]
- project_2.team << [member, :developer]
+ project_1.add_developer(member)
+ project_2.add_developer(member)
results = described_class.new(member, limit_projects, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index eec6858a5de..81d9e6a8f82 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -2,12 +2,19 @@ require 'spec_helper'
require 'stringio'
describe Gitlab::Shell do
- let(:project) { double('Project', id: 7, path: 'diaspora') }
+ set(:project) { create(:project, :repository) }
+
let(:gitlab_shell) { described_class.new }
let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } }
+ let(:gitlab_projects) { double('gitlab_projects') }
+ let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
before do
allow(Project).to receive(:find).and_return(project)
+
+ allow(gitlab_shell).to receive(:gitlab_projects)
+ .with(project.repository_storage_path, project.disk_path + '.git')
+ .and_return(gitlab_projects)
end
it { is_expected.to respond_to :add_key }
@@ -44,38 +51,6 @@ describe Gitlab::Shell do
end
end
- describe 'projects commands' do
- let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') }
- let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') }
- let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') }
-
- before do
- allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path)
- allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path)
- allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
- end
-
- describe '#mv_repository' do
- it 'executes the command' do
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [projects_path, 'mv-project', 'storage/path', 'project/path.git', 'new/path.git']
- )
- gitlab_shell.mv_repository('storage/path', 'project/path', 'new/path')
- end
- end
-
- describe '#add_key' do
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
- )
-
- gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
- end
- end
- end
-
describe Gitlab::Shell::KeyAdder do
describe '#add_key' do
it 'removes trailing garbage' do
@@ -121,6 +96,17 @@ describe Gitlab::Shell do
allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
end
+ describe '#add_key' do
+ it 'removes trailing garbage' do
+ allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
+ expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
+ [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
+ )
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
+ end
+
describe '#add_repository' do
shared_examples '#add_repository' do
let(:repository_storage) { 'default' }
@@ -162,83 +148,76 @@ describe Gitlab::Shell do
end
describe '#remove_repository' do
+ subject { gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) }
+
it 'returns true when the command succeeds' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
- nil, popen_vars).and_return([nil, 0])
+ expect(gitlab_projects).to receive(:rm_project) { true }
- expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true
+ is_expected.to be_truthy
end
it 'returns false when the command fails' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
- nil, popen_vars).and_return(["error", 1])
+ expect(gitlab_projects).to receive(:rm_project) { false }
- expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false
+ is_expected.to be_falsy
end
end
describe '#mv_repository' do
it 'returns true when the command succeeds' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'],
- nil, popen_vars).and_return([nil, 0])
+ expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { true }
- expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be true
+ expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_truthy
end
it 'returns false when the command fails' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'],
- nil, popen_vars).and_return(["error", 1])
+ expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { false }
- expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be false
+ expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_falsy
end
end
describe '#fork_repository' do
+ subject do
+ gitlab_shell.fork_repository(
+ project.repository_storage_path,
+ project.disk_path,
+ 'new/storage',
+ 'fork/path'
+ )
+ end
+
it 'returns true when the command succeeds' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fork-repository', 'current/storage', 'project/path.git', 'new/storage', 'fork/path.git'],
- nil, popen_vars).and_return([nil, 0])
+ expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { true }
- expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'fork/path')).to be true
+ is_expected.to be_truthy
end
it 'return false when the command fails' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fork-repository', 'current/storage', 'project/path.git', 'new/storage', 'fork/path.git'],
- nil, popen_vars).and_return(["error", 1])
+ expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { false }
- expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'fork/path')).to be false
+ is_expected.to be_falsy
end
end
shared_examples 'fetch_remote' do |gitaly_on|
- let(:project2) { create(:project, :repository) }
- let(:repository) { project2.repository }
+ let(:repository) { project.repository }
def fetch_remote(ssh_auth = nil)
- gitlab_shell.fetch_remote(repository.raw_repository, 'new/storage', ssh_auth: ssh_auth)
+ gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth)
end
- def expect_popen(fail = false, vars = {})
- popen_args = [
- projects_path,
- 'fetch-remote',
- TestEnv.repos_path,
- repository.relative_path,
- 'new/storage',
- Gitlab.config.gitlab_shell.git_timeout.to_s
- ]
-
- return_value = fail ? ["error", 1] : [nil, 0]
+ def expect_gitlab_projects(fail = false, options = {})
+ expect(gitlab_projects).to receive(:fetch_remote).with(
+ 'remote-name',
+ timeout,
+ options
+ ).and_return(!fail)
- expect(Gitlab::Popen).to receive(:popen).with(popen_args, nil, popen_vars.merge(vars)).and_return(return_value)
+ allow(gitlab_projects).to receive(:output).and_return('error') if fail
end
- def expect_gitaly_call(fail, vars = {})
+ def expect_gitaly_call(fail, options = {})
receive_fetch_remote =
if fail
receive(:fetch_remote).and_raise(GRPC::NotFound)
@@ -250,12 +229,12 @@ describe Gitlab::Shell do
end
if gitaly_on
- def expect_call(fail, vars = {})
- expect_gitaly_call(fail, vars)
+ def expect_call(fail, options = {})
+ expect_gitaly_call(fail, options)
end
else
- def expect_call(fail, vars = {})
- expect_popen(fail, vars)
+ def expect_call(fail, options = {})
+ expect_gitlab_projects(fail, options)
end
end
@@ -271,20 +250,27 @@ describe Gitlab::Shell do
end
it 'returns true when the command succeeds' do
- expect_call(false)
+ expect_call(false, force: false, tags: true)
expect(fetch_remote).to be_truthy
end
it 'raises an exception when the command fails' do
- expect_call(true)
+ expect_call(true, force: false, tags: true)
expect { fetch_remote }.to raise_error(Gitlab::Shell::Error)
end
+ it 'allows forced and no_tags to be changed' do
+ expect_call(false, force: true, tags: false)
+
+ result = gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', forced: true, no_tags: true)
+ expect(result).to be_truthy
+ end
+
context 'SSH auth' do
it 'passes the SSH key if specified' do
- expect_call(false, 'GITLAB_SHELL_SSH_KEY' => 'foo')
+ expect_call(false, force: false, tags: true, ssh_key: 'foo')
ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo')
@@ -292,7 +278,7 @@ describe Gitlab::Shell do
end
it 'does not pass an empty SSH key' do
- expect_call(false)
+ expect_call(false, force: false, tags: true)
ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '')
@@ -300,7 +286,7 @@ describe Gitlab::Shell do
end
it 'does not pass the key unless SSH key auth is to be used' do
- expect_call(false)
+ expect_call(false, force: false, tags: true)
ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo')
@@ -308,7 +294,7 @@ describe Gitlab::Shell do
end
it 'passes the known_hosts data if specified' do
- expect_call(false, 'GITLAB_SHELL_KNOWN_HOSTS' => 'foo')
+ expect_call(false, force: false, tags: true, known_hosts: 'foo')
ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo')
@@ -316,7 +302,7 @@ describe Gitlab::Shell do
end
it 'does not pass empty known_hosts data' do
- expect_call(false)
+ expect_call(false, force: false, tags: true)
ssh_auth = build_ssh_auth(ssh_known_hosts: '')
@@ -324,7 +310,7 @@ describe Gitlab::Shell do
end
it 'does not pass known_hosts data unless SSH is to be used' do
- expect_call(false, popen_vars)
+ expect_call(false, force: false, tags: true)
ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo')
@@ -342,20 +328,23 @@ describe Gitlab::Shell do
end
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::Popen).to receive(:popen)
- .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"],
- nil, popen_vars).and_return([nil, 0])
+ expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true }
+
+ result = gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url)
- expect(gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git')).to be true
+ expect(result).to be_truthy
end
it 'raises an exception when the command fails' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"],
- nil, popen_vars).and_return(["error", 1])
+ allow(gitlab_projects).to receive(:output) { 'error' }
+ expect(gitlab_projects).to receive(:import_project) { false }
- expect { gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git') }.to raise_error(Gitlab::Shell::Error, "error")
+ expect do
+ gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url)
+ end.to raise_error(Gitlab::Shell::Error, "error")
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb
index 09f95be2213..0c66d764851 100644
--- a/spec/lib/gitlab/sidekiq_config_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config_spec.rb
@@ -16,9 +16,30 @@ describe Gitlab::SidekiqConfig do
expect(queues).to include('post_receive')
expect(queues).to include('merge')
- expect(queues).to include('cronjob')
+ expect(queues).to include('cronjob:stuck_import_jobs')
expect(queues).to include('mailers')
expect(queues).to include('default')
end
end
+
+ describe '.expand_queues' do
+ it 'expands queue namespaces to concrete queue names' do
+ queues = described_class.expand_queues(%w[cronjob])
+
+ expect(queues).to include('cronjob:stuck_import_jobs')
+ expect(queues).to include('cronjob:stuck_merge_jobs')
+ end
+
+ it 'lets concrete queue names pass through' do
+ queues = described_class.expand_queues(%w[post_receive])
+
+ expect(queues).to include('post_receive')
+ end
+
+ it 'lets unknown queues pass through' do
+ queues = described_class.expand_queues(%w[unknown])
+
+ expect(queues).to include('unknown')
+ end
+ end
end
diff --git a/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb
new file mode 100644
index 00000000000..7debf70a16f
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqVersioning::Manager do
+ before do
+ Sidekiq::Manager.prepend described_class
+ end
+
+ describe '#initialize' do
+ it 'listens on all expanded queues' do
+ manager = Sidekiq::Manager.new(queues: %w[post_receive repository_fork cronjob unknown])
+
+ queues = manager.options[:queues]
+
+ expect(queues).to include('post_receive')
+ expect(queues).to include('repository_fork')
+ expect(queues).to include('cronjob')
+ expect(queues).to include('cronjob:stuck_import_jobs')
+ expect(queues).to include('cronjob:stuck_merge_jobs')
+ expect(queues).to include('unknown')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb
new file mode 100644
index 00000000000..fa6d42e730d
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqVersioning, :sidekiq, :redis do
+ let(:foo_worker) do
+ Class.new do
+ def self.name
+ 'FooWorker'
+ end
+
+ include ApplicationWorker
+ end
+ end
+
+ let(:bar_worker) do
+ Class.new do
+ def self.name
+ 'BarWorker'
+ end
+
+ include ApplicationWorker
+ end
+ end
+
+ before do
+ allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker])
+ allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue])
+ end
+
+ describe '.install!' do
+ it 'prepends SidekiqVersioning::Manager into Sidekiq::Manager' do
+ described_class.install!
+
+ expect(Sidekiq::Manager).to include(Gitlab::SidekiqVersioning::Manager)
+ end
+
+ it 'registers all versionless and versioned queues with Redis' do
+ described_class.install!
+
+ queues = Sidekiq::Queue.all.map(&:name)
+ expect(queues).to include('foo')
+ expect(queues).to include('bar')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb
index 75ae58d0582..3b077c58c50 100644
--- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::SlashCommands::IssueNew do
let(:regex_match) { described_class.match("issue create bird is the word") }
before do
- project.team << [user, :master]
+ project.add_master(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 51f59216413..e41e5254dde 100644
--- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::SlashCommands::IssueSearch do
context 'the user has access' do
before do
- project.team << [user, :master]
+ project.add_master(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 08c380ca8f1..e5834d5a2ee 100644
--- a/spec/lib/gitlab/slash_commands/issue_show_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::SlashCommands::IssueShow do
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
subject do
diff --git a/spec/lib/gitlab/tcp_checker_spec.rb b/spec/lib/gitlab/tcp_checker_spec.rb
new file mode 100644
index 00000000000..4acf0334496
--- /dev/null
+++ b/spec/lib/gitlab/tcp_checker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::TcpChecker do
+ before do
+ @server = TCPServer.new('localhost', 0)
+ _, @port, _, @ip = @server.addr
+ end
+
+ after do
+ @server.close
+ end
+
+ subject(:checker) { described_class.new(@ip, @port) }
+
+ describe '#check' do
+ subject { checker.check }
+
+ it 'can connect to an open port' do
+ is_expected.to be_truthy
+
+ expect(checker.error).to be_nil
+ end
+
+ it 'fails to connect to a closed port' do
+ @server.close
+
+ is_expected.to be_falsy
+
+ expect(checker.error).to be_a(Errno::ECONNREFUSED)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index cd97416bcc9..7280acb6c82 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -8,19 +8,19 @@ 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.team << [user, :master]
+ project.add_master(user)
expect(access.can_push_to_branch?('random_branch')).to be_truthy
end
it 'returns true if user is a developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
expect(access.can_push_to_branch?('random_branch')).to be_truthy
end
it 'returns false if user is a reporter' do
- project.team << [user, :reporter]
+ project.add_reporter(user)
expect(access.can_push_to_branch?('random_branch')).to be_falsey
end
@@ -31,34 +31,34 @@ describe Gitlab::UserAccess do
let(:project_access) { described_class.new(user, project: empty_project) }
it 'returns true if user is master' do
- empty_project.team << [user, :master]
+ empty_project.add_master(user)
expect(project_access.can_push_to_branch?('master')).to be_truthy
end
it 'returns false if user is developer and project is fully protected' do
- empty_project.team << [user, :developer]
+ empty_project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
expect(project_access.can_push_to_branch?('master')).to be_falsey
end
it 'returns false if user is developer and it is not allowed to push new commits but can merge into branch' do
- empty_project.team << [user, :developer]
+ empty_project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(project_access.can_push_to_branch?('master')).to be_falsey
end
it 'returns true if user is developer and project is unprotected' do
- empty_project.team << [user, :developer]
+ empty_project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
expect(project_access.can_push_to_branch?('master')).to be_truthy
end
it 'returns true if user is developer and project grants developers permission' do
- empty_project.team << [user, :developer]
+ empty_project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
expect(project_access.can_push_to_branch?('master')).to be_truthy
@@ -70,25 +70,25 @@ describe Gitlab::UserAccess do
let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
it 'returns true if user is a master' do
- project.team << [user, :master]
+ project.add_master(user)
expect(access.can_push_to_branch?(branch.name)).to be_truthy
end
it 'returns false if user is a developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
it 'returns false if user is a reporter' do
- project.team << [user, :reporter]
+ project.add_reporter(user)
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
it 'returns false if branch does not exist' do
- project.team << [user, :developer]
+ project.add_developer(user)
expect(access.can_push_to_branch?(not_existing_branch.name)).to be_falsey
end
@@ -100,19 +100,19 @@ describe Gitlab::UserAccess do
end
it 'returns true if user is a master' do
- project.team << [user, :master]
+ project.add_master(user)
expect(access.can_push_to_branch?(@branch.name)).to be_truthy
end
it 'returns true if user is a developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
expect(access.can_push_to_branch?(@branch.name)).to be_truthy
end
it 'returns false if user is a reporter' do
- project.team << [user, :reporter]
+ project.add_reporter(user)
expect(access.can_push_to_branch?(@branch.name)).to be_falsey
end
@@ -124,19 +124,19 @@ describe Gitlab::UserAccess do
end
it 'returns true if user is a master' do
- project.team << [user, :master]
+ project.add_master(user)
expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
end
it 'returns true if user is a developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
end
it 'returns false if user is a reporter' do
- project.team << [user, :reporter]
+ project.add_reporter(user)
expect(access.can_merge_to_branch?(@branch.name)).to be_falsey
end
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
index 4a104ab6d97..473f8100771 100644
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -49,4 +49,16 @@ describe Gitlab::Utils::StrongMemoize do
end
end
end
+
+ describe '#clear_memoization' do
+ let(:value) { 'mepmep' }
+
+ it 'removes the instance variable' do
+ object.method_name
+
+ object.clear_memoization(:method_name)
+
+ expect(object.instance_variable_defined?(:@method_name)).to be(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb
index 70d2e22b48f..6120bafb2e3 100644
--- a/spec/lib/gitlab/view/presenter/factory_spec.rb
+++ b/spec/lib/gitlab/view/presenter/factory_spec.rb
@@ -27,5 +27,13 @@ describe Gitlab::View::Presenter::Factory do
expect(presenter).to be_a(Ci::BuildPresenter)
end
+
+ it 'uses the presenter_class if given on #initialize' do
+ MyCustomPresenter = Class.new(described_class)
+
+ presenter = described_class.new(build, presenter_class: MyCustomPresenter).fabricate!
+
+ expect(presenter).to be_a(MyCustomPresenter)
+ end
end
end
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
index 48a67773de9..d85dac630b4 100644
--- a/spec/lib/gitlab/visibility_level_spec.rb
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -49,4 +49,31 @@ describe Gitlab::VisibilityLevel do
.to eq([Gitlab::VisibilityLevel::PUBLIC])
end
end
+
+ describe '.allowed_levels' do
+ it 'only includes the levels that arent restricted' do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+
+ expect(described_class.allowed_levels)
+ .to contain_exactly(described_class::PRIVATE, described_class::PUBLIC)
+ end
+ end
+
+ describe '.closest_allowed_level' do
+ it 'picks INTERNAL instead of PUBLIC if public is restricted' do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+
+ expect(described_class.closest_allowed_level(described_class::PUBLIC))
+ .to eq(described_class::INTERNAL)
+ end
+
+ it 'picks PRIVATE if nothing is available' do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PRIVATE])
+
+ expect(described_class.closest_allowed_level(described_class::PUBLIC))
+ .to eq(described_class::PRIVATE)
+ end
+ end
end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index ecb4034ec8b..f65e41dfea3 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -50,6 +50,30 @@ 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/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index e1d71a9573b..cbc8c67da61 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -342,6 +342,46 @@ describe Notify do
end
end
+ context 'for issue notes' do
+ let(:host) { Gitlab.config.gitlab.host }
+
+ context 'in discussion' do
+ set(:first_note) { create(:discussion_note_on_issue) }
+ set(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note) }
+ set(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note) }
+
+ subject { described_class.note_issue_email(recipient.id, third_note.id) }
+
+ it 'has In-Reply-To header pointing to previous note in discussion' do
+ expect(subject.header['In-Reply-To'].message_ids).to eq(["note_#{second_note.id}@#{host}"])
+ end
+
+ it 'has References header including the notes and issue of the discussion' do
+ expect(subject.header['References'].message_ids).to include("issue_#{first_note.noteable.id}@#{host}",
+ "note_#{first_note.id}@#{host}",
+ "note_#{second_note.id}@#{host}")
+ end
+
+ it 'has X-GitLab-Discussion-ID header' do
+ expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id)
+ end
+ end
+
+ context 'individual issue comments' do
+ set(:note) { create(:note_on_issue) }
+
+ subject { described_class.note_issue_email(recipient.id, note.id) }
+
+ it 'has In-Reply-To header pointing to the issue' do
+ expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"])
+ end
+
+ it 'has References header including the notes and issue of the discussion' do
+ expect(subject.header['References'].message_ids).to include("issue_#{note.noteable.id}@#{host}")
+ end
+ end
+ end
+
context 'for snippet notes' do
let(:project_snippet) { create(:project_snippet, project: project) }
let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
@@ -377,7 +417,7 @@ describe Notify do
context 'for a project in a user namespace' do
let(:project) do
create(:project, :public, :access_requestable) do |project|
- project.team << [project.owner, :master, project.owner]
+ project.add_master(project.owner, current_user: project.owner)
end
end
@@ -480,7 +520,7 @@ describe Notify do
end
describe 'project invitation' do
- let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:master) { create(:user).tap { |u| project.add_master(u) } }
let(:project_member) { invite_to_project(project, inviter: master) }
subject { described_class.member_invited_email('project', project_member.id, project_member.invite_token) }
@@ -500,7 +540,7 @@ describe Notify do
describe 'project invitation accepted' do
let(:invited_user) { create(:user, name: 'invited user') }
- let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:master) { create(:user).tap { |u| project.add_master(u) } }
let(:project_member) do
invitee = invite_to_project(project, inviter: master)
invitee.accept_invite!(invited_user)
@@ -523,7 +563,7 @@ describe Notify do
end
describe 'project invitation declined' do
- let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:master) { create(:user).tap { |u| project.add_master(u) } }
let(:project_member) do
invitee = invite_to_project(project, inviter: master)
invitee.decline_invite!
diff --git a/spec/migrations/clean_up_for_members_spec.rb b/spec/migrations/clean_up_for_members_spec.rb
new file mode 100644
index 00000000000..0258860d169
--- /dev/null
+++ b/spec/migrations/clean_up_for_members_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb')
+
+describe CleanUpForMembers, :migration do
+ let(:migration) { described_class.new }
+ let!(:group_member) { create_group_member }
+ let!(:unbinded_group_member) { create_group_member }
+ let!(:invited_group_member) { create_group_member(true) }
+ let!(:not_valid_group_member) { create_group_member }
+ let!(:project_member) { create_project_member }
+ let!(:invited_project_member) { create_project_member(true) }
+ let!(:unbinded_project_member) { create_project_member }
+ let!(:not_valid_project_member) { create_project_member }
+
+ it 'removes members without proper user_id' do
+ unbinded_group_member.update_column(:user_id, nil)
+ not_valid_group_member.update_column(:user_id, 9999)
+ unbinded_project_member.update_column(:user_id, nil)
+ not_valid_project_member.update_column(:user_id, 9999)
+
+ migrate!
+
+ expect(Member.all).not_to include(unbinded_group_member, not_valid_group_member, unbinded_project_member, not_valid_project_member)
+ expect(Member.all).to include(group_member, invited_group_member, project_member, invited_project_member)
+ end
+
+ def create_group_member(invited = false)
+ fill_member(GroupMember.new(group: create_group), invited)
+ end
+
+ def create_project_member(invited = false)
+ fill_member(ProjectMember.new(project: create_project), invited)
+ end
+
+ def fill_member(member_object, invited)
+ member_object.tap do |m|
+ m.access_level = 40
+ m.notification_level = 3
+
+ if invited
+ m.user_id = nil
+ m.invite_token = 'xxx'
+ m.invite_email = 'email@email.com'
+ else
+ m.user_id = create_user.id
+ end
+
+ m.save
+ end
+
+ member_object
+ end
+
+ def create_group
+ name = FFaker::Lorem.characters(10)
+
+ Group.create(name: name, path: name.downcase.gsub(/\s/, '_'))
+ end
+
+ def create_project
+ name = FFaker::Lorem.characters(10)
+ creator = create_user
+
+ Project.create(name: name,
+ path: name.downcase.gsub(/\s/, '_'),
+ namespace: creator.namespace,
+ creator: creator)
+ end
+
+ def create_user
+ User.create(email: FFaker::Internet.email,
+ password: '12345678',
+ name: FFaker::Name.name,
+ username: FFaker::Internet.user_name,
+ confirmed_at: Time.now,
+ confirmation_token: nil)
+ end
+end
diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
index 1df2477da51..8a191bd7139 100644
--- a/spec/migrations/delete_conflicting_redirect_routes_spec.rb
+++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
@@ -10,9 +10,6 @@ describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do
end
before do
- stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2)
- stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2)
-
routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1')
routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2')
routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3')
@@ -32,27 +29,14 @@ describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do
redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
end
- it 'correctly schedules background migrations' do
+ # No-op. See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252
+ it 'NO-OP: does not schedule any background migrations' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f)
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f)
- expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]])
- expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f)
- expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ expect(BackgroundMigrationWorker.jobs.size).to eq 0
end
end
end
-
- it 'schedules background migrations' do
- Sidekiq::Testing.inline! do
- expect do
- migrate!
- end.to change { redirect_routes.count }.from(8).to(3)
- end
- end
end
diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
new file mode 100644
index 00000000000..d2eef81f396
--- /dev/null
+++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20171106151218_issues_moved_to_id_foreign_key.rb')
+
+# The schema version has to be far enough in advance to have the
+# only_mirror_protected_branches column in the projects table to create a
+# project via FactoryBot.
+describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do
+ let!(:issue_first) { create(:issue, moved_to_id: issue_second.id) }
+ let!(:issue_second) { create(:issue, moved_to_id: issue_third.id) }
+ let!(:issue_third) { create(:issue) }
+
+ subject { described_class.new }
+
+ it 'removes the orphaned moved_to_id' do
+ subject.down
+
+ issue_third.update_attributes(moved_to_id: 100000)
+
+ subject.up
+
+ expect(issue_first.reload.moved_to_id).to eq(issue_second.id)
+ expect(issue_second.reload.moved_to_id).to eq(issue_third.id)
+ expect(issue_third.reload.moved_to_id).to be_nil
+ end
+end
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
index 05f281fffff..c81ec887ded 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
@@ -2,9 +2,10 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb')
describe MigrateGcpClustersToNewClustersArchitectures, :migration do
- let(:project) { create(:project) }
+ let(:projects) { table(:projects) }
+ let(:project) { projects.create }
let(:user) { create(:user) }
- let(:service) { create(:kubernetes_service, project: project) }
+ let(:service) { create(:kubernetes_service, project_id: project.id) }
context 'when cluster is being created' do
let(:project_id) { project.id }
@@ -32,7 +33,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
- let(:cluster) { Clusters::Cluster.last }
+ let(:cluster) { described_class::Cluster.last }
let(:cluster_id) { cluster.id }
before do
@@ -45,21 +46,20 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
it 'correctly migrate to new clusters architectures' do
migrate!
- expect(Clusters::Cluster.count).to eq(1)
- expect(Clusters::Project.count).to eq(1)
- expect(Clusters::Providers::Gcp.count).to eq(1)
- expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+ expect(described_class::Cluster.count).to eq(1)
+ expect(described_class::ClustersProject.count).to eq(1)
+ expect(described_class::ProvidersGcp.count).to eq(1)
+ expect(described_class::PlatformsKubernetes.count).to eq(1)
- expect(cluster.user).to eq(user)
+ expect(cluster.user_id).to eq(user.id)
expect(cluster.enabled).to be_truthy
expect(cluster.name).to eq(gcp_cluster_name.delete!("'"))
expect(cluster.provider_type).to eq('gcp')
expect(cluster.platform_type).to eq('kubernetes')
- expect(cluster.project).to eq(project)
- expect(project.clusters).to include(cluster)
+ expect(cluster.project_ids).to include(project.id)
- expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.cluster_id).to eq(cluster.id)
expect(cluster.provider_gcp.status).to eq(status)
expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
@@ -71,7 +71,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
- expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.cluster_id).to eq(cluster.id)
expect(cluster.platform_kubernetes.api_url).to be_nil
expect(cluster.platform_kubernetes.ca_cert).to be_nil
expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
@@ -109,7 +109,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
- let(:cluster) { Clusters::Cluster.last }
+ let(:cluster) { described_class::Cluster.last }
let(:cluster_id) { cluster.id }
before do
@@ -122,21 +122,20 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
it 'correctly migrate to new clusters architectures' do
migrate!
- expect(Clusters::Cluster.count).to eq(1)
- expect(Clusters::Project.count).to eq(1)
- expect(Clusters::Providers::Gcp.count).to eq(1)
- expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+ expect(described_class::Cluster.count).to eq(1)
+ expect(described_class::ClustersProject.count).to eq(1)
+ expect(described_class::ProvidersGcp.count).to eq(1)
+ expect(described_class::PlatformsKubernetes.count).to eq(1)
- expect(cluster.user).to eq(user)
+ expect(cluster.user_id).to eq(user.id)
expect(cluster.enabled).to be_truthy
expect(cluster.name).to eq(tr(gcp_cluster_name))
expect(cluster.provider_type).to eq('gcp')
expect(cluster.platform_type).to eq('kubernetes')
- expect(cluster.project).to eq(project)
- expect(project.clusters).to include(cluster)
+ expect(cluster.project_ids).to include(project.id)
- expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.cluster_id).to eq(cluster.id)
expect(cluster.provider_gcp.status).to eq(status)
expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
@@ -148,7 +147,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
- expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.cluster_id).to eq(cluster.id)
expect(cluster.platform_kubernetes.api_url).to eq('https://' + tr(endpoint))
expect(cluster.platform_kubernetes.ca_cert).to eq(tr(ca_cert))
expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
diff --git a/spec/migrations/migrate_kubernetes_service_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_kubernetes_service_to_new_clusters_architectures_spec.rb
new file mode 100644
index 00000000000..df0015b6dd3
--- /dev/null
+++ b/spec/migrations/migrate_kubernetes_service_to_new_clusters_architectures_spec.rb
@@ -0,0 +1,312 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb')
+
+describe MigrateKubernetesServiceToNewClustersArchitectures, :migration do
+ context 'when unique KubernetesService exists' do
+ shared_examples 'KubernetesService migration' do
+ let(:sample_num) { 2 }
+
+ let(:projects) do
+ (1..sample_num).each_with_object([]) do |n, array|
+ array << MigrateKubernetesServiceToNewClustersArchitectures::Project.create!
+ end
+ end
+
+ let!(:kubernetes_services) do
+ projects.map do |project|
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ active: active,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"namespace\":\"prod\",\"api_url\":\"https://kubernetes#{project.id}.com\",\"ca_pem\":\"ca_pem#{project.id}\",\"token\":\"token#{project.id}\"}")
+ end
+ end
+
+ it 'migrates the KubernetesService to Platform::Kubernetes' do
+ expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(sample_num)
+
+ projects.each do |project|
+ project.clusters.last.tap do |cluster|
+ expect(cluster.enabled).to eq(active)
+ expect(cluster.platform_kubernetes.api_url).to eq(project.kubernetes_service.api_url)
+ expect(cluster.platform_kubernetes.ca_cert).to eq(project.kubernetes_service.ca_pem)
+ expect(cluster.platform_kubernetes.token).to eq(project.kubernetes_service.token)
+ expect(project.kubernetes_service).not_to be_active
+ end
+ end
+ end
+ end
+
+ context 'when KubernetesService is active' do
+ let(:active) { true }
+
+ it_behaves_like 'KubernetesService migration'
+ end
+ end
+
+ context 'when unique KubernetesService spawned from Service Template' do
+ let(:sample_num) { 2 }
+
+ let(:projects) do
+ (1..sample_num).each_with_object([]) do |n, array|
+ array << MigrateKubernetesServiceToNewClustersArchitectures::Project.create!
+ end
+ end
+
+ let!(:kubernetes_service_template) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ template: true,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}")
+ end
+
+ let!(:kubernetes_services) do
+ projects.map do |project|
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"namespace\":\"prod\",\"api_url\":\"#{kubernetes_service_template.api_url}\",\"ca_pem\":\"#{kubernetes_service_template.ca_pem}\",\"token\":\"#{kubernetes_service_template.token}\"}")
+ end
+ end
+
+ it 'migrates the KubernetesService to Platform::Kubernetes without template' do
+ expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(sample_num)
+
+ projects.each do |project|
+ project.clusters.last.tap do |cluster|
+ expect(cluster.platform_kubernetes.api_url).to eq(project.kubernetes_service.api_url)
+ expect(cluster.platform_kubernetes.ca_cert).to eq(project.kubernetes_service.ca_pem)
+ expect(cluster.platform_kubernetes.token).to eq(project.kubernetes_service.token)
+ expect(project.kubernetes_service).not_to be_active
+ end
+ end
+ end
+ end
+
+ context 'when managed KubernetesService exists' do
+ let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
+
+ let(:cluster) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
+ projects: [project],
+ name: 'sample-cluster',
+ platform_type: :kubernetes,
+ provider_type: :user,
+ platform_kubernetes_attributes: {
+ api_url: 'https://sample.kubernetes.com',
+ ca_cert: 'ca_pem-sample',
+ token: 'token-sample'
+ } )
+ end
+
+ let!(:kubernetes_service) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ active: cluster.enabled,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"api_url\":\"#{cluster.platform_kubernetes.api_url}\"}")
+ end
+
+ it 'does not migrate the KubernetesService and disables the kubernetes_service' do # Because the corresponding Platform::Kubernetes already exists
+ expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }
+
+ kubernetes_service.reload
+ expect(kubernetes_service).not_to be_active
+ end
+ end
+
+ context 'when production cluster has already been existed' do # i.e. There are no environment_scope conflicts
+ let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
+
+ let(:cluster) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
+ projects: [project],
+ name: 'sample-cluster',
+ platform_type: :kubernetes,
+ provider_type: :user,
+ environment_scope: 'production/*',
+ platform_kubernetes_attributes: {
+ api_url: 'https://sample.kubernetes.com',
+ ca_cert: 'ca_pem-sample',
+ token: 'token-sample'
+ } )
+ end
+
+ let!(:kubernetes_service) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ active: true,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"api_url\":\"https://debug.kube.com\"}")
+ end
+
+ it 'migrates the KubernetesService to Platform::Kubernetes' do
+ expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
+
+ kubernetes_service.reload
+ project.clusters.last.tap do |cluster|
+ expect(cluster.environment_scope).to eq('*')
+ expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url)
+ expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem)
+ expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token)
+ expect(kubernetes_service).not_to be_active
+ end
+ end
+ end
+
+ context 'when default cluster has already been existed' do
+ let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
+
+ let!(:cluster) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
+ projects: [project],
+ name: 'sample-cluster',
+ platform_type: :kubernetes,
+ provider_type: :user,
+ environment_scope: '*',
+ platform_kubernetes_attributes: {
+ api_url: 'https://sample.kubernetes.com',
+ ca_cert: 'ca_pem-sample',
+ token: 'token-sample'
+ } )
+ end
+
+ let!(:kubernetes_service) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ active: true,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"api_url\":\"https://debug.kube.com\"}")
+ end
+
+ it 'migrates the KubernetesService to Platform::Kubernetes with dedicated environment_scope' do # Because environment_scope is duplicated
+ expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
+
+ kubernetes_service.reload
+ project.clusters.last.tap do |cluster|
+ expect(cluster.environment_scope).to eq('migrated/*')
+ expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url)
+ expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem)
+ expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token)
+ expect(kubernetes_service).not_to be_active
+ end
+ end
+ end
+
+ context 'when default cluster and migrated cluster has already been existed' do
+ let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
+
+ let!(:cluster) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
+ projects: [project],
+ name: 'sample-cluster',
+ platform_type: :kubernetes,
+ provider_type: :user,
+ environment_scope: '*',
+ platform_kubernetes_attributes: {
+ api_url: 'https://sample.kubernetes.com',
+ ca_cert: 'ca_pem-sample',
+ token: 'token-sample'
+ } )
+ end
+
+ let!(:migrated_cluster) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
+ projects: [project],
+ name: 'sample-cluster',
+ platform_type: :kubernetes,
+ provider_type: :user,
+ environment_scope: 'migrated/*',
+ platform_kubernetes_attributes: {
+ api_url: 'https://sample.kubernetes.com',
+ ca_cert: 'ca_pem-sample',
+ token: 'token-sample'
+ } )
+ end
+
+ let!(:kubernetes_service) do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ active: true,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"api_url\":\"https://debug.kube.com\"}")
+ end
+
+ it 'migrates the KubernetesService to Platform::Kubernetes with dedicated environment_scope' do # Because environment_scope is duplicated
+ expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
+
+ kubernetes_service.reload
+ project.clusters.last.tap do |cluster|
+ expect(cluster.environment_scope).to eq('migrated0/*')
+ expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url)
+ expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem)
+ expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token)
+ expect(kubernetes_service).not_to be_active
+ end
+ end
+ end
+
+ context 'when KubernetesService has nullified parameters' do
+ let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
+
+ before do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ active: false,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{}")
+ end
+
+ it 'does not migrate the KubernetesService and disables the kubernetes_service' do
+ expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }
+
+ expect(project.kubernetes_service).not_to be_active
+ end
+ end
+
+ # Platforms::Kubernetes validates `token` reagdless of the activeness,
+ # whereas KubernetesService validates `token` if only it's activated
+ # However, in this migration file, there are no validations because of the re-defined model class
+ # therefore, we should safely add this raw to Platform::Kubernetes
+ context 'when KubernetesService has empty token' do
+ let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
+
+ before do
+ MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
+ project: project,
+ active: false,
+ category: 'deployment',
+ type: 'KubernetesService',
+ properties: "{\"namespace\":\"prod\",\"api_url\":\"http://111.111.111.111\",\"ca_pem\":\"a\",\"token\":\"\"}")
+ end
+
+ it 'does not migrate the KubernetesService and disables the kubernetes_service' do
+ expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
+
+ project.clusters.last.tap do |cluster|
+ expect(cluster.environment_scope).to eq('*')
+ expect(cluster.platform_kubernetes.namespace).to eq('prod')
+ expect(cluster.platform_kubernetes.api_url).to eq('http://111.111.111.111')
+ expect(cluster.platform_kubernetes.ca_cert).to eq('a')
+ expect(cluster.platform_kubernetes.token).to be_empty
+ expect(project.kubernetes_service).not_to be_active
+ end
+ end
+ end
+
+ context 'when KubernetesService does not exist' do
+ let!(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
+
+ it 'does not migrate the KubernetesService' do
+ expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }
+ end
+ end
+end
diff --git a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb
index 9b92f4b70b0..a837498e1b1 100644
--- a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb
+++ b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb
@@ -35,9 +35,9 @@ describe MigrateStageIdReferenceInBackground, :migration, :sidekiq do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 1, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 3, 3)
- expect(described_class::MIGRATION).to be_scheduled_migration(4.minutes, 4, 5)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 3, 3)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 4, 5)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb
index 094c9bc604e..79d2708f9ad 100644
--- a/spec/migrations/migrate_stages_statuses_spec.rb
+++ b/spec/migrations/migrate_stages_statuses_spec.rb
@@ -50,9 +50,9 @@ describe MigrateStagesStatuses, :migration do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1)
- expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 2, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 3, 3)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/normalize_ldap_extern_uids_spec.rb b/spec/migrations/normalize_ldap_extern_uids_spec.rb
index 262d7742aaf..56a78f52802 100644
--- a/spec/migrations/normalize_ldap_extern_uids_spec.rb
+++ b/spec/migrations/normalize_ldap_extern_uids_spec.rb
@@ -27,11 +27,11 @@ describe NormalizeLdapExternUids, :migration, :sidekiq do
migrate!
expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(5.minutes.from_now.to_f)
expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(10.minutes.from_now.to_f)
expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]])
- expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(30.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(15.minutes.from_now.to_f)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/remove_assignee_id_from_issue_spec.rb b/spec/migrations/remove_assignee_id_from_issue_spec.rb
new file mode 100644
index 00000000000..2c6f992d3ae
--- /dev/null
+++ b/spec/migrations/remove_assignee_id_from_issue_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170523073948_remove_assignee_id_from_issue.rb')
+
+describe RemoveAssigneeIdFromIssue, :migration do
+ let(:issues) { table(:issues) }
+ let(:issue_assignees) { table(:issue_assignees) }
+ let(:users) { table(:users) }
+
+ let!(:user_1) { users.create(email: 'email1@example.com') }
+ let!(:user_2) { users.create(email: 'email2@example.com') }
+ let!(:user_3) { users.create(email: 'email3@example.com') }
+
+ def create_issue(assignees:)
+ issues.create.tap do |issue|
+ assignees.each do |assignee|
+ issue_assignees.create(issue_id: issue.id, user_id: assignee.id)
+ end
+ end
+ end
+
+ let!(:issue_single_assignee) { create_issue(assignees: [user_1]) }
+ let!(:issue_no_assignee) { create_issue(assignees: []) }
+ let!(:issue_multiple_assignees) { create_issue(assignees: [user_2, user_3]) }
+
+ describe '#down' do
+ it 'sets the assignee_id to a random matching assignee from the assignees table' do
+ migrate!
+ disable_migrations_output { described_class.new.down }
+
+ expect(issue_single_assignee.reload.assignee_id).to eq(user_1.id)
+ expect(issue_no_assignee.reload.assignee_id).to be_nil
+ expect(issue_multiple_assignees.reload.assignee_id).to eq(user_2.id).or(user_3.id)
+
+ disable_migrations_output { described_class.new.up }
+ end
+ end
+end
diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
index 0e884a7d910..65ec07da31c 100644
--- a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
+++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
@@ -2,18 +2,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys')
describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do
- matcher :be_scheduled_migration do |*expected|
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration, expected]
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
before do
create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key)
create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key)
diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
index 76afb6c19cf..d230f064444 100644
--- a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
+++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
@@ -24,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1)
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 2, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, 4, 4)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 4, 4)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
index cf323973384..1aab4ae1650 100644
--- a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
+++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
@@ -24,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 1, 1)
- expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes, 2, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes, 4, 4)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(20.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(30.minutes, 4, 4)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb
index 158d0bc02ed..c9fdbe95d13 100644
--- a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb
+++ b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb
@@ -44,9 +44,9 @@ describe ScheduleMergeRequestLatestMergeRequestDiffIdMigrations, :migration, :si
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, merge_request_1.id, merge_request_1.id)
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, merge_request_2.id, merge_request_2.id)
- expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, merge_request_4.id, merge_request_4.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, merge_request_1.id, merge_request_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, merge_request_2.id, merge_request_2.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, merge_request_4.id, merge_request_4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb
new file mode 100644
index 00000000000..2e6b2cff0ab
--- /dev/null
+++ b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb')
+
+describe SchedulePopulateMergeRequestMetricsWithEventsData, :migration, :sidekiq do
+ let!(:mrs) { create_list(:merge_request, 3) }
+
+ 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/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb
index 7fe7a140e2f..fe4d5b8a279 100644
--- a/spec/migrations/track_untracked_uploads_spec.rb
+++ b/spec/migrations/track_untracked_uploads_spec.rb
@@ -4,18 +4,6 @@ require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_up
describe TrackUntrackedUploads, :migration, :sidekiq do
include TrackUntrackedUploadsHelpers
- matcher :be_scheduled_migration do
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration]
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
it 'correctly schedules the follow-up background migration' do
Sidekiq::Testing.fake! do
migrate!
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 71aa51e1857..38fb98d4f50 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -34,19 +34,19 @@ describe Ability do
end
it 'returns false for a guest user' do
- project.team << [user, :guest]
+ project.add_guest(user)
expect(described_class.can_edit_note?(user, note)).to be_falsy
end
it 'returns false for a developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
expect(described_class.can_edit_note?(user, note)).to be_falsy
end
it 'returns true for a master' do
- project.team << [user, :master]
+ project.add_master(user)
expect(described_class.can_edit_note?(user, note)).to be_truthy
end
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
index 0f8330e91c1..5ed2f4400bc 100644
--- a/spec/models/blob_viewer/package_json_spec.rb
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -22,4 +22,51 @@ describe BlobViewer::PackageJson do
expect(subject.package_name).to eq('module-name')
end
end
+
+ describe '#package_url' do
+ it 'returns the package URL' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq("https://www.npmjs.com/package/#{subject.package_name}")
+ end
+ end
+
+ describe '#package_type' do
+ it 'returns "package"' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_type).to eq('package')
+ end
+ end
+
+ context 'when package.json has "private": true' do
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "module-name",
+ "version": "10.3.1",
+ "private": true,
+ "homepage": "myawesomepackage.com"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'package.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_url' do
+ it 'returns homepage if any' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq('myawesomepackage.com')
+ end
+ end
+
+ describe '#package_type' do
+ it 'returns "private package"' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_type).to eq('private package')
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 83352421d7d..3af27504967 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -132,11 +132,9 @@ describe Ci::Build do
end
describe '#artifacts?' do
- context 'when new artifacts are used' do
- let(:build) { create(:ci_build, :artifacts) }
-
- subject { build.artifacts? }
+ subject { build.artifacts? }
+ context 'when new artifacts are used' do
context 'artifacts archive does not exist' do
let(:build) { create(:ci_build) }
@@ -144,25 +142,19 @@ describe Ci::Build do
end
context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+
it { is_expected.to be_truthy }
context 'is expired' do
- let!(:build) { create(:ci_build, :artifacts, :expired) }
+ let(:build) { create(:ci_build, :artifacts, :expired) }
it { is_expected.to be_falsy }
end
-
- context 'is not expired' do
- it { is_expected.to be_truthy }
- end
end
end
context 'when legacy artifacts are used' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
-
- subject { build.artifacts? }
-
context 'artifacts archive does not exist' do
let(:build) { create(:ci_build) }
@@ -184,17 +176,15 @@ describe Ci::Build do
end
context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :legacy_artifacts) }
+
it { is_expected.to be_truthy }
context 'is expired' do
- let!(:build) { create(:ci_build, :legacy_artifacts, :expired) }
+ let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
it { is_expected.to be_falsy }
end
-
- context 'is not expired' do
- it { is_expected.to be_truthy }
- end
end
end
end
@@ -323,6 +313,42 @@ describe Ci::Build do
end
end
+ describe '#cache' do
+ let(:options) { { cache: { key: "key", paths: ["public"], policy: "pull-push" } } }
+
+ subject { build.cache }
+
+ context 'when build has cache' do
+ before do
+ allow(build).to receive(:options).and_return(options)
+ end
+
+ context 'when project has jobs_cache_index' do
+ before do
+ allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1)
+ end
+
+ it { is_expected.to be_an(Array).and all(include(key: "key:1")) }
+ end
+
+ context 'when project does not have jobs_cache_index' do
+ before do
+ allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(nil)
+ end
+
+ it { is_expected.to eq([options[:cache]]) }
+ end
+ end
+
+ context 'when build does not have cache' do
+ before do
+ allow(build).to receive(:options).and_return({})
+ end
+
+ it { is_expected.to eq([nil]) }
+ end
+ end
+
describe '#depends_on_builds' do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
@@ -1929,9 +1955,9 @@ describe Ci::Build do
describe 'state transition: any => [:running]' do
shared_examples 'validation is active' do
context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
- it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) }
+ it { expect { job.run! }.not_to raise_error(Ci::Build::MissingDependenciesError) }
end
context 'when artifacts of depended job has been expired' do
@@ -1953,11 +1979,10 @@ describe Ci::Build do
shared_examples 'validation is not active' do
context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
it { expect { job.run! }.not_to raise_error }
end
-
context 'when artifacts of depended job has been expired' do
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index bb89e093890..7bef798a782 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -116,7 +116,7 @@ describe Ci::Pipeline, :mailer do
end
it "calculates average when there is one build without coverage" do
- FactoryGirl.create(:ci_build, pipeline: pipeline)
+ FactoryBot.create(:ci_build, pipeline: pipeline)
expect(pipeline.coverage).to be_nil
end
end
@@ -435,7 +435,7 @@ describe Ci::Pipeline, :mailer do
describe 'merge request metrics' do
let(:project) { create(:project, :repository) }
- let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+ let(:pipeline) { FactoryBot.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
before do
@@ -1440,7 +1440,7 @@ describe Ci::Pipeline, :mailer do
end
before do
- project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
+ project.add_developer(pipeline.user)
pipeline.user.global_notification_setting
.update(level: 'custom', failed_pipeline: true, success_pipeline: true)
@@ -1530,4 +1530,16 @@ describe Ci::Pipeline, :mailer do
expect(query_count).to eq(1)
end
end
+
+ describe '#total_size' do
+ let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
+ let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
+ let!(:test_job_failed_and_retried) { create(:ci_build, :failed, :retried, pipeline: pipeline, stage_idx: 1) }
+ let!(:second_test_job) { create(:ci_build, pipeline: pipeline, stage_idx: 1) }
+ let!(:deploy_job) { create(:ci_build, pipeline: pipeline, stage_idx: 2) }
+
+ it 'returns all jobs (including failed and retried)' do
+ expect(pipeline.total_size).to eq(5)
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index a93e7e233a8..b2b64e6ff48 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -51,24 +51,24 @@ describe Ci::Runner do
describe '#display_name' do
it 'returns the description if it has a value' do
- runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
+ runner = FactoryBot.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 = FactoryGirl.create(:ci_runner)
+ runner = FactoryBot.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 = FactoryGirl.build(:ci_runner, description: '', token: 'token')
+ runner = FactoryBot.build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token
end
end
describe '#assign_to' do
- let!(:project) { FactoryGirl.create :project }
- let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) }
+ let!(:project) { FactoryBot.create :project }
+ let!(:shared_runner) { FactoryBot.create(:ci_runner, :shared) }
before do
shared_runner.assign_to(project)
@@ -83,15 +83,15 @@ describe Ci::Runner do
subject { described_class.online }
before do
- @runner1 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.year.ago)
- @runner2 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago)
+ @runner1 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.year.ago)
+ @runner2 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago)
end
it { is_expected.to eq([@runner2])}
end
describe '#online?' do
- let(:runner) { FactoryGirl.create(:ci_runner, :shared) }
+ let(:runner) { FactoryBot.create(:ci_runner, :shared) }
subject { runner.online? }
@@ -268,7 +268,7 @@ describe Ci::Runner do
end
describe '#status' do
- let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
+ let(:runner) { FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
subject { runner.status }
@@ -442,9 +442,9 @@ describe Ci::Runner do
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
- runner = FactoryGirl.create(:ci_runner)
- project = FactoryGirl.create(:project)
- project1 = FactoryGirl.create(:project)
+ runner = FactoryBot.create(:ci_runner)
+ project = FactoryBot.create(:project)
+ project1 = FactoryBot.create(:project)
project.runners << runner
project1.runners << runner
@@ -452,8 +452,8 @@ describe Ci::Runner do
end
it "returns true" do
- runner = FactoryGirl.create(:ci_runner)
- project = FactoryGirl.create(:project)
+ runner = FactoryBot.create(:ci_runner)
+ project = FactoryBot.create(:project)
project.runners << runner
expect(runner.belongs_to_one_project?).to be_truthy
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index bd9c837402f..d4b72205203 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -69,7 +69,7 @@ describe Ci::Trigger do
context 'and is member of the project' do
before do
- project.team << [owner, :developer]
+ project.add_developer(owner)
end
it { is_expected.to eq(true) }
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index f8855079842..eb57abaf6ef 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -40,13 +40,13 @@ describe Clusters::Applications::Helm do
describe '#install_command' do
it 'has all the needed information' do
- expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true, chart: nil)
+ expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true)
end
end
describe 'status state machine' do
describe '#make_installing' do
- subject { create(:cluster_applications_helm, :scheduled) }
+ subject { create(:clusters_applications_helm, :scheduled) }
it 'is installing' do
subject.make_installing!
@@ -56,7 +56,7 @@ describe Clusters::Applications::Helm do
end
describe '#make_installed' do
- subject { create(:cluster_applications_helm, :installing) }
+ subject { create(:clusters_applications_helm, :installing) }
it 'is installed' do
subject.make_installed
@@ -66,7 +66,7 @@ describe Clusters::Applications::Helm do
end
describe '#make_errored' do
- subject { create(:cluster_applications_helm, :installing) }
+ subject { create(:clusters_applications_helm, :installing) }
let(:reason) { 'some errors' }
it 'is errored' do
@@ -78,7 +78,7 @@ describe Clusters::Applications::Helm do
end
describe '#make_scheduled' do
- subject { create(:cluster_applications_helm, :installable) }
+ subject { create(:clusters_applications_helm, :installable) }
it 'is scheduled' do
subject.make_scheduled
@@ -87,7 +87,7 @@ describe Clusters::Applications::Helm do
end
describe 'when was errored' do
- subject { create(:cluster_applications_helm, :errored) }
+ subject { create(:clusters_applications_helm, :errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index b83472e1944..619c088b0bf 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -4,105 +4,5 @@ describe Clusters::Applications::Ingress do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) }
- describe '#name' do
- it 'is .application_name' do
- expect(subject.name).to eq(described_class.application_name)
- end
-
- it 'is recorded in Clusters::Cluster::APPLICATIONS' do
- expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
- end
- end
-
- describe '#status' do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
-
- subject { described_class.new(cluster: cluster) }
-
- it 'defaults to :not_installable' do
- expect(subject.status_name).to be(:not_installable)
- end
-
- context 'when application helm is scheduled' do
- before do
- create(:cluster_applications_helm, :scheduled, cluster: cluster)
- end
-
- it 'defaults to :not_installable' do
- expect(subject.status_name).to be(:not_installable)
- end
- end
-
- context 'when application helm is installed' do
- before do
- create(:cluster_applications_helm, :installed, cluster: cluster)
- end
-
- it 'defaults to :installable' do
- expect(subject.status_name).to be(:installable)
- end
- end
- end
-
- describe '#install_command' do
- it 'has all the needed information' do
- expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false, chart: subject.chart)
- end
- end
-
- describe 'status state machine' do
- describe '#make_installing' do
- subject { create(:cluster_applications_ingress, :scheduled) }
-
- it 'is installing' do
- subject.make_installing!
-
- expect(subject).to be_installing
- end
- end
-
- describe '#make_installed' do
- subject { create(:cluster_applications_ingress, :installing) }
-
- it 'is installed' do
- subject.make_installed
-
- expect(subject).to be_installed
- end
- end
-
- describe '#make_errored' do
- subject { create(:cluster_applications_ingress, :installing) }
- let(:reason) { 'some errors' }
-
- it 'is errored' do
- subject.make_errored(reason)
-
- expect(subject).to be_errored
- expect(subject.status_reason).to eq(reason)
- end
- end
-
- describe '#make_scheduled' do
- subject { create(:cluster_applications_ingress, :installable) }
-
- it 'is scheduled' do
- subject.make_scheduled
-
- expect(subject).to be_scheduled
- end
-
- describe 'when was errored' do
- subject { create(:cluster_applications_ingress, :errored) }
-
- it 'clears #status_reason' do
- expect(subject.status_reason).not_to be_nil
-
- subject.make_scheduled!
-
- expect(subject.status_reason).to be_nil
- end
- end
- end
- end
+ include_examples 'cluster application specs', described_class
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
new file mode 100644
index 00000000000..696099f7cf7
--- /dev/null
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Prometheus do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:cluster) }
+
+ include_examples 'cluster application specs', described_class
+
+ describe "#chart_values_file" do
+ subject { create(:clusters_applications_prometheus).chart_values_file }
+
+ it 'should return chart values file path' do
+ expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
+ end
+ end
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 2683d21ddbe..799d7ced116 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -5,6 +5,9 @@ describe Clusters::Cluster do
it { is_expected.to have_many(:projects) }
it { is_expected.to have_one(:provider_gcp) }
it { is_expected.to have_one(:platform_kubernetes) }
+ it { is_expected.to have_one(:application_helm) }
+ it { is_expected.to have_one(:application_ingress) }
+ it { is_expected.to have_one(:application_prometheus) }
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
it { is_expected.to delegate_method(:status_name).to(:provider) }
@@ -190,11 +193,12 @@ describe Clusters::Cluster do
end
context 'when applications are created' do
- let!(:helm) { create(:cluster_applications_helm, cluster: cluster) }
- let!(:ingress) { create(:cluster_applications_ingress, cluster: cluster) }
+ let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
+ let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress)
+ is_expected.to contain_exactly(helm, ingress, prometheus)
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index d18a5c9dfa6..817254c7d1e 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -13,6 +13,45 @@ describe Commit do
it { is_expected.to include_module(StaticModel) }
end
+ describe '.lazy' do
+ set(:project) { create(:project, :repository) }
+
+ context 'when the commits are found' do
+ let(:oids) do
+ %w(
+ 498214de67004b1da3d820901307bed2a68a8ef6
+ c642fe9b8b9f28f9225d7ea953fe14e74748d53b
+ 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ 048721d90c449b244b7b4c53a9186b04330174ec
+ 281d3a76f31c812dbf48abce82ccf6860adedd81
+ )
+ end
+
+ subject { oids.map { |oid| described_class.lazy(project, oid) } }
+
+ it 'batches requests for commits' do
+ expect(project.repository).to receive(:commits_by).once.and_call_original
+
+ subject.first.title
+ subject.last.title
+ end
+
+ it 'maintains ordering' do
+ subject.each_with_index do |commit, i|
+ expect(commit.id).to eq(oids[i])
+ end
+ end
+ end
+
+ context 'when not found' do
+ it 'returns nil as commit' do
+ commit = described_class.lazy(project, 'deadbeef').__sync
+
+ expect(commit).to be_nil
+ end
+ end
+ end
+
describe '#author' do
it 'looks up the author in a case-insensitive way' do
user = create(:user, email: commit.author_email.upcase)
@@ -142,7 +181,6 @@ eos
it { is_expected.to respond_to(:parents) }
it { is_expected.to respond_to(:date) }
it { is_expected.to respond_to(:diffs) }
- it { is_expected.to respond_to(:tree) }
it { is_expected.to respond_to(:id) }
it { is_expected.to respond_to(:to_patch) }
end
@@ -154,8 +192,8 @@ eos
let(:commiter) { create :user }
before do
- project.team << [commiter, :developer]
- other_project.team << [commiter, :developer]
+ project.add_developer(commiter)
+ other_project.add_developer(commiter)
end
it 'detects issues that this commit is marked as closing' do
diff --git a/spec/models/concerns/blocks_json_serialization_spec.rb b/spec/models/concerns/blocks_json_serialization_spec.rb
new file mode 100644
index 00000000000..5906b588d0e
--- /dev/null
+++ b/spec/models/concerns/blocks_json_serialization_spec.rb
@@ -0,0 +1,17 @@
+require 'rails_helper'
+
+describe BlocksJsonSerialization do
+ DummyModel = Class.new do
+ include BlocksJsonSerialization
+ end
+
+ it 'blocks as_json' do
+ expect { DummyModel.new.as_json }
+ .to raise_error(described_class::JsonSerializationError, /DummyModel/)
+ end
+
+ it 'blocks to_json' do
+ expect { DummyModel.new.to_json }
+ .to raise_error(described_class::JsonSerializationError, /DummyModel/)
+ end
+end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 129dfa07f15..3c7f578975b 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -102,6 +102,26 @@ describe CacheMarkdownField do
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end
+ context 'when a markdown field is set repeatedly to an empty string' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.foo = ''
+ thing.save
+ thing.foo = ''
+ thing.save
+ end
+ end
+
+ context 'when a markdown field is set repeatedly to a string which renders as empty html' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.foo = '[//]: # (This is also a comment.)'
+ thing.save
+ thing.foo = '[//]: # (This is also a comment.)'
+ thing.save
+ end
+ end
+
context 'a non-markdown field changed' do
before do
thing.bar = 'OK'
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
new file mode 100644
index 00000000000..7bb89fe41dc
--- /dev/null
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -0,0 +1,73 @@
+require 'rails_helper'
+
+describe DeploymentPlatform do
+ let(:project) { create(:project) }
+
+ describe '#deployment_platform' do
+ subject { project.deployment_platform }
+
+ context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service and a Kubernetes template configured' do
+ let!(:kubernetes_service) { create(:kubernetes_service, template: true) }
+
+ it 'returns a platform kubernetes' do
+ expect(subject).to be_a_kind_of(Clusters::Platforms::Kubernetes)
+ end
+
+ it 'creates a cluster and a platform kubernetes' do
+ expect { subject }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(1)
+ end
+
+ it 'includes appropriate attributes for Cluster' do
+ cluster = subject.cluster
+ expect(cluster.name).to eq('kubernetes-template')
+ expect(cluster.project).to eq(project)
+ expect(cluster.provider_type).to eq('user')
+ expect(cluster.platform_type).to eq('kubernetes')
+ end
+
+ it 'creates a platform kubernetes' do
+ expect { subject }.to change { Clusters::Platforms::Kubernetes.count }.by(1)
+ end
+
+ it 'copies attributes from Clusters::Platform::Kubernetes template into the new Cluster::Platforms::Kubernetes' do
+ expect(subject.api_url).to eq(kubernetes_service.api_url)
+ expect(subject.ca_pem).to eq(kubernetes_service.ca_pem)
+ expect(subject.token).to eq(kubernetes_service.token)
+ expect(subject.namespace).to eq(kubernetes_service.namespace)
+ end
+ end
+
+ context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service and no Kubernetes template configured' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:platform_kubernetes) { cluster.platform_kubernetes }
+
+ it 'returns the Kubernetes platform' do
+ expect(subject).to eq(platform_kubernetes)
+ end
+ end
+
+ context 'when user configured kubernetes integration from project services' do
+ let!(:kubernetes_service) { create(:kubernetes_service, project: project) }
+
+ it 'returns the Kubernetes service' do
+ expect(subject).to eq(kubernetes_service)
+ end
+ end
+
+ context 'when the cluster creation fails' do
+ let!(:kubernetes_service) { create(:kubernetes_service, template: true) }
+
+ before do
+ allow_any_instance_of(Clusters::Cluster).to receive(:persisted?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 9df26f06a11..4b217df2e8f 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -291,7 +291,7 @@ describe Issuable do
context 'total_time_spent is updated' do
before do
- issue.spend_time(duration: 2, user: user, spent_at: Time.now)
+ issue.spend_time(duration: 2, user_id: user.id, spent_at: Time.now)
issue.save
expect(Gitlab::HookData::IssuableBuilder)
.to receive(:new).with(issue).and_return(builder)
@@ -485,7 +485,7 @@ describe Issuable do
let(:issue) { create(:issue) }
def spend_time(seconds)
- issue.spend_time(duration: seconds, user: user)
+ issue.spend_time(duration: seconds, user_id: user.id)
issue.save!
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 8b545aec7f5..c73ea6aa94c 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -62,7 +62,7 @@ describe Issue, "Mentionable" do
context 'when the current user can see the issue' do
before do
- private_project.team << [user, Gitlab::Access::DEVELOPER]
+ private_project.add_developer(user)
end
it 'includes the reference' do
@@ -107,7 +107,7 @@ describe Issue, "Mentionable" do
let(:issues) { create_list(:issue, 2, project: project, author: author) }
before do
- project.team << [author, Gitlab::Access::DEVELOPER]
+ project.add_developer(author)
end
context 'before changes are persisted' do
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 9048da0c73d..87bf731340f 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -24,8 +24,8 @@ describe Milestone, 'Milestoneish' do
let(:label_3) { create(:label, title: 'label_3', project: project) }
before do
- project.team << [member, :developer]
- project.team << [guest, :guest]
+ project.add_developer(member)
+ project.add_guest(guest)
end
describe '#sorted_issues' do
@@ -189,9 +189,9 @@ describe Milestone, 'Milestoneish' do
describe '#total_issue_time_spent' do
it 'calculates total issue time spent' do
- closed_issue_1.spend_time(duration: 300, user: author)
+ closed_issue_1.spend_time(duration: 300, user_id: author.id)
closed_issue_1.save!
- closed_issue_2.spend_time(duration: 600, user: assignee)
+ closed_issue_2.spend_time(duration: 600, user_id: assignee.id)
closed_issue_2.save!
expect(milestone.total_issue_time_spent).to eq(900)
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
index 1616c2ea985..2a2ef5a304d 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.team << [current_user, :master]
+ subject.project.add_master(current_user)
end
it "returns true" do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index c5708e70ef9..ba8aa13d5ad 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -126,7 +126,7 @@ describe Deployment do
subject { deployment.stop_action }
context 'when no other actions' do
- let(:deployment) { FactoryGirl.build(:deployment, deployable: build) }
+ let(:deployment) { FactoryBot.build(:deployment, deployable: build) }
it { is_expected.to be_nil }
end
@@ -135,13 +135,13 @@ describe Deployment do
let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
context 'when matching action is defined' do
- let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') }
+ let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_other_app') }
it { is_expected.to be_nil }
end
context 'when no matching action is defined' do
- let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+ let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_app') }
it { is_expected.to eq(close_action) }
end
@@ -159,7 +159,7 @@ describe Deployment do
context 'when matching action is defined' do
let(:build) { create(:ci_build) }
- let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+ let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_app') }
let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
it { is_expected.to be_truthy }
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
index fa02434b0fd..50b19000799 100644
--- a/spec/models/diff_discussion_spec.rb
+++ b/spec/models/diff_discussion_spec.rb
@@ -47,8 +47,20 @@ describe DiffDiscussion do
diff_note.save!
end
- it 'returns the diff ID for the version to show' do
- expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id)
+ context 'when commit_id is not present' do
+ it 'returns the diff ID for the version to show' do
+ expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id)
+ end
+ end
+
+ context 'when commit_id is present' do
+ before do
+ diff_note.update_attribute(:commit_id, 'commit_123')
+ end
+
+ it 'includes the commit_id in the result' do
+ expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id, commit_id: 'commit_123')
+ end
end
end
@@ -70,8 +82,20 @@ describe DiffDiscussion do
diff_note.save!
end
- it 'returns the diff ID and start sha of the versions to compare' do
- expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
+ context 'when commit_id is not present' do
+ it 'returns the diff ID and start sha of the versions to compare' do
+ expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
+ end
+ end
+
+ context 'when commit_id is present' do
+ before do
+ diff_note.update_attribute(:commit_id, 'commit_123')
+ end
+
+ it 'includes the commit_id in the result' do
+ expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha, commit_id: 'commit_123')
+ end
end
end
@@ -83,8 +107,20 @@ describe DiffDiscussion do
diff_note.save!
end
- it 'returns nil' do
- expect(subject.merge_request_version_params).to be_nil
+ context 'when commit_id is not present' do
+ it 'returns empty hash' do
+ expect(subject.merge_request_version_params).to eq(nil)
+ end
+ end
+
+ context 'when commit_id is present' do
+ before do
+ diff_note.update_attribute(:commit_id, 'commit_123')
+ end
+
+ it 'returns the commit_id' do
+ expect(subject.merge_request_version_params).to eq(commit_id: 'commit_123')
+ end
end
end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 4d0b3245a13..2705421e540 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -112,22 +112,6 @@ describe DiffNote do
end
end
- describe "#for_line?" do
- context "when provided the correct diff line" do
- it "returns true" do
- expect(subject.for_line?(subject.diff_line)).to be true
- end
- end
-
- context "when provided a different diff line" do
- it "returns false" do
- some_line = subject.diff_file.diff_lines.first
-
- expect(subject.for_line?(some_line)).to be false
- end
- end
- end
-
describe "#active?" do
context "when noteable is a commit" do
subject { build(:diff_note_on_commit, project: project, position: position) }
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index aa7a8342a4c..67f49348acb 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -125,8 +125,8 @@ describe Event do
let(:event) { described_class.new(project: project, target: target, author_id: author.id) }
before do
- project.team << [member, :developer]
- project.team << [guest, :guest]
+ project.add_developer(member)
+ project.add_guest(guest)
end
context 'commit note event' do
@@ -347,6 +347,22 @@ describe Event do
end
end
+ describe '#target' do
+ it 'eager loads the author of an event target' do
+ create(:closed_issue_event)
+
+ events = described_class.preload(:target).all.to_a
+ count = ActiveRecord::QueryRecorder
+ .new { events.first.target.author }.count
+
+ # This expectation exists to make sure the test doesn't pass when the
+ # author is for some reason not loaded at all.
+ expect(events.first.target.author).to be_an_instance_of(User)
+
+ expect(count).to be_zero
+ end
+ end
+
def create_push_event(project, user)
event = create(:push_event, project: project, author: user)
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 7f1909710d8..673049d1cc4 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -43,7 +43,7 @@ describe GenericCommitStatus do
context 'when user has ability to see datails' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'details path points to an external URL' do
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 431e3db9f00..0e965f541d8 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -62,7 +62,7 @@ describe SystemHook do
end
it "project_create hook" do
- project.team << [user, :master]
+ project.add_master(user)
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_team/,
@@ -71,7 +71,7 @@ describe SystemHook do
end
it "project_destroy hook" do
- project.team << [user, :master]
+ project.add_master(user)
project.project_members.destroy_all
expect(WebMock).to have_requested(:post, system_hook.url).with(
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index a45a6088831..7c66c98231b 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -44,4 +44,31 @@ describe Identity do
end
end
end
+
+ context 'callbacks' do
+ context 'before_save' do
+ describe 'normalizes extern uid' do
+ let!(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com') }
+
+ it 'if extern_uid changes' do
+ expect(ldap_identity).not_to receive(:ensure_normalized_extern_uid)
+ ldap_identity.save
+ end
+
+ it 'if current_uid is nil' do
+ expect(ldap_identity).to receive(:ensure_normalized_extern_uid)
+
+ ldap_identity.update(extern_uid: nil)
+
+ expect(ldap_identity.extern_uid).to be_nil
+ end
+
+ it 'if extern_uid changed and not nil' do
+ ldap_identity.update(extern_uid: 'uid=john1,ou=PEOPLE,dc=example,dc=com')
+
+ expect(ldap_identity.extern_uid).to eq 'uid=john1,ou=people,dc=example,dc=com'
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index 34d98a3c975..580a98193af 100644
--- a/spec/models/issue_collection_spec.rb
+++ b/spec/models/issue_collection_spec.rb
@@ -42,7 +42,7 @@ describe IssueCollection do
context 'using a user that has reporter access to the project' do
it 'returns the issues of the project' do
- project.team << [user, :reporter]
+ project.add_reporter(user)
expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 0ea287d007a..5ced000cdb6 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -23,6 +23,32 @@ describe Issue do
it { is_expected.to have_db_index(:deleted_at) }
end
+ describe 'callbacks' do
+ describe '#ensure_metrics' do
+ it 'creates metrics after saving' do
+ issue = create(:issue)
+
+ expect(issue.metrics).to be_persisted
+ expect(Issue::Metrics.count).to eq(1)
+ end
+
+ it 'does not create duplicate metrics for an issue' do
+ issue = create(:issue)
+
+ issue.close!
+
+ expect(issue.metrics).to be_persisted
+ expect(Issue::Metrics.count).to eq(1)
+ end
+
+ it 'records current metrics' do
+ expect_any_instance_of(Issue::Metrics).to receive(:record!)
+
+ create(:issue)
+ end
+ end
+ end
+
describe '#order_by_position_and_priority' do
let(:project) { create :project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
@@ -238,7 +264,7 @@ describe Issue do
let(:issue) { create(:issue, project: project) }
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
it { is_expected.to eq true }
@@ -254,7 +280,7 @@ describe Issue do
context 'destination project allowed' do
before do
- to_project.team << [user, :reporter]
+ to_project.add_reporter(user)
end
it { is_expected.to eq true }
@@ -262,7 +288,7 @@ describe Issue do
context 'destination project not allowed' do
before do
- to_project.team << [user, :guest]
+ to_project.add_guest(user)
end
it { is_expected.to eq false }
@@ -550,7 +576,7 @@ describe Issue do
context 'when the user is the project owner' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'returns true for a regular issue' do
@@ -574,7 +600,7 @@ describe Issue do
context 'using a public project' do
before do
- project.team << [user, Gitlab::Access::DEVELOPER]
+ project.add_developer(user)
end
it 'returns true for a regular issue' do
@@ -594,7 +620,7 @@ describe Issue do
let(:project) { create(:project, :internal) }
before do
- project.team << [user, Gitlab::Access::DEVELOPER]
+ project.add_developer(user)
end
it 'returns true for a regular issue' do
@@ -614,7 +640,7 @@ describe Issue do
let(:project) { create(:project, :private) }
before do
- project.team << [user, Gitlab::Access::DEVELOPER]
+ project.add_developer(user)
end
it 'returns true for a regular issue' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 0a017c068ad..6aa0e7f49c3 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -62,12 +62,12 @@ 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.team << [u, :master] }
+ @master_user = create(:user).tap { |u| project.add_master(u) }
@master = project.members.find_by(user_id: @master_user.id)
@blocked_user = create(:user).tap do |u|
- project.team << [u, :master]
- project.team << [u, :developer]
+ project.add_master(u)
+ project.add_developer(u)
u.block!
end
@@ -527,7 +527,7 @@ describe Member do
it "refreshes user's authorized projects" do
project = create(:project, :private)
user = create(:user)
- member = project.team << [user, :reporter]
+ member = project.add_reporter(user)
member.destroy
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index fa3e80ba062..3e46fa36375 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -88,8 +88,8 @@ describe ProjectMember do
@user_1 = create :user
@user_2 = create :user
- @project_1.team << [@user_1, :developer]
- @project_2.team << [@user_2, :reporter]
+ @project_1.add_developer(@user_1)
+ @project_2.add_reporter(@user_2)
@status = @project_2.team.import(@project_1)
end
@@ -136,8 +136,8 @@ describe ProjectMember do
@user_1 = create :user
@user_2 = create :user
- @project_1.team << [@user_1, :developer]
- @project_2.team << [@user_2, :reporter]
+ @project_1.add_developer(@user_1)
+ @project_2.add_reporter(@user_2)
described_class.truncate_teams([@project_1.id, @project_2.id])
end
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index 9353d5c3c8a..02ff7839739 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -1,16 +1,11 @@
require 'spec_helper'
describe MergeRequest::Metrics do
- subject { create(:merge_request) }
+ subject { described_class.new }
- describe "when recording the default set of metrics on merge request save" do
- it "records the merge time" do
- time = Time.now
- Timecop.freeze(time) { subject.mark_as_merged }
- metrics = subject.metrics
-
- expect(metrics).to be_present
- expect(metrics.merged_at).to be_like_time(time)
- end
+ describe 'associations' do
+ it { is_expected.to belong_to(:merge_request) }
+ it { is_expected.to belong_to(:latest_closed_by).class_name('User') }
+ it { is_expected.to belong_to(:merged_by).class_name('User') }
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 30a5a3bbff7..07b3e1c1758 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -65,6 +65,25 @@ describe MergeRequest do
end
end
+ describe 'callbacks' do
+ describe '#ensure_merge_request_metrics' do
+ it 'creates metrics after saving' do
+ merge_request = create(:merge_request)
+
+ expect(merge_request.metrics).to be_persisted
+ expect(MergeRequest::Metrics.count).to eq(1)
+ end
+
+ it 'does not duplicate metrics for a merge request' do
+ merge_request = create(:merge_request)
+
+ merge_request.mark_as_merged!
+
+ expect(MergeRequest::Metrics.count).to eq(1)
+ end
+ end
+ end
+
describe 'respond to' do
it { is_expected.to respond_to(:unchecked?) }
it { is_expected.to respond_to(:can_be_merged?) }
@@ -124,6 +143,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
project.repository.rm_branch(subject.author, subject.target_branch)
+ subject.clear_memoized_shas
end
it 'returns nil' do
@@ -194,7 +214,7 @@ describe MergeRequest do
describe '#cache_merge_request_closes_issues!' do
before do
- subject.project.team << [subject.author, :developer]
+ subject.project.add_developer(subject.author)
subject.target_branch = subject.project.default_branch
end
@@ -480,7 +500,7 @@ describe MergeRequest do
let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
before do
- subject.project.team << [subject.author, :developer]
+ subject.project.add_developer(subject.author)
allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
end
@@ -508,7 +528,7 @@ describe MergeRequest do
let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
it 'detects issues mentioned in description but not closed' do
- subject.project.team << [subject.author, :developer]
+ subject.project.add_developer(subject.author)
subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
allow(subject).to receive(:commits).and_return([commit])
@@ -520,7 +540,7 @@ describe MergeRequest do
context 'when the project has an external issue tracker' do
before do
- subject.project.team << [subject.author, :developer]
+ subject.project.add_developer(subject.author)
commit = double(:commit, safe_message: 'Fixes TEST-3')
create(:jira_service, project: subject.project)
@@ -600,30 +620,30 @@ describe MergeRequest do
end
describe '#can_remove_source_branch?' do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ set(:user) { create(:user) }
+ set(:merge_request) { create(:merge_request, :simple) }
- before do
- subject.source_project.team << [user, :master]
+ subject { merge_request }
- subject.source_branch = "feature"
- subject.target_branch = "master"
- subject.save!
+ before do
+ subject.source_project.add_master(user)
end
it "can't be removed when its a protected branch" do
allow(ProtectedBranch).to receive(:protected?).and_return(true)
+
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
it "can't remove a root ref" do
- subject.source_branch = "master"
- subject.target_branch = "feature"
+ subject.update(source_branch: 'master', target_branch: 'feature')
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
it "is unable to remove the source branch for a project the user cannot push to" do
+ user2 = create(:user)
+
expect(subject.can_remove_source_branch?(user2)).to be_falsey
end
@@ -634,6 +654,7 @@ describe MergeRequest do
end
it "cannot be removed if the last commit is not also the head of the source branch" do
+ subject.clear_memoized_shas
subject.source_branch = "lfs"
expect(subject.can_remove_source_branch?(user)).to be_falsey
@@ -658,7 +679,7 @@ describe MergeRequest do
it 'includes its closed issues in the body' do
issue = create(:issue, project: subject.project)
- subject.project.team << [subject.author, :developer]
+ subject.project.add_developer(subject.author)
subject.description = "This issue Closes #{issue.to_reference}"
allow(subject.project).to receive(:default_branch)
@@ -733,7 +754,7 @@ describe MergeRequest do
before do
project.repository.raw_repository.delete_branch(subject.target_branch)
- subject.reload
+ subject.clear_memoized_shas
end
it 'does not crash' do
@@ -1404,6 +1425,16 @@ describe MergeRequest do
subject.reload_diff
end
+
+ context 'when using the after_update hook to update' do
+ context 'when the branches are updated' do
+ it 'uses the new heads to generate the diff' do
+ expect { subject.update!(source_branch: subject.target_branch, target_branch: subject.source_branch) }
+ .to change { subject.merge_request_diff.start_commit_sha }
+ .and change { subject.merge_request_diff.head_commit_sha }
+ end
+ end
+ end
end
describe '#update_diff_discussion_positions' do
@@ -1468,6 +1499,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
subject.project.repository.rm_branch(subject.author, subject.target_branch)
+ subject.clear_memoized_shas
end
it 'returns nil' do
@@ -1675,7 +1707,7 @@ describe MergeRequest do
let(:mr_sha) { merge_request.diff_head_sha }
before do
- project.team << [developer, :developer]
+ project.add_developer(developer)
end
context 'when autocomplete_precheck is set to true' do
@@ -1855,4 +1887,66 @@ describe MergeRequest do
it_behaves_like 'throttled touch' do
subject { create(:merge_request, updated_at: 1.hour.ago) }
end
+
+ context 'state machine transitions' do
+ describe '#unlock_mr' do
+ subject { create(:merge_request, state: 'locked', merge_jid: 123) }
+
+ it 'updates merge request head pipeline and sets merge_jid to nil' do
+ pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)
+
+ subject.unlock_mr
+
+ subject.reload
+ expect(subject.head_pipeline).to eq(pipeline)
+ expect(subject.merge_jid).to be_nil
+ end
+ end
+ end
+
+ describe '#should_be_rebased?' do
+ let(:project) { create(:project, :repository) }
+
+ it 'returns false for the same source and target branches' do
+ merge_request = create(:merge_request, source_project: project, target_project: project)
+
+ expect(merge_request.should_be_rebased?).to be_falsey
+ end
+ end
+
+ describe '#rebase_in_progress?' do
+ # Create merge request and project before we stub file calls
+ before do
+ subject
+ end
+
+ it 'returns true when there is a current rebase directory' do
+ allow(File).to receive(:exist?).and_return(true)
+ allow(File).to receive(:mtime).and_return(Time.now)
+
+ expect(subject.rebase_in_progress?).to be_truthy
+ end
+
+ it 'returns false when there is no rebase directory' do
+ allow(File).to receive(:exist?).and_return(false)
+
+ expect(subject.rebase_in_progress?).to be_falsey
+ end
+
+ it 'returns false when the rebase directory has expired' do
+ allow(File).to receive(:exist?).and_return(true)
+ allow(File).to receive(:mtime).and_return(20.minutes.ago)
+
+ expect(subject.rebase_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)
+ allow(File).to receive(:exist?).and_return(true)
+ allow(File).to receive(:mtime).and_return(Time.now)
+
+ expect(File).not_to have_received(:exist?)
+ expect(subject.rebase_in_progress?).to be_falsey
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 3817f20bfe7..b3f160f3119 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -203,7 +203,7 @@ describe Namespace do
context 'with subgroups' do
let(:parent) { create(:group, name: 'parent', path: 'parent') }
let(:child) { create(:group, name: 'child', path: 'child', parent: parent) }
- let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) }
+ let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child, skip_disk_validation: true) }
let(:uploads_dir) { File.join(CarrierWave.root, FileUploader.base_dir) }
let(:pages_dir) { File.join(TestEnv.pages_path) }
@@ -240,6 +240,24 @@ describe Namespace do
end
end
end
+
+ it 'updates project full path in .git/config for each project inside namespace' do
+ parent = create(:group, name: 'mygroup', path: 'mygroup')
+ subgroup = create(:group, name: 'mysubgroup', path: 'mysubgroup', parent: parent)
+ project_in_parent_group = create(:project, :repository, namespace: parent, name: 'foo1')
+ hashed_project_in_subgroup = create(:project, :repository, :hashed, namespace: subgroup, name: 'foo2')
+ legacy_project_in_subgroup = create(:project, :repository, namespace: subgroup, name: 'foo3')
+
+ parent.update(path: 'mygroup_new')
+
+ expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}"
+ expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}"
+ expect(project_rugged(legacy_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}"
+ end
+
+ def project_rugged(project)
+ project.repository.rugged
+ end
end
describe '#rm_dir', 'callback' do
@@ -559,4 +577,34 @@ describe Namespace do
end
end
end
+
+ describe "#allowed_path_by_redirects" do
+ let(:namespace1) { create(:namespace, path: 'foo') }
+
+ context "when the path has been taken before" do
+ before do
+ namespace1.path = 'bar'
+ namespace1.save!
+ end
+
+ it 'should be invalid' do
+ namespace2 = build(:group, path: 'foo')
+ expect(namespace2).to be_invalid
+ end
+
+ it 'should return an error on path' do
+ namespace2 = build(:group, path: 'foo')
+ namespace2.valid?
+ expect(namespace2.errors.messages[:path].first).to eq('foo has been taken before. Please use another one')
+ end
+ end
+
+ context "when the path has not been taken before" do
+ it 'should be valid' do
+ expect(RedirectRoute.count).to eq(0)
+ namespace = build(:namespace)
+ expect(namespace).to be_valid
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index e1a0c55b6a6..3d030927036 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -195,7 +195,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.team << [private_user, :master] } }
+ let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_master(private_user) } }
let(:private_issue) { create(:issue, project: private_project) }
let(:ext_proj) { create(:project, :public) }
@@ -756,6 +756,28 @@ describe Note do
end
end
+ describe '#references' do
+ context 'when part of a discussion' do
+ it 'references all earlier notes in the discussion' do
+ first_note = create(:discussion_note_on_issue)
+ second_note = create(:discussion_note_on_issue, in_reply_to: first_note)
+ third_note = create(:discussion_note_on_issue, in_reply_to: second_note)
+ create(:discussion_note_on_issue, in_reply_to: third_note)
+
+ expect(third_note.references).to eq([first_note.noteable, first_note, second_note])
+ end
+ end
+
+ context 'when not part of a discussion' do
+ subject { create(:note) }
+ let(:note) { create(:note, in_reply_to: subject) }
+
+ it 'returns the noteable' do
+ expect(note.references).to eq([note.noteable])
+ end
+ end
+ end
+
describe 'expiring ETag cache' do
let(:note) { build(:note_on_issue) }
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index de3e86b627f..63c6fbda3f2 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -37,7 +37,7 @@ describe ProjectFeature do
end
it "returns true when user is a team member" do
- project.team << [user, :developer]
+ project.add_developer(user)
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index ad22fb2a386..c9b3c6cf602 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -395,6 +395,26 @@ describe JiraService do
end
end
+ describe 'additional cookies' do
+ let(:project) { create(:project) }
+
+ context 'provides additional cookies to allow basic auth with oracle webgate' do
+ before do
+ @service = project.create_jira_service(
+ active: true, properties: { url: 'http://jira.com' })
+ end
+
+ after do
+ @service.destroy!
+ end
+
+ it 'is initialized' do
+ expect(@service.options[:use_cookies]).to eq(true)
+ expect(@service.options[:additional_cookies]).to eq(["OBBasicAuth=fromDialog"])
+ end
+ end
+ end
+
describe 'project and issue urls' do
let(:project) { create(:project) }
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index f037ee77a94..6980ba335b8 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -52,12 +52,75 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'when service is inactive' do
before do
+ subject.project = project
subject.active = false
end
it { is_expected.not_to validate_presence_of(:api_url) }
it { is_expected.not_to validate_presence_of(:token) }
end
+
+ context 'with a deprecated service' do
+ let(:kubernetes_service) { create(:kubernetes_service) }
+
+ before do
+ kubernetes_service.update_attribute(:active, false)
+ kubernetes_service.properties[:namespace] = "foo"
+ end
+
+ it 'should not update attributes' do
+ expect(kubernetes_service.save).to be_falsy
+ end
+
+ it 'should include an error with a deprecation message' do
+ kubernetes_service.valid?
+ expect(kubernetes_service.errors[:base].first).to match(/Kubernetes service integration has been deprecated/)
+ end
+ end
+
+ context 'with a non-deprecated service' do
+ let(:kubernetes_service) { create(:kubernetes_service) }
+
+ it 'should update attributes' do
+ kubernetes_service.properties[:namespace] = 'foo'
+ expect(kubernetes_service.save).to be_truthy
+ end
+ end
+
+ context 'with an active and deprecated service' do
+ let(:kubernetes_service) { create(:kubernetes_service) }
+
+ before do
+ kubernetes_service.active = false
+ kubernetes_service.properties[:namespace] = 'foo'
+ kubernetes_service.save
+ end
+
+ it 'should deactive the service' do
+ expect(kubernetes_service.active?).to be_falsy
+ end
+
+ it 'should not include a deprecation message as error' do
+ expect(kubernetes_service.errors.messages.count).to eq(0)
+ end
+
+ it 'should update attributes' do
+ expect(kubernetes_service.properties[:namespace]).to eq("foo")
+ end
+ end
+
+ context 'with a template service' do
+ let(:kubernetes_service) { create(:kubernetes_service, template: true, active: false) }
+
+ before do
+ 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')
+ end
+ end
end
describe '#initialize_properties' do
@@ -318,4 +381,42 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it { is_expected.to eq(pods: []) }
end
end
+
+ describe "#deprecated?" do
+ let(:kubernetes_service) { create(:kubernetes_service) }
+
+ context 'with an active kubernetes service' do
+ it 'should return false' do
+ expect(kubernetes_service.deprecated?).to be_falsy
+ end
+ end
+
+ context 'with a inactive kubernetes service' do
+ it 'should return true' do
+ kubernetes_service.update_attribute(:active, false)
+ expect(kubernetes_service.deprecated?).to be_truthy
+ end
+ end
+ end
+
+ describe "#deprecation_message" do
+ let(:kubernetes_service) { create(:kubernetes_service) }
+
+ it 'should indicate the service is deprecated' do
+ expect(kubernetes_service.deprecation_message).to match(/Kubernetes service integration has been deprecated/)
+ end
+
+ context 'if the services is active' do
+ it 'should return a message' do
+ expect(kubernetes_service.deprecation_message).to match(/Your cluster information on this page is still editable/)
+ end
+ end
+
+ context 'if the service is not active' do
+ it 'should return a message' do
+ kubernetes_service.update_attribute(:active, false)
+ expect(kubernetes_service.deprecation_message).to match(/Fields on this page are now uneditable/)
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index be07ca2d945..75ae2207910 100644
--- a/spec/models/project_services/pipelines_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
@@ -37,7 +37,7 @@ describe PipelinesEmailService, :mailer do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'builds test data' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f4699fd243d..00afa09f1a3 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -93,7 +93,7 @@ describe Project do
let(:developer) { create(:user) }
before do
project.request_access(requester)
- project.team << [developer, :developer]
+ project.add_developer(developer)
end
it_behaves_like 'members and requesters associations' do
@@ -289,12 +289,12 @@ describe Project do
describe 'project token' do
it 'sets an random token if none provided' do
- project = FactoryGirl.create :project, runners_token: ''
+ project = FactoryBot.create :project, runners_token: ''
expect(project.runners_token).not_to eq('')
end
it 'does not set an random token if one provided' do
- project = FactoryGirl.create :project, runners_token: 'my-token'
+ project = FactoryBot.create :project, runners_token: 'my-token'
expect(project.runners_token).to eq('my-token')
end
end
@@ -418,14 +418,21 @@ describe Project do
end
describe '#merge_method' do
- it 'returns "ff" merge_method when ff is enabled' do
- project = build(:project, merge_requests_ff_only_enabled: true)
- expect(project.merge_method).to be :ff
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ff, :rebase, :method) do
+ true | true | :ff
+ true | false | :ff
+ false | true | :rebase_merge
+ false | false | :merge
end
- it 'returns "merge" merge_method when ff is disabled' do
- project = build(:project, merge_requests_ff_only_enabled: false)
- expect(project.merge_method).to be :merge
+ with_them do
+ let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) }
+
+ subject { project.merge_method }
+
+ it { is_expected.to eq(method) }
end
end
@@ -520,7 +527,7 @@ describe Project do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'with default issues tracker' do
@@ -1435,35 +1442,35 @@ describe Project do
let(:user) { create(:user) }
it 'returns false when default_branch_protection is in full protection and user is developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
end
it 'returns false when default_branch_protection only lets devs merge and user is dev' do
- project.team << [user, :developer]
+ project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
end
it 'returns true when default_branch_protection lets devs push and user is developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
end
it 'returns true when default_branch_protection is unprotected and user is developer' do
- project.team << [user, :developer]
+ project.add_developer(user)
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
end
it 'returns true when user is master' do
- project.team << [user, :master]
+ project.add_master(user)
expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
end
@@ -2626,6 +2633,14 @@ describe Project do
project.rename_repo
end
end
+
+ it 'updates project full path in .git/config' do
+ allow(project_storage).to receive(:rename_repo).and_return(true)
+
+ project.rename_repo
+
+ expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path)
+ end
end
describe '#pages_path' do
@@ -2668,14 +2683,12 @@ describe Project do
end
context 'hashed storage' do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository, skip_disk_validation: true) }
let(:gitlab_shell) { Gitlab::Shell.new }
- let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
+ let(:hash) { Digest::SHA2.hexdigest(project.id.to_s) }
before do
stub_application_setting(hashed_storage_enabled: true)
- allow(Digest::SHA2).to receive(:hexdigest) { hash }
- allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
end
describe '#legacy_storage?' do
@@ -2698,13 +2711,13 @@ describe Project do
describe '#base_dir' do
it 'returns base_dir based on hash of project id' do
- expect(project.base_dir).to eq('@hashed/6b/86')
+ expect(project.base_dir).to eq("@hashed/#{hash[0..1]}/#{hash[2..3]}")
end
end
describe '#disk_path' do
it 'returns disk_path based on hash of project id' do
- hashed_path = '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b'
+ hashed_path = "@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}"
expect(project.disk_path).to eq(hashed_path)
end
@@ -2712,7 +2725,9 @@ describe Project do
describe '#ensure_storage_path_exists' do
it 'delegates to gitlab_shell to ensure namespace is created' do
- expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, '@hashed/6b/86')
+ allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
+
+ expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, "@hashed/#{hash[0..1]}/#{hash[2..3]}")
project.ensure_storage_path_exists
end
@@ -2772,7 +2787,7 @@ describe Project do
end
context 'when not rolled out' do
- let(:project) { create(:project, :repository, storage_version: 1) }
+ let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) }
it 'moves pages folder to new location' do
expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project)
@@ -2781,6 +2796,12 @@ describe Project do
end
end
end
+
+ it 'updates project full path in .git/config' do
+ project.rename_repo
+
+ expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path)
+ end
end
describe '#pages_path' do
@@ -3058,9 +3079,51 @@ describe Project do
expect(project).to receive(:import_finish)
expect(project).to receive(:update_project_counter_caches)
expect(project).to receive(:remove_import_jid)
+ expect(project).to receive(:after_create_default_branch)
project.after_import
end
+
+ context 'branch protection' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does not protect when branch protection is disabled' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+ project.after_import
+
+ expect(project.protected_branches).to be_empty
+ end
+
+ it "gives developer access to push when branch protection is set to 'developers can push'" do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ project.after_import
+
+ 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::DEVELOPER])
+ end
+
+ it "gives developer access to merge when branch protection is set to 'developers can merge'" do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ project.after_import
+
+ expect(project.protected_branches).not_to be_empty
+ expect(project.default_branch).to eq(project.protected_branches.first.name)
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+ end
+
+ it 'protects default branch' do
+ project.after_import
+
+ 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])
+ end
+ end
end
describe '#update_project_counter_caches' do
@@ -3123,22 +3186,25 @@ describe Project do
end
end
- describe '#deployment_platform' do
- subject { project.deployment_platform }
+ describe '#write_repository_config' do
+ set(:project) { create(:project, :repository) }
- let(:project) { create(:project) }
+ 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
+ end
- context 'when user configured kubernetes from Integration > Kubernetes' do
- let!(:kubernetes_service) { create(:kubernetes_service, project: project) }
+ it 'updates full path in .git/config when key is present' do
+ project.write_repository_config(gl_full_path: 'old/path')
- it { is_expected.to eq(kubernetes_service) }
+ expect { project.write_repository_config }.to change { project.repository.rugged.config['gitlab.fullpath'] }.from('old/path').to(project.full_path)
end
- context 'when user configured kubernetes from CI/CD > Clusters' do
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let(:platform_kubernetes) { cluster.platform_kubernetes }
+ it 'does not raise an error with an empty repository' do
+ project = create(:project_empty_repo)
- it { is_expected.to eq(platform_kubernetes) }
+ expect { project.write_repository_config }.not_to raise_error
end
end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 314824b32da..e07c522800a 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -291,8 +291,8 @@ describe ProjectTeam do
group.add_master(master)
group.add_developer(developer)
- members_project.team << [developer, :developer]
- members_project.team << [master, :master]
+ members_project.add_developer(developer)
+ members_project.add_master(master)
create(:project_group_link, project: shared_project, group: group)
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 358bc3dfb94..c0db2c1b386 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -59,12 +59,18 @@ describe Repository do
end
describe 'tags_sorted_by' do
- context 'name' do
- subject { repository.tags_sorted_by('name').map(&:name) }
+ context 'name_desc' do
+ subject { repository.tags_sorted_by('name_desc').map(&:name) }
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
end
+ context 'name_asc' do
+ subject { repository.tags_sorted_by('name_asc').map(&:name) }
+
+ it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
+ end
+
context 'updated' do
let(:tag_a) { repository.find_tag('v1.0.0') }
let(:tag_b) { repository.find_tag('v1.1.0') }
@@ -233,6 +239,54 @@ describe Repository do
end
end
+ describe '#commits_by' do
+ set(:project) { create(:project, :repository) }
+
+ shared_examples 'batch commits fetching' do
+ let(:oids) { TestEnv::BRANCH_SHA.values }
+
+ subject { project.repository.commits_by(oids: oids) }
+
+ it 'finds each commit' do
+ expect(subject).not_to include(nil)
+ expect(subject.size).to eq(oids.size)
+ end
+
+ it 'returns only Commit instances' do
+ expect(subject).to all( be_a(Commit) )
+ end
+
+ context 'when some commits are not found ' do
+ let(:oids) do
+ ['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10)
+ end
+
+ it 'returns only found commits' do
+ expect(subject).not_to include(nil)
+ expect(subject.size).to eq(10)
+ end
+ end
+
+ context 'when no oids are passed' do
+ let(:oids) { [] }
+
+ it 'does not call #batch_by_oid' do
+ expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid)
+
+ subject
+ end
+ end
+ end
+
+ context 'when Gitaly list_commits_by_oid is enabled' do
+ it_behaves_like 'batch commits fetching'
+ end
+
+ context 'when Gitaly list_commits_by_oid is enabled', :disable_gitaly do
+ it_behaves_like 'batch commits fetching'
+ end
+ end
+
describe '#find_commits_by_message' do
shared_examples 'finding commits by message' do
it 'returns commits with messages containing a given string' do
@@ -528,38 +582,6 @@ describe Repository do
end
end
- describe '#get_committer_and_author' do
- it 'returns the committer and author data' do
- options = repository.get_committer_and_author(user)
- expect(options[:committer][:email]).to eq(user.email)
- expect(options[:author][:email]).to eq(user.email)
- end
-
- context 'when the email/name are given' do
- it 'returns an object containing the email/name' do
- options = repository.get_committer_and_author(user, email: author_email, name: author_name)
- expect(options[:author][:email]).to eq(author_email)
- expect(options[:author][:name]).to eq(author_name)
- end
- end
-
- context 'when the email is given but the name is not' do
- it 'returns the committer as the author' do
- options = repository.get_committer_and_author(user, email: author_email)
- expect(options[:author][:email]).to eq(user.email)
- expect(options[:author][:name]).to eq(user.name)
- end
- end
-
- context 'when the name is given but the email is not' do
- it 'returns nil' do
- options = repository.get_committer_and_author(user, name: author_name)
- expect(options[:author][:email]).to eq(user.email)
- expect(options[:author][:name]).to eq(user.name)
- end
- end
- end
-
describe "search_files_by_content" do
let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
@@ -1007,7 +1029,7 @@ describe Repository do
it 'runs without errors' do
# old_rev is an ancestor of new_rev
- expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
+ expect(repository.merge_base(old_rev, new_rev)).to eq(old_rev)
# old_rev is not a direct ancestor (parent) of new_rev
expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)
@@ -1029,7 +1051,7 @@ describe Repository do
it 'raises an exception' do
# The 'master' branch is NOT an ancestor of new_rev.
- expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
+ expect(repository.merge_base(old_rev, new_rev)).not_to eq(old_rev)
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
@@ -1058,16 +1080,16 @@ describe Repository do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
end
- it 'expires branch cache' do
- expect(repository).not_to receive(:expire_exists_cache)
- expect(repository).not_to receive(:expire_root_ref_cache)
- expect(repository).not_to receive(:expire_emptiness_caches)
- expect(repository).to receive(:expire_branches_cache)
-
- repository.with_branch(user, 'new-feature') do
+ subject do
+ Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('new-feature') do
new_rev
end
end
+
+ it 'returns branch_created as true' do
+ expect(subject).not_to be_repo_created
+ expect(subject).to be_branch_created
+ end
end
context 'when repository is empty' do
@@ -1157,6 +1179,15 @@ describe Repository do
end
end
+ describe '#tag_exists?' do
+ it 'uses tag_names' do
+ allow(repository).to receive(:tag_names).and_return(['foobar'])
+
+ expect(repository.tag_exists?('foobar')).to eq(true)
+ expect(repository.tag_exists?('master')).to eq(false)
+ end
+ end
+
describe '#branch_names', :use_clean_rails_memory_store_caching do
let(:fake_branch_names) { ['foobar'] }
@@ -2152,6 +2183,15 @@ describe Repository do
end
end
+ describe '#diverging_commit_counts' do
+ it 'returns the commit counts behind and ahead of default branch' do
+ result = repository.diverging_commit_counts(
+ repository.find_branch('fix'))
+
+ expect(result).to eq(behind: 29, ahead: 2)
+ end
+ end
+
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
@@ -2343,4 +2383,111 @@ describe Repository do
end
end
end
+
+ describe '#contributors' do
+ let(:author_a) { build(:author, email: 'tiagonbotelho@hotmail.com', name: 'tiagonbotelho') }
+ let(:author_b) { build(:author, email: 'gitlab@winniehell.de', name: 'Winnie') }
+ let(:author_c) { build(:author, email: 'douwe@gitlab.com', name: 'Douwe Maan') }
+ let(:stubbed_commits) do
+ [build(:commit, author: author_a),
+ build(:commit, author: author_a),
+ build(:commit, author: author_b),
+ build(:commit, author: author_c),
+ build(:commit, author: author_c),
+ build(:commit, author: author_c)]
+ end
+ let(:order_by) { nil }
+ let(:sort) { nil }
+
+ before do
+ allow(repository).to receive(:commits).with(nil, limit: 2000, offset: 0, skip_merges: true).and_return(stubbed_commits)
+ end
+
+ subject { repository.contributors(order_by: order_by, sort: sort) }
+
+ def expect_contributors(*contributors)
+ expect(subject.map(&:email)).to eq(contributors.map(&:email))
+ end
+
+ it 'returns the array of Gitlab::Contributor for the repository' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+
+ context 'order_by email' do
+ let(:order_by) { 'email' }
+
+ context 'asc' do
+ let(:sort) { 'asc' }
+
+ it 'returns all the contributors ordered by email asc case insensitive' do
+ expect_contributors(author_c, author_b, author_a)
+ end
+ end
+
+ context 'desc' do
+ let(:sort) { 'desc' }
+
+ it 'returns all the contributors ordered by email desc case insensitive' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+ end
+ end
+
+ context 'order_by name' do
+ let(:order_by) { 'name' }
+
+ context 'asc' do
+ let(:sort) { 'asc' }
+
+ it 'returns all the contributors ordered by name asc case insensitive' do
+ expect_contributors(author_c, author_a, author_b)
+ end
+ end
+
+ context 'desc' do
+ let(:sort) { 'desc' }
+
+ it 'returns all the contributors ordered by name desc case insensitive' do
+ expect_contributors(author_b, author_a, author_c)
+ end
+ end
+ end
+
+ context 'order_by commits' do
+ let(:order_by) { 'commits' }
+
+ context 'asc' do
+ let(:sort) { 'asc' }
+
+ it 'returns all the contributors ordered by commits asc' do
+ expect_contributors(author_b, author_a, author_c)
+ end
+ end
+
+ context 'desc' do
+ let(:sort) { 'desc' }
+
+ it 'returns all the contributors ordered by commits desc' do
+ expect_contributors(author_c, author_a, author_b)
+ end
+ end
+ end
+
+ context 'invalid ordering' do
+ let(:order_by) { 'unknown' }
+
+ it 'returns the contributors unsorted' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+ end
+
+ context 'invalid sorting' do
+ let(:order_by) { 'name' }
+ let(:sort) { 'unknown' }
+
+ it 'returns the contributors unsorted' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index fece370c03f..ddad6862a63 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -87,6 +87,7 @@ describe Route do
end
context 'when conflicting redirects exist' do
+ let(:route) { create(:project).route }
let!(:conflicting_redirect1) { route.create_redirect('bar/test') }
let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') }
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
@@ -141,11 +142,50 @@ describe Route do
expect(redirect_route.source).to eq(route.source)
expect(redirect_route.path).to eq('foo')
end
+
+ context 'when the source is a Project' do
+ it 'creates a temporal RedirectRoute' do
+ project = create(:project)
+ route = project.route
+ redirect_route = route.create_redirect('foo')
+ expect(redirect_route.permanent?).to be_falsy
+ end
+ end
+
+ context 'when the source is not a project' do
+ it 'creates a permanent RedirectRoute' do
+ redirect_route = route.create_redirect('foo', permanent: true)
+ expect(redirect_route.permanent?).to be_truthy
+ end
+ end
end
describe '#delete_conflicting_redirects' do
+ context 'with permanent redirect' do
+ it 'does not delete the redirect' do
+ route.create_redirect("#{route.path}/foo", permanent: true)
+
+ expect do
+ route.delete_conflicting_redirects
+ end.not_to change { RedirectRoute.count }
+ end
+ end
+
+ context 'with temporal redirect' do
+ let(:route) { create(:project).route }
+
+ it 'deletes the redirect' do
+ route.create_redirect("#{route.path}/foo")
+
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-1)
+ end
+ end
+
context 'when a redirect route with the same path exists' do
context 'when the redirect route has matching case' do
+ let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path) }
it 'deletes the redirect' do
@@ -169,6 +209,7 @@ describe Route do
end
context 'when the redirect route is differently cased' do
+ let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path.upcase) }
it 'deletes the redirect' do
@@ -185,7 +226,32 @@ describe Route do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
end
+ context 'with permanent redirects' do
+ it 'does not return anything' do
+ route.create_redirect("#{route.path}/foo", permanent: true)
+ route.create_redirect("#{route.path}/foo/bar", permanent: true)
+ route.create_redirect("#{route.path}/baz/quz", permanent: true)
+
+ expect(route.conflicting_redirects).to be_empty
+ end
+ end
+
+ context 'with temporal redirects' do
+ let(:route) { create(:project).route }
+
+ it 'returns the redirect routes' do
+ route = create(:project).route
+ redirect1 = route.create_redirect("#{route.path}/foo")
+ redirect2 = route.create_redirect("#{route.path}/foo/bar")
+ redirect3 = route.create_redirect("#{route.path}/baz/quz")
+
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
+ end
+ end
+
context 'when a redirect route with the same path exists' do
+ let(:route) { create(:project).route }
+
context 'when the redirect route has matching case' do
let!(:redirect1) { route.create_redirect(route.path) }
@@ -214,4 +280,42 @@ describe Route do
end
end
end
+
+ describe "#conflicting_redirect_exists?" do
+ context 'when a conflicting redirect exists' do
+ let(:group1) { create(:group, path: 'foo') }
+ let(:group2) { create(:group, path: 'baz') }
+
+ it 'should not be saved' do
+ group1.path = 'bar'
+ group1.save
+
+ group2.path = 'foo'
+
+ expect(group2.save).to be_falsy
+ end
+
+ it 'should return an error on path' do
+ group1.path = 'bar'
+ group1.save
+
+ group2.path = 'foo'
+ group2.valid?
+ expect(group2.errors["route.path"].first).to eq('foo has been taken before. Please use another one')
+ end
+ end
+
+ context 'when a conflicting redirect does not exist' do
+ let(:project1) { create(:project, path: 'foo') }
+ let(:project2) { create(:project, path: 'baz') }
+
+ it 'should be saved' do
+ project1.path = 'bar'
+ project1.save
+
+ project2.path = 'foo'
+ expect(project2.save).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 0f2f906c667..ab6678cab38 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -254,4 +254,30 @@ describe Service do
end
end
end
+
+ describe "#deprecated?" do
+ let(:project) { create(:project, :repository) }
+
+ it 'should return false by default' do
+ service = create(:service, project: project)
+ expect(service.deprecated?).to be_falsy
+ end
+ end
+
+ describe "#deprecation_message" do
+ let(:project) { create(:project, :repository) }
+
+ it 'should be empty by default' do
+ service = create(:service, project: project)
+ expect(service.deprecation_message).to be_nil
+ end
+ end
+
+ describe '.find_by_template' do
+ let!(:kubernetes_service) { create(:kubernetes_service, template: true) }
+
+ it 'returns service template' do
+ expect(KubernetesService.find_by_template).to eq(kubernetes_service)
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 03c96a8f5aa..8d0eaf565a7 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -12,6 +12,7 @@ describe User do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
+ it { is_expected.to include_module(BlocksJsonSerialization) }
end
describe 'delegations' do
@@ -21,7 +22,9 @@ describe User do
describe 'associations' do
it { is_expected.to have_one(:namespace) }
it { is_expected.to have_many(:snippets).dependent(:destroy) }
- it { is_expected.to have_many(:project_members).dependent(:destroy) }
+ it { is_expected.to have_many(:members) }
+ it { is_expected.to have_many(:project_members) }
+ it { is_expected.to have_many(:group_members) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
@@ -133,6 +136,16 @@ describe User do
end
end
+ it 'has a DB-level NOT NULL constraint on projects_limit' do
+ user = create(:user)
+
+ expect(user.persisted?).to eq(true)
+
+ expect do
+ user.update_columns(projects_limit: nil)
+ end.to raise_error(ActiveRecord::StatementInvalid)
+ end
+
it { is_expected.to validate_presence_of(:projects_limit) }
it { is_expected.to validate_numericality_of(:projects_limit) }
it { is_expected.to allow_value(0).for(:projects_limit) }
@@ -759,7 +772,7 @@ describe User do
before do
# add user to project
- project.team << [user, :master]
+ project.add_master(user)
# create invite to projet
create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com')
@@ -804,6 +817,13 @@ describe User do
expect(user.can_create_group).to be_falsey
expect(user.theme_id).to eq(1)
end
+
+ it 'does not undo projects_limit setting if it matches old DB default of 10' do
+ # If the real default project limit is 10 then this test is worthless
+ expect(Gitlab.config.gitlab.default_projects_limit).not_to eq(10)
+ user = described_class.new(projects_limit: 10)
+ expect(user.projects_limit).to eq(10)
+ end
end
context 'when current_application_settings.user_default_external is true' do
@@ -913,11 +933,11 @@ describe User do
describe 'email matching' do
it 'returns users with a matching Email' do
- expect(described_class.search(user.email)).to eq([user, user2])
+ expect(described_class.search(user.email)).to eq([user])
end
- it 'returns users with a partially matching Email' do
- expect(described_class.search(user.email[0..2])).to eq([user, user2])
+ it 'does not return users with a partially matching Email' do
+ expect(described_class.search(user.email[0..2])).not_to include(user, user2)
end
it 'returns users with a matching Email regardless of the casing' do
@@ -973,8 +993,8 @@ describe User do
expect(search_with_secondary_emails(user.email)).to eq([user])
end
- it 'returns users with a partially matching email' do
- expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
+ it 'does not return users with a partially matching email' do
+ expect(search_with_secondary_emails(user.email[0..2])).not_to include([user])
end
it 'returns users with a matching email regardless of the casing' do
@@ -997,29 +1017,8 @@ describe User do
expect(search_with_secondary_emails(email.email)).to eq([email.user])
end
- it 'returns users with a matching part of secondary email' do
- expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
- end
-
- it 'return users with a matching part of secondary email regardless of case' do
- expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
- expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
- expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
- end
-
- it 'returns multiple users with matching secondary emails' do
- email1 = create(:email, email: '1_testemail@example.com')
- email2 = create(:email, email: '2_testemail@example.com')
- email3 = create(:email, email: 'other@email.com')
- email3.user.update_attributes!(email: 'another@mail.com')
-
- expect(
- search_with_secondary_emails('testemail@example.com').map(&:id)
- ).to include(email1.user.id, email2.user.id)
-
- expect(
- search_with_secondary_emails('testemail@example.com').map(&:id)
- ).not_to include(email3.user.id)
+ it 'does not return users with a matching part of secondary email' do
+ expect(search_with_secondary_emails(email.email[1..4])).not_to include([email.user])
end
end
@@ -1468,8 +1467,8 @@ describe User do
let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
- project1.team << [subject, :master]
- project2.team << [subject, :master]
+ project1.add_master(subject)
+ project2.add_master(subject)
end
it "includes IDs for projects the user has pushed to" do
@@ -1568,7 +1567,7 @@ describe User do
user = create(:user)
project = create(:project, :private)
- project.team << [user, Gitlab::Access::MASTER]
+ project.add_master(user)
expect(user.authorized_projects(Gitlab::Access::REPORTER))
.to contain_exactly(project)
@@ -1587,7 +1586,7 @@ describe User do
user2 = create(:user)
project = create(:project, :private, namespace: user1.namespace)
- project.team << [user2, Gitlab::Access::DEVELOPER]
+ project.add_developer(user2)
expect(user2.authorized_projects).to include(project)
end
@@ -1632,7 +1631,7 @@ describe User do
user2 = create(:user)
project = create(:project, :private, namespace: user1.namespace)
- project.team << [user2, Gitlab::Access::DEVELOPER]
+ project.add_developer(user2)
expect(user2.authorized_projects).to include(project)
@@ -1722,7 +1721,7 @@ describe User do
shared_examples :member do
context 'when the user is a master' do
before do
- add_user(Gitlab::Access::MASTER)
+ add_user(:master)
end
it 'loads' do
@@ -1732,7 +1731,7 @@ describe User do
context 'when the user is a developer' do
before do
- add_user(Gitlab::Access::DEVELOPER)
+ add_user(:developer)
end
it 'does not load' do
@@ -1756,7 +1755,7 @@ describe User do
let(:project) { create(:project) }
def add_user(access)
- project.team << [user, access]
+ project.add_role(user, access)
end
it_behaves_like :member
@@ -1769,8 +1768,8 @@ describe User do
let(:user) { create(:user) }
before do
- project1.team << [user, :reporter]
- project2.team << [user, :guest]
+ project1.add_reporter(user)
+ project2.add_guest(user)
end
it 'returns the projects when using a single project ID' do
@@ -1912,8 +1911,8 @@ describe User do
let(:user) { create(:user) }
before do
- project1.team << [user, :reporter]
- project2.team << [user, :guest]
+ project1.add_reporter(user)
+ project2.add_guest(user)
user.project_authorizations.delete_all
user.refresh_authorized_projects
@@ -2592,4 +2591,28 @@ describe User do
include_examples 'max member access for groups'
end
end
+
+ describe "#username_previously_taken?" do
+ let(:user1) { create(:user, username: 'foo') }
+
+ context 'when the username has been taken before' do
+ before do
+ user1.username = 'bar'
+ user1.save!
+ end
+
+ it 'should raise an ActiveRecord::RecordInvalid exception' do
+ user2 = build(:user, username: 'foo')
+ expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Path foo has been taken before/)
+ end
+ end
+
+ context 'when the username has not been taken before' do
+ it 'should be valid' do
+ expect(RedirectRoute.count).to eq(0)
+ user2 = build(:user, username: 'baz')
+ expect(user2).to be_valid
+ end
+ end
+ end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 298a9d16425..41cf2ef7225 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -57,7 +57,7 @@ describe Ci::BuildPolicy do
context 'team member is a guest' do
before do
- project.team << [user, :guest]
+ project.add_guest(user)
end
context 'when public builds are enabled' do
@@ -77,7 +77,7 @@ describe Ci::BuildPolicy do
context 'team member is a reporter' do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
context 'when public builds are enabled' do
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
new file mode 100644
index 00000000000..1b0e9fac355
--- /dev/null
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe Ci::PipelineSchedulePolicy, :models do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :repository) }
+ set(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
+
+ let(:policy) do
+ described_class.new(user, pipeline_schedule)
+ end
+
+ describe 'rules' do
+ describe 'rules for protected ref' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when no one can push or merge to the branch' do
+ before do
+ create(:protected_branch, :no_one_can_push,
+ name: pipeline_schedule.ref, project: project)
+ end
+
+ it 'does not include ability to play pipeline schedule' do
+ expect(policy).to be_disallowed :play_pipeline_schedule
+ end
+ end
+
+ context 'when developers can push to the branch' do
+ before do
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline_schedule.ref, project: project)
+ end
+
+ it 'includes ability to update pipeline' do
+ expect(policy).to be_allowed :play_pipeline_schedule
+ end
+ end
+
+ context 'when no one can create the tag' do
+ let(:tag) { 'v1.0.0' }
+
+ before do
+ pipeline_schedule.update(ref: tag)
+
+ create(:protected_tag, :no_one_can_create,
+ name: pipeline_schedule.ref, project: project)
+ end
+
+ it 'does not include ability to play pipeline schedule' do
+ expect(policy).to be_disallowed :play_pipeline_schedule
+ end
+ end
+
+ context 'when no one can create the tag but it is not a tag' do
+ before do
+ create(:protected_tag, :no_one_can_create,
+ name: pipeline_schedule.ref, project: project)
+ end
+
+ it 'includes ability to play pipeline schedule' do
+ expect(policy).to be_allowed :play_pipeline_schedule
+ end
+ end
+ end
+
+ describe 'rules for owner of schedule' do
+ before do
+ project.add_developer(user)
+ pipeline_schedule.update(owner: user)
+ end
+
+ it 'includes abilities to do do all operations on pipeline schedule' do
+ expect(policy).to be_allowed :play_pipeline_schedule
+ expect(policy).to be_allowed :update_pipeline_schedule
+ expect(policy).to be_allowed :admin_pipeline_schedule
+ end
+ end
+
+ describe 'rules for a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it 'includes abilities to do do all operations on pipeline schedule' do
+ expect(policy).to be_allowed :play_pipeline_schedule
+ expect(policy).to be_allowed :update_pipeline_schedule
+ expect(policy).to be_allowed :admin_pipeline_schedule
+ end
+ end
+ end
+end
diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb
index be40dbb2aa9..14630748c90 100644
--- a/spec/policies/ci/trigger_policy_spec.rb
+++ b/spec/policies/ci/trigger_policy_spec.rb
@@ -45,7 +45,7 @@ describe Ci::TriggerPolicy do
context 'when user is master of the project' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it_behaves_like 'allows to admin and manage trigger'
@@ -53,7 +53,7 @@ describe Ci::TriggerPolicy do
context 'when user is developer of the project' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it_behaves_like 'disallows to admin and manage trigger'
@@ -69,7 +69,7 @@ describe Ci::TriggerPolicy do
context 'when user is master of the project' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it_behaves_like 'allows to admin and manage trigger'
@@ -81,7 +81,7 @@ describe Ci::TriggerPolicy do
context 'when user is master of the project' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it_behaves_like 'allows to manage trigger'
@@ -89,7 +89,7 @@ describe Ci::TriggerPolicy do
context 'when user is developer of the project' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it_behaves_like 'disallows to admin and manage trigger'
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index be4c24c727c..a4af9361ea6 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -19,10 +19,10 @@ describe IssuePolicy do
let(:issue_no_assignee) { create(:issue, project: project) }
before do
- project.team << [guest, :guest]
- project.team << [author, :guest]
- project.team << [assignee, :guest]
- project.team << [reporter, :reporter]
+ project.add_guest(guest)
+ project.add_guest(author)
+ project.add_guest(assignee)
+ project.add_reporter(reporter)
group.add_reporter(reporter_from_group_link)
@@ -114,8 +114,8 @@ describe IssuePolicy do
let(:issue_no_assignee) { create(:issue, project: project) }
before do
- project.team << [guest, :guest]
- project.team << [reporter, :reporter]
+ project.add_guest(guest)
+ project.add_reporter(reporter)
group.add_reporter(reporter_from_group_link)
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index f0bf46c480a..cdba1b09fc1 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -87,7 +87,7 @@ describe ProjectSnippetPolicy do
subject { abilities(external_user, :internal) }
before do
- project.team << [external_user, :developer]
+ project.add_developer(external_user)
end
it do
@@ -131,7 +131,7 @@ describe ProjectSnippetPolicy do
subject { abilities(regular_user, :private) }
before do
- project.team << [regular_user, :developer]
+ project.add_developer(regular_user)
end
it do
@@ -144,7 +144,7 @@ describe ProjectSnippetPolicy do
subject { abilities(external_user, :private) }
before do
- project.team << [external_user, :developer]
+ project.add_developer(external_user)
end
it do
diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb
new file mode 100644
index 00000000000..c00e41725d9
--- /dev/null
+++ b/spec/presenters/group_member_presenter_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe GroupMemberPresenter do
+ let(:user) { double(:user) }
+ let(:group) { double(:group) }
+ let(:group_member) { double(:group_member, source: group) }
+ let(:presenter) { described_class.new(group_member, current_user: user) }
+
+ describe '#can_resend_invite?' do
+ context 'when group_member is invited' do
+ before do
+ expect(group_member).to receive(:invite?).and_return(true)
+ end
+
+ context 'and user can admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(true) }
+ end
+
+ context 'and user cannot admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+
+ context 'when group_member is not invited' do
+ before do
+ expect(group_member).to receive(:invite?).and_return(false)
+ end
+
+ context 'and user can admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+
+ context 'and user cannot admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+ end
+
+ describe '#can_update?' do
+ context 'when user can update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_update?).to eq(true) }
+ end
+
+ context 'when user cannot update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_update?).to eq(false) }
+ end
+ end
+
+ describe '#can_remove?' do
+ context 'when user can destroy_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_remove?).to eq(true) }
+ end
+
+ context 'when user cannot destroy_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_remove?).to eq(false) }
+ end
+ end
+
+ describe '#can_approve?' do
+ context 'when group_member has request an invite' do
+ before do
+ expect(group_member).to receive(:request?).and_return(true)
+ end
+
+ context 'when user can update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(true) }
+ end
+
+ context 'when user cannot update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+
+ context 'when group_member did not request an invite' do
+ before do
+ expect(group_member).to receive(:request?).and_return(false)
+ end
+
+ context 'when user can update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+
+ context 'when user cannot update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index f325d1776e4..e3b37739e8e 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -116,7 +116,7 @@ describe MergeRequestPresenter do
end
before do
- project.team << [user, :developer]
+ project.add_developer(user)
allow(resource.project).to receive(:default_branch)
.and_return(resource.target_branch)
@@ -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.team << [user, :master]
+ project.add_master(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.team << [user, :master]
+ project.add_master(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.team << [user, :master]
+ project.add_master(user)
is_expected
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip")
@@ -404,4 +404,67 @@ describe MergeRequestPresenter do
.to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>")
end
end
+
+ describe '#rebase_path' do
+ before do
+ allow(resource).to receive(:rebase_in_progress?) { rebase_in_progress }
+ allow(resource).to receive(:should_be_rebased?) { should_be_rebased }
+
+ allow_any_instance_of(Gitlab::UserAccess::RequestCacheExtension)
+ .to receive(:can_push_to_branch?)
+ .with(resource.source_branch)
+ .and_return(can_push_to_branch)
+ end
+
+ subject do
+ described_class.new(resource, current_user: user).rebase_path
+ end
+
+ context 'when can rebase' do
+ let(:rebase_in_progress) { false }
+ let(:can_push_to_branch) { true }
+ let(:should_be_rebased) { true }
+
+ before do
+ allow(resource).to receive(:source_branch_exists?) { true }
+ end
+
+ it 'returns path' do
+ is_expected
+ .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase")
+ end
+ end
+
+ context 'when cannot rebase' do
+ context 'when rebase in progress' do
+ let(:rebase_in_progress) { true }
+ let(:can_push_to_branch) { true }
+ let(:should_be_rebased) { true }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when user cannot merge' do
+ let(:rebase_in_progress) { false }
+ let(:can_push_to_branch) { false }
+ let(:should_be_rebased) { true }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'should not be rebased' do
+ let(:rebase_in_progress) { false }
+ let(:can_push_to_branch) { true }
+ let(:should_be_rebased) { false }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
end
diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb
new file mode 100644
index 00000000000..83db5c56cdf
--- /dev/null
+++ b/spec/presenters/project_member_presenter_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe ProjectMemberPresenter do
+ let(:user) { double(:user) }
+ let(:project) { double(:project) }
+ let(:project_member) { double(:project_member, source: project) }
+ let(:presenter) { described_class.new(project_member, current_user: user) }
+
+ describe '#can_resend_invite?' do
+ context 'when project_member is invited' do
+ before do
+ expect(project_member).to receive(:invite?).and_return(true)
+ end
+
+ context 'and user can admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(true) }
+ end
+
+ context 'and user cannot admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+
+ context 'when project_member is not invited' do
+ before do
+ expect(project_member).to receive(:invite?).and_return(false)
+ end
+
+ context 'and user can admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+
+ context 'and user cannot admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+ end
+
+ describe '#can_update?' do
+ context 'when user can update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_update?).to eq(true) }
+ end
+
+ context 'when user cannot update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_update?).to eq(false) }
+ end
+ end
+
+ describe '#can_remove?' do
+ context 'when user can destroy_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_remove?).to eq(true) }
+ end
+
+ context 'when user cannot destroy_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_remove?).to eq(false) }
+ end
+ end
+
+ describe '#can_approve?' do
+ context 'when project_member has request an invite' do
+ before do
+ expect(project_member).to receive(:request?).and_return(true)
+ end
+
+ context 'and user can update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(true) }
+ end
+
+ context 'and user cannot update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+
+ context 'when project_member did not request an invite' do
+ before do
+ expect(project_member).to receive(:request?).and_return(false)
+ end
+
+ context 'and user can update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+
+ context 'and user cannot update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 35ca3635a9d..24389f28b21 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -8,8 +8,8 @@ describe API::AccessRequests do
set(:project) do
create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
- project.team << [developer, :developer]
- project.team << [master, :master]
+ project.add_developer(developer)
+ project.add_master(master)
project.request_access(access_requester)
end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index eaf12f71421..5adfb33677f 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.team << [user, :master]
+ project.add_master(user)
end
describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 546a1697e56..c6c10025f7f 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -6,18 +6,18 @@ describe API::Boards do
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
- set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
set(:dev_label) do
- create(:label, title: 'Development', color: '#FFAABB', project: project)
+ create(:label, title: 'Development', color: '#FFAABB', project: board_parent)
end
set(:test_label) do
- create(:label, title: 'Testing', color: '#FFAACC', project: project)
+ create(:label, title: 'Testing', color: '#FFAACC', project: board_parent)
end
set(:ux_label) do
- create(:label, title: 'UX', color: '#FF0000', project: project)
+ create(:label, title: 'UX', color: '#FF0000', project: board_parent)
end
set(:dev_list) do
@@ -28,180 +28,25 @@ describe API::Boards 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.team << [user, :reporter]
- project.team << [guest, :guest]
- end
+ set(:milestone) { create(:milestone, project: board_parent) }
+ set(:board_label) { create(:label, project: board_parent) }
+ set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) }
- describe "GET /projects/:id/boards" do
- let(:base_url) { "/projects/#{project.id}/boards" }
+ it_behaves_like 'group and project boards', "/projects/:id/boards"
- context "when unauthenticated" do
- it "returns authentication error" do
- get 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 api(base_url, 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['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 api(base_url, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
- expect(json_response.first['label']['name']).to eq(dev_label.title)
- end
-
- it 'returns 404 if board not found' do
- get api("/projects/#{project.id}/boards/22343/lists", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it 'returns a list' do
- get api("#{base_url}/#{dev_list.id}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['id']).to eq(dev_list.id)
- expect(json_response['label']['name']).to eq(dev_label.title)
- expect(json_response['position']).to eq(1)
- end
-
- it 'returns 404 if list not found' do
- get api("#{base_url}/5324", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "POST /projects/:id/board/lists" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+ describe "POST /projects/:id/boards/lists" do
+ let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" }
it 'creates a new issue board list for group labels' do
group = create(:group)
group_label = create(:group_label, group: group)
- project.update(group: group)
+ board_parent.update(group: group)
- post api(base_url, user), label_id: group_label.id
+ post api(url, user), label_id: group_label.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(group_label.title)
expect(json_response['position']).to eq(3)
end
-
- it 'creates a new issue board list for project labels' do
- post api(base_url, user), label_id: ux_label.id
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['label']['name']).to eq(ux_label.title)
- expect(json_response['position']).to eq(3)
- end
-
- it 'returns 400 when creating a new list if label_id is invalid' do
- post api(base_url, user), label_id: 23423
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- it 'returns 403 for project members with guest role' do
- put api("#{base_url}/#{test_list.id}", guest), position: 1
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it "updates a list" do
- put api("#{base_url}/#{test_list.id}", user),
- position: 1
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['position']).to eq(1)
- end
-
- it "returns 404 error if list id not found" do
- put api("#{base_url}/44444", user),
- position: 1
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 403 for project members with guest role" do
- put api("#{base_url}/#{test_list.id}", guest),
- position: 1
-
- expect(response).to have_gitlab_http_status(403)
- 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 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 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 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 api("#{base_url}/#{dev_list.id}", owner)
-
- expect(response).to have_gitlab_http_status(204)
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("#{base_url}/#{dev_list.id}", owner) }
- end
- end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index c7977e624ff..6732c99e329 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -7,7 +7,7 @@ describe API::Deployments do
let!(:deployment) { create(:deployment) }
before do
- project.team << [user, :master]
+ project.add_master(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 3665cfd7241..53d48a91007 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.team << [user, :master]
+ project.add_master(user)
end
describe 'GET /projects/:id/environments' do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 5d8338a3fb7..d8fdfd6dee1 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -14,7 +14,7 @@ describe API::Files do
let(:author_name) { 'John Doe' }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
def route(file_path = nil)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 554723d6b1e..3c0b4728dc2 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -173,6 +173,28 @@ describe API::Groups do
end
describe "GET /groups/:id" do
+ # Given a group, create one project for each visibility level
+ #
+ # group - Group to add projects to
+ # share_with - If provided, each project will be shared with this Group
+ #
+ # Returns a Hash of visibility_level => Project pairs
+ def add_projects_to_group(group, share_with: nil)
+ projects = {
+ public: create(:project, :public, namespace: group),
+ internal: create(:project, :internal, namespace: group),
+ private: create(:project, :private, namespace: group)
+ }
+
+ if share_with
+ create(:project_group_link, project: projects[:public], group: share_with)
+ create(:project_group_link, project: projects[:internal], group: share_with)
+ create(:project_group_link, project: projects[:private], group: share_with)
+ end
+
+ projects
+ end
+
context 'when unauthenticated' do
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}")
@@ -183,6 +205,26 @@ describe API::Groups do
get api("/groups/#{group1.id}")
expect(response).to have_gitlab_http_status(200)
end
+
+ it 'returns only public projects in the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group)
+
+ get api("/groups/#{public_group.id}")
+
+ expect(json_response['projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id)
+ end
+
+ it 'returns only public projects shared with the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group, share_with: group1)
+
+ get api("/groups/#{group1.id}")
+
+ expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id)
+ end
end
context "when authenticated as user" do
@@ -222,6 +264,26 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404)
end
+
+ it 'returns only public and internal projects in the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group)
+
+ get api("/groups/#{public_group.id}", user2)
+
+ expect(json_response['projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id, projects[:internal].id)
+ end
+
+ it 'returns only public and internal projects shared with the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group, share_with: group1)
+
+ get api("/groups/#{group1.id}", user2)
+
+ expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id, projects[:internal].id)
+ end
end
context "when authenticated as admin" do
@@ -355,7 +417,7 @@ describe API::Groups do
end
it "only returns projects to which user has access" do
- project3.team << [user3, :developer]
+ project3.add_developer(user3)
get api("/groups/#{group1.id}/projects", user3)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 67e1539cbc3..7b25047ea8f 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -147,7 +147,7 @@ describe API::Internal do
describe "POST /internal/lfs_authenticate" do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'user key' do
@@ -199,7 +199,7 @@ describe API::Internal do
end
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'with env passed as a JSON' do
@@ -359,7 +359,7 @@ describe API::Internal do
context "access denied" do
before do
- project.team << [user, :guest]
+ project.add_guest(user)
end
context "git pull" do
@@ -413,7 +413,7 @@ describe API::Internal do
context "archived project" do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
project.archive!
end
@@ -527,7 +527,7 @@ describe API::Internal do
context 'web actions are always allowed' do
it 'allows WEB push' do
stub_application_setting(enabled_git_access_protocol: 'ssh')
- project.team << [user, :developer]
+ project.add_developer(user)
push(key, project, 'web')
expect(response.status).to eq(200)
@@ -537,37 +537,26 @@ describe API::Internal do
context 'the project path was changed' do
let!(:old_path_to_repo) { project.repository.path_to_repo }
- let!(:old_full_path) { project.full_path }
- let(:project_moved_message) do
- <<-MSG.strip_heredoc
- Project '#{old_full_path}' was moved to '#{project.full_path}'.
-
- Please update your Git remote and try again:
-
- git remote set-url origin #{project.ssh_url_to_repo}
- MSG
- end
+ let!(:repository) { project.repository }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
project.path = 'new_path'
project.save!
end
it 'rejects the push' do
- push_with_path(key, old_path_to_repo)
+ push(key, project)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to be_falsey
- expect(json_response['message']).to eq(project_moved_message)
+ expect(json_response['status']).to be_falsy
end
it 'rejects the SSH pull' do
- pull_with_path(key, old_path_to_repo)
+ pull(key, project)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to be_falsey
- expect(json_response['message']).to eq(project_moved_message)
+ expect(json_response['status']).to be_falsy
end
end
end
@@ -577,7 +566,7 @@ describe API::Internal do
let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'returns link to create new merge request' do
@@ -695,7 +684,7 @@ describe API::Internal do
# end
# end
- describe 'POST /internal/post_receive' do
+ describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
let(:identifier) { 'key-123' }
let(:valid_params) do
@@ -712,7 +701,9 @@ describe API::Internal do
end
before do
- project.team << [user, :developer]
+ project.add_developer(user)
+ allow(described_class).to receive(:identify).and_return(user)
+ allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user)
end
it 'enqueues a PostReceive worker job' do
@@ -780,6 +771,29 @@ describe API::Internal do
expect(json_response['broadcast_message']).to eq(nil)
end
end
+
+ context 'with a redirected data' do
+ it 'returns redirected message on the response' do
+ project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http')
+ project_moved.add_redirect_message
+
+ post api("/internal/post_receive"), valid_params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response["redirected_message"]).to be_present
+ expect(json_response["redirected_message"]).to eq(project_moved.redirect_message)
+ end
+ end
+
+ context 'with an orphaned write deploy key' do
+ it 'does not try to notify that project moved' do
+ allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(nil)
+
+ post api("/internal/post_receive"), valid_params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
describe 'POST /internal/pre_receive' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 99525cd0a6a..320217f2032 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -58,8 +58,8 @@ describe API::Issues, :mailer do
let(:no_milestone_title) { URI.escape(Milestone::None.title) }
before(:all) do
- project.team << [user, :reporter]
- project.team << [guest, :guest]
+ project.add_reporter(user)
+ project.add_guest(guest)
end
describe "GET /issues" do
@@ -344,7 +344,7 @@ describe API::Issues, :mailer do
let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
before do
- group_project.team << [user, :reporter]
+ group_project.add_reporter(user)
end
let(:base_url) { "/groups/#{group.id}/issues" }
@@ -860,6 +860,20 @@ describe API::Issues, :mailer do
end
end
+ context 'user does not have permissions to create issue' do
+ let(:not_member) { create(:user) }
+
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'renders 403' do
+ post api("/projects/#{project.id}/issues", not_member), title: 'new issue'
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
@@ -953,7 +967,7 @@ describe API::Issues, :mailer do
let(:project) { merge_request.source_project }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
context 'resolving all discussions in a merge request' do
@@ -1568,4 +1582,16 @@ describe API::Issues, :mailer do
expect(json_response).to be_an Array
expect(json_response.length).to eq(size) if size
end
+
+ describe 'GET projects/:id/issues/:issue_iid/participants' do
+ it_behaves_like 'issuable participants endpoint' do
+ let(:entity) { issue }
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 9ebf5bf7e97..79130f2d2a8 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -11,7 +11,7 @@ describe API::Jobs do
ref: project.default_branch)
end
- let(:job) { create(:ci_build, pipeline: pipeline) }
+ let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
let(:user) { create(:user) }
let(:api_user) { user }
@@ -511,7 +511,7 @@ describe API::Jobs 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')
+ expect(project.builds.first.status).to eq('success')
end
end
@@ -571,7 +571,7 @@ describe API::Jobs do
let(:role) { :master }
before do
- project.team << [user, role]
+ project.add_role(user, role)
post api("/projects/#{project.id}/jobs/#{job.id}/erase", user)
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 3498e5bc8d9..34cbf75f4c1 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.team << [user, :master]
+ project.add_master(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 3349e396ab8..5d4f81e07a6 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -8,8 +8,8 @@ describe API::Members do
let(:project) do
create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
- project.team << [developer, :developer]
- project.team << [master, :master]
+ project.add_developer(developer)
+ project.add_master(master)
project.request_access(access_requester)
end
end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index bf4c8443b23..cb647aee70f 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.team << [user, :master]
+ project.add_master(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 91616da6d9a..0c9fbb1f187 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -25,7 +25,7 @@ describe API::MergeRequests do
let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) }
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
describe 'GET /merge_requests' do
@@ -150,6 +150,26 @@ describe API::MergeRequests do
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request3.id)
end
+
+ context 'search params' do
+ before do
+ merge_request.update(title: 'Search title', description: 'Search description')
+ end
+
+ it 'returns merge requests matching given search string for title' do
+ get api("/merge_requests", user), search: merge_request.title
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns merge requests for project matching given search string for description' do
+ get api("/merge_requests", user), project_id: project.id, search: merge_request.description
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+ end
end
end
@@ -480,6 +500,12 @@ describe API::MergeRequests do
end
end
+ describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do
+ it_behaves_like 'issuable participants endpoint' do
+ let(:entity) { merge_request }
+ end
+ end
+
describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
it 'returns a 200 when merge request is valid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
@@ -710,7 +736,7 @@ describe API::MergeRequests do
let(:developer) { create(:user) }
before do
- project.team << [developer, :developer]
+ project.add_developer(developer)
end
it "denies the deletion of the merge request" do
@@ -788,7 +814,7 @@ describe API::MergeRequests do
it "returns 401 if user has no permissions to merge" do
user2 = create(:user)
- project.team << [user2, :reporter]
+ project.add_reporter(user2)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2)
expect(response).to have_gitlab_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
@@ -977,7 +1003,7 @@ describe API::MergeRequests do
project = create(:project, :private)
merge_request = create(:merge_request, :simple, source_project: project)
guest = create(:user)
- project.team << [guest, :guest]
+ project.add_guest(guest)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest)
@@ -1025,7 +1051,7 @@ describe API::MergeRequests do
it 'returns 403 if user has no access to read code' do
guest = create(:user)
- project.team << [guest, :guest]
+ project.add_guest(guest)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest)
@@ -1061,7 +1087,7 @@ describe API::MergeRequests do
it 'returns 403 if user has no access to read code' do
guest = create(:user)
- project.team << [guest, :guest]
+ project.add_guest(guest)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest)
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 3bfb4c5506f..981c9c27325 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -14,7 +14,7 @@ describe API::Notes do
let(:private_user) { create(:user) }
let(:private_project) do
create(:project, namespace: private_user.namespace)
- .tap { |p| p.team << [private_user, :master] }
+ .tap { |p| p.add_master(private_user) }
end
let(:private_issue) { create(:issue, project: private_project) }
@@ -29,7 +29,7 @@ describe API::Notes do
end
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
describe "GET /projects/:id/noteable/:noteable_id/notes" do
@@ -464,7 +464,7 @@ describe API::Notes do
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)
+ expect(Event).to receive(:create!)
post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!'
end
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index d412b045e9f..5d01dc37f0e 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -46,6 +46,7 @@ describe API::PagesDomains do
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.last).to have_key('domain')
+ expect(json_response.last).to have_key('project_id')
expect(json_response.last).to have_key('certificate_expiration')
expect(json_response.last['certificate_expiration']['expired']).to be true
expect(json_response.first).not_to have_key('certificate_expiration')
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index e4dcc9252fa..0736329f9fd 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.team << [user, :master]
+ project.add_master(user)
end
describe 'GET /projects/:id/pipelines ' do
@@ -424,7 +424,7 @@ describe API::Pipelines do
let!(:reporter) { create(:user) }
before do
- project.team << [reporter, :reporter]
+ project.add_reporter(reporter)
end
it 'rejects the action' do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index f31344a6238..1fd082ecc38 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -13,8 +13,8 @@ describe API::ProjectHooks, 'ProjectHooks' do
end
before do
- project.team << [user, :master]
- project.team << [user3, :developer]
+ project.add_master(user)
+ project.add_developer(user3)
end
describe "GET /projects/:id/hooks" do
@@ -206,7 +206,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.team << [test_user, :master]
+ other_project.add_master(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_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index 72e1574b55f..08ea7314bb3 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -7,7 +7,7 @@ describe API::ProjectMilestones do
let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it_behaves_like 'group and project milestones', "/projects/:id/milestones" do
@@ -16,7 +16,7 @@ describe API::ProjectMilestones do
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)
+ expect(Event).to receive(:create!)
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
state_event: 'close'
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index a41345da05b..de1763015fa 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -908,7 +908,7 @@ describe API::Projects do
describe 'permissions' do
context 'all projects' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'contains permission information' do
@@ -923,7 +923,7 @@ describe API::Projects do
context 'personal project' do
it 'sets project access and returns 200' do
- project.team << [user, :master]
+ project.add_master(user)
get api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(200)
@@ -1539,7 +1539,7 @@ describe API::Projects do
context 'user without archiving rights to the project' do
before do
- project.team << [user3, :developer]
+ project.add_developer(user3)
end
it 'rejects the action' do
@@ -1575,7 +1575,7 @@ describe API::Projects do
context 'user without archiving rights to the project' do
before do
- project.team << [user3, :developer]
+ project.add_developer(user3)
end
it 'rejects the action' do
@@ -1650,7 +1650,7 @@ describe API::Projects do
it 'does not remove a project if not an owner' do
user3 = create(:user)
- project.team << [user3, :developer]
+ project.add_developer(user3)
delete api("/projects/#{project.id}", user3)
expect(response).to have_gitlab_http_status(403)
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 9f2ff3b5af6..741800ff61d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -378,6 +378,28 @@ describe API::Repositories do
expect(first_contributor['additions']).to eq(0)
expect(first_contributor['deletions']).to eq(0)
end
+
+ context 'using sorting' do
+ context 'by commits desc' do
+ it 'returns the repository contribuors sorted by commits desc' do
+ get api(route, current_user), { order_by: 'commits', sort: 'desc' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('contributors')
+ expect(json_response.first['commits']).to be > json_response.last['commits']
+ end
+ end
+
+ context 'by name desc' do
+ it 'returns the repository contribuors sorted by name asc case insensitive' do
+ get api(route, current_user), { order_by: 'name', sort: 'asc' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('contributors')
+ expect(json_response.first['name'].downcase).to be < json_response.last['name'].downcase
+ end
+ end
+ end
end
context 'when unauthenticated', 'and project is public' do
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index ba697e2b305..26d56c04862 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -53,6 +53,10 @@ describe API::Services do
describe "DELETE /projects/:id/services/#{service.dasherize}" do
include_context service
+ before do
+ initialize_service(service)
+ end
+
it "deletes #{service}" do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
@@ -67,9 +71,7 @@ describe API::Services do
# inject some properties into the service
before do
- service_object = project.find_or_initialize_service(service)
- service_object.properties = service_attrs
- service_object.save
+ initialize_service(service)
end
it 'returns authentication error when unauthenticated' do
@@ -92,7 +94,7 @@ describe API::Services do
end
it "returns error when authenticated but not a project owner" do
- project.team << [user2, :developer]
+ project.add_developer(user2)
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
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 0bf7863bdc8..e2b19ad59f9 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -16,6 +16,44 @@ describe API::Tags do
describe 'GET /projects/:id/repository/tags' do
let(:route) { "/projects/#{project_id}/repository/tags" }
+ context 'sorting' do
+ let(:current_user) { user }
+
+ it 'sorts by descending order by default' do
+ get api(route, current_user)
+
+ desc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
+ desc_order_tags.reverse!.map! { |tag| tag.dereferenced_target.id }
+
+ expect(json_response.map { |tag| tag['commit']['id'] }).to eq(desc_order_tags)
+ end
+
+ it 'sorts by ascending order if specified' do
+ get api("#{route}?sort=asc", current_user)
+
+ asc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
+ asc_order_tags.map! { |tag| tag.dereferenced_target.id }
+
+ expect(json_response.map { |tag| tag['commit']['id'] }).to eq(asc_order_tags)
+ end
+
+ it 'sorts by name in descending order when requested' do
+ get api("#{route}?order_by=name", current_user)
+
+ ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort.reverse
+
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
+ end
+
+ it 'sorts by name in ascending order when requested' do
+ get api("#{route}?order_by=name&sort=asc", current_user)
+
+ ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort
+
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
+ end
+ end
+
shared_examples_for 'repository tags' do
it 'returns the repository tags' do
get api(route, current_user)
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index c6063a2e089..fb3a33cadff 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -13,8 +13,8 @@ describe API::Todos do
let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
before do
- project_1.team << [john_doe, :developer]
- project_2.team << [john_doe, :developer]
+ project_1.add_developer(john_doe)
+ project_2.add_developer(john_doe)
end
describe 'GET /todos' do
@@ -191,7 +191,7 @@ describe API::Todos do
it 'returns an error if the issuable is not accessible' do
guest = create(:user)
- project_1.team << [guest, :guest]
+ project_1.add_guest(guest)
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest)
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
index 0cd8b70007f..6dc430676b0 100644
--- a/spec/requests/api/v3/award_emoji_spec.rb
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -9,7 +9,7 @@ describe API::V3::AwardEmoji do
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
set(:note) { create(:note, project: project, noteable: issue) }
- before { project.team << [user, :master] }
+ before { project.add_master(user) }
describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
context 'on an issue' do
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index 14409d25544..dde4f096193 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -27,8 +27,8 @@ describe API::V3::Boards do
end
before do
- project.team << [user, :reporter]
- project.team << [guest, :guest]
+ project.add_reporter(user)
+ project.add_guest(guest)
end
describe "GET /projects/:id/boards" do
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index d31c94ddd2c..8b115e01f47 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -9,11 +9,11 @@ describe API::V3::Commits do
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.team << [user, :reporter] }
+ before { project.add_reporter(user) }
describe "List repository commits" do
context "authorized user" do
- before { project.team << [user2, :reporter] }
+ before { project.add_reporter(user2) }
it "returns project commits" do
commit = project.repository.commit
@@ -415,7 +415,7 @@ describe API::V3::Commits do
describe "Get the diff of a commit" do
context "authorized user" do
- before { project.team << [user2, :reporter] }
+ 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)
@@ -487,7 +487,7 @@ describe API::V3::Commits do
end
it 'returns 400 if you are not allowed to push to the target branch' do
- project.team << [user2, :developer]
+ 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
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
index 90eabda4dac..ac86fbea498 100644
--- a/spec/requests/api/v3/deployments_spec.rb
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -7,7 +7,7 @@ describe API::V3::Deployments do
let!(:deployment) { create(:deployment) }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
shared_examples 'a paginated resources' do
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
index 937250b5219..68be5256b64 100644
--- a/spec/requests/api/v3/environments_spec.rb
+++ b/spec/requests/api/v3/environments_spec.rb
@@ -7,7 +7,7 @@ describe API::V3::Environments do
let!(:environment) { create(:environment, project: project) }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
shared_examples 'a paginated resources' do
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 5500c1cf770..26a3d8870a0 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -27,7 +27,7 @@ describe API::V3::Files do
let(:author_email) { 'user@example.org' }
let(:author_name) { 'John Doe' }
- before { project.team << [user, :developer] }
+ before { project.add_developer(user) }
describe "GET /projects/:id/repository/files" do
let(:route) { "/projects/#{project.id}/repository/files" }
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index 498cb42fad1..a1cdf583de3 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -330,7 +330,7 @@ describe API::V3::Groups do
end
it "only returns projects to which user has access" do
- project3.team << [user3, :developer]
+ project3.add_developer(user3)
get v3_api("/groups/#{group1.id}/projects", user3)
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 39a47a62f16..0dd6d673625 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -50,8 +50,8 @@ describe API::V3::Issues, :mailer do
let(:no_milestone_title) { URI.escape(Milestone::None.title) }
before do
- project.team << [user, :reporter]
- project.team << [guest, :guest]
+ project.add_reporter(user)
+ project.add_guest(guest)
end
describe "GET /issues" do
@@ -278,7 +278,7 @@ describe API::V3::Issues, :mailer do
let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
before do
- group_project.team << [user, :reporter]
+ group_project.add_reporter(user)
end
let(:base_url) { "/groups/#{group.id}/issues" }
@@ -827,7 +827,7 @@ describe API::V3::Issues, :mailer do
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
before do
- project.team << [user, :master]
+ project.add_master(user)
post v3_api("/projects/#{project.id}/issues", user),
title: 'New Issue',
merge_request_for_resolving_discussions: merge_request.iid
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
index 1d31213d5ca..cdab4d2bd73 100644
--- a/spec/requests/api/v3/labels_spec.rb
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -7,7 +7,7 @@ describe API::V3::Labels do
let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
describe 'GET /projects/:id/labels' do
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index 68be3d24c26..b91782ae511 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -8,8 +8,8 @@ describe API::V3::Members do
let(:project) do
create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
- project.team << [developer, :developer]
- project.team << [master, :master]
+ project.add_developer(developer)
+ project.add_master(master)
project.request_access(access_requester)
end
end
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
index e613036a88d..547c066fadc 100644
--- a/spec/requests/api/v3/merge_request_diffs_spec.rb
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -8,7 +8,7 @@ describe API::V3::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.team << [user, :master]
+ project.add_master(user)
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index 2e2b9449429..b8b7d9d1c40 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -14,7 +14,7 @@ describe API::MergeRequests do
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
describe "GET /projects/:id/merge_requests" do
@@ -396,7 +396,7 @@ describe API::MergeRequests do
let(:developer) { create(:user) }
before do
- project.team << [developer, :developer]
+ project.add_developer(developer)
end
it "denies the deletion of the merge request" do
@@ -458,7 +458,7 @@ describe API::MergeRequests do
it "returns 401 if user has no permissions to merge" do
user2 = create(:user)
- project.team << [user2, :reporter]
+ 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')
@@ -645,7 +645,7 @@ describe API::MergeRequests do
project = create(:project, :private, :repository)
merge_request = create(:merge_request, :simple, source_project: project)
guest = create(:user)
- project.team << [guest, :guest]
+ project.add_guest(guest)
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
@@ -675,7 +675,7 @@ describe API::MergeRequests do
it 'returns 403 if user has no access to read code' do
guest = create(:user)
- project.team << [guest, :guest]
+ project.add_guest(guest)
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
@@ -705,7 +705,7 @@ describe API::MergeRequests do
it 'returns 403 if user has no access to read code' do
guest = create(:user)
- project.team << [guest, :guest]
+ project.add_guest(guest)
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb
index e82f35598a6..6021600e09c 100644
--- a/spec/requests/api/v3/milestones_spec.rb
+++ b/spec/requests/api/v3/milestones_spec.rb
@@ -6,7 +6,7 @@ describe API::V3::Milestones do
let!(:closed_milestone) { create(:closed_milestone, project: project) }
let!(:milestone) { create(:milestone, project: project) }
- before { project.team << [user, :developer] }
+ before { project.add_developer(user) }
describe 'GET /projects/:id/milestones' do
it 'returns project milestones' do
@@ -161,7 +161,7 @@ describe API::V3::Milestones do
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)
+ expect(Event).to receive(:create!)
put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
state_event: 'close'
@@ -200,7 +200,7 @@ describe API::V3::Milestones do
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
before do
- public_project.team << [user, :developer]
+ public_project.add_developer(user)
milestone.issues << issue << confidential_issue
end
@@ -215,7 +215,7 @@ describe API::V3::Milestones do
it 'does not return confidential issues to team members with guest role' do
member = create(:user)
- project.team << [member, :guest]
+ project.add_guest(member)
get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
index d3455a4bba4..5532795ab02 100644
--- a/spec/requests/api/v3/notes_spec.rb
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -14,7 +14,7 @@ describe API::V3::Notes do
let(:private_user) { create(:user) }
let(:private_project) do
create(:project, namespace: private_user.namespace)
- .tap { |p| p.team << [private_user, :master] }
+ .tap { |p| p.add_master(private_user) }
end
let(:private_issue) { create(:issue, project: private_project) }
@@ -28,7 +28,7 @@ describe API::V3::Notes do
system: true
end
- before { project.team << [user, :reporter] }
+ before { project.add_reporter(user) }
describe "GET /projects/:id/noteable/:noteable_id/notes" do
context "when noteable is an Issue" do
@@ -302,7 +302,7 @@ describe API::V3::Notes do
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)
+ expect(Event).to receive(:create!)
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
end
diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb
index 1c7d9fe32bb..ea943f22c41 100644
--- a/spec/requests/api/v3/pipelines_spec.rb
+++ b/spec/requests/api/v3/pipelines_spec.rb
@@ -10,7 +10,7 @@ describe API::V3::Pipelines do
ref: project.default_branch)
end
- before { project.team << [user, :master] }
+ before { project.add_master(user) }
shared_examples 'a paginated resources' do
before do
@@ -188,7 +188,7 @@ describe API::V3::Pipelines do
context 'user without proper access rights' do
let!(:reporter) { create(:user) }
- before { project.team << [reporter, :reporter] }
+ before { project.add_reporter(reporter) }
it 'rejects the action' do
post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
index 00f59744a31..248ae97f875 100644
--- a/spec/requests/api/v3/project_hooks_spec.rb
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -13,8 +13,8 @@ describe API::ProjectHooks, 'ProjectHooks' do
end
before do
- project.team << [user, :master]
- project.team << [user3, :developer]
+ project.add_master(user)
+ project.add_developer(user3)
end
describe "GET /projects/:id/hooks" do
@@ -205,7 +205,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.team << [test_user, :master]
+ 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)
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 27288b98d1c..13e465e0b2d 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -753,7 +753,7 @@ describe API::V3::Projects do
describe 'permissions' do
context 'all projects' do
- before { project.team << [user, :master] }
+ before { project.add_master(user) }
it 'contains permission information' do
get v3_api("/projects", user)
@@ -767,7 +767,7 @@ describe API::V3::Projects do
context 'personal project' do
it 'sets project access and returns 200' do
- project.team << [user, :master]
+ project.add_master(user)
get v3_api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(200)
@@ -1362,7 +1362,7 @@ describe API::V3::Projects do
context 'user without archiving rights to the project' do
before do
- project.team << [user3, :developer]
+ project.add_developer(user3)
end
it 'rejects the action' do
@@ -1398,7 +1398,7 @@ describe API::V3::Projects do
context 'user without archiving rights to the project' do
before do
- project.team << [user3, :developer]
+ project.add_developer(user3)
end
it 'rejects the action' do
@@ -1466,7 +1466,7 @@ describe API::V3::Projects do
it 'does not remove a project if not an owner' do
user3 = create(:user)
- project.team << [user3, :developer]
+ project.add_developer(user3)
delete v3_api("/projects/#{project.id}", user3)
expect(response).to have_gitlab_http_status(403)
end
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
index 8f212ab6be6..c69a7d58ca6 100644
--- a/spec/requests/api/v3/services_spec.rb
+++ b/spec/requests/api/v3/services_spec.rb
@@ -10,6 +10,10 @@ describe API::V3::Services do
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)
diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb
index 8f5c3fbf8dd..53fd962272a 100644
--- a/spec/requests/api/v3/todos_spec.rb
+++ b/spec/requests/api/v3/todos_spec.rb
@@ -12,8 +12,8 @@ describe API::V3::Todos do
let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
before do
- project_1.team << [john_doe, :developer]
- project_2.team << [john_doe, :developer]
+ project_1.add_developer(john_doe)
+ project_2.add_developer(john_doe)
end
describe 'DELETE /todos/:id' do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 65bd001e491..fb0806ff9f1 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -12,6 +12,8 @@ require 'spec_helper'
describe API::Wikis do
let(:user) { create(:user) }
+ let(:group) { create(:group).tap { |g| g.add_owner(user) } }
+ let(:project_wiki) { create(:project_wiki, project: project, user: user) }
let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } }
let(:expected_keys_with_content) { %w(content format slug title) }
let(:expected_keys_without_content) { %w(format slug title) }
@@ -19,8 +21,8 @@ describe API::Wikis do
shared_examples_for 'returns list of wiki pages' do
context 'when wiki has pages' do
let!(:pages) do
- [create(:wiki_page, wiki: project.wiki, attrs: { title: 'page1', content: 'content of page1' }),
- create(:wiki_page, wiki: project.wiki, attrs: { title: 'page2', content: 'content of page2' })]
+ [create(:wiki_page, wiki: project_wiki, attrs: { title: 'page1', content: 'content of page1' }),
+ create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2', content: 'content of page2' })]
end
it 'returns the list of wiki pages without content' do
@@ -445,7 +447,7 @@ describe API::Wikis do
end
describe 'PUT /projects/:id/wikis/:slug' do
- let(:page) { create(:wiki_page, wiki: project.wiki) }
+ let(:page) { create(:wiki_page, wiki: project_wiki) }
let(:payload) { { title: 'new title', content: 'new content' } }
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
@@ -568,10 +570,20 @@ describe API::Wikis do
end
end
end
+
+ context 'when wiki belongs to a group project' do
+ let(:project) { create(:project, namespace: group) }
+
+ before do
+ put(api(url, user), payload)
+ end
+
+ include_examples 'updates wiki page'
+ end
end
describe 'DELETE /projects/:id/wikis/:slug' do
- let(:page) { create(:wiki_page, wiki: project.wiki) }
+ let(:page) { create(:wiki_page, wiki: project_wiki) }
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
@@ -675,5 +687,15 @@ describe API::Wikis do
end
end
end
+
+ context 'when wiki belongs to a group project' do
+ let(:project) { create(:project, namespace: group) }
+
+ before do
+ delete(api(url, user))
+ end
+
+ include_examples '204 No Content'
+ end
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index a16f98bec36..27bd22d6bca 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -149,7 +149,7 @@ describe 'Git HTTP requests' do
context 'and as a developer on the team' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'but the repo is disabled' do
@@ -182,7 +182,7 @@ describe 'Git HTTP requests' do
context 'when authenticated' do
context 'and as a developer on the team' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'but the repo is disabled' do
@@ -240,7 +240,7 @@ describe 'Git HTTP requests' do
context 'as a developer on the team' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it_behaves_like 'pulls are allowed'
@@ -324,9 +324,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote and try again:
+ Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo}
+ git remote set-url origin #{project.http_url_to_repo} and try again.
MSG
end
@@ -365,13 +365,13 @@ describe 'Git HTTP requests' do
context "when the user has access to the project" do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
context "when the user is blocked" do
it "rejects pulls with 401 Unauthorized" do
user.block
- project.team << [user, :master]
+ project.add_master(user)
download(path, env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -434,7 +434,7 @@ describe 'Git HTTP requests' do
let(:path) { "#{project.full_path}.git" }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
context 'when username and password are provided' do
@@ -533,9 +533,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote and try again:
+ Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo}
+ git remote set-url origin #{project.http_url_to_repo} and try again.
MSG
end
@@ -612,7 +612,7 @@ describe 'Git HTTP requests' do
context 'and build created by' do
before do
build.update(user: user)
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
shared_examples 'can download code only' do
@@ -795,7 +795,7 @@ describe 'Git HTTP requests' do
context 'and the user is on the team' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it "responds with status 200" do
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index d7bdfde918c..8bfc8693981 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.team << [user, :master]
+ project.add_master(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.team << [user, :master]
+ project.add_master(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.team << [user, :master]
+ project.add_master(user)
project.lfs_objects << lfs_object
end
@@ -276,7 +276,7 @@ describe 'Git LFS API and storage' do
context 'when user allowed' do
let(:update_permissions) do
- project.team << [user, :master]
+ project.add_master(user)
project.lfs_objects << lfs_object
end
@@ -312,7 +312,7 @@ describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_permissions) do
- project.team << [user, :reporter]
+ project.add_reporter(user)
project.lfs_objects << lfs_object
end
@@ -534,7 +534,7 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_user }
let(:update_user_permissions) do
- project.team << [user, role]
+ project.add_role(user, role)
end
it_behaves_like 'an authorized requests' do
@@ -570,7 +570,7 @@ describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_user_permissions) do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
it_behaves_like 'an authorized requests'
@@ -690,7 +690,7 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_user }
let(:update_user_permissions) do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when pushing an lfs object that already exists' do
@@ -812,7 +812,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.team << [user, :master]
+ project.add_master(user)
end
it 'responds with status 401' do
@@ -857,7 +857,7 @@ describe 'Git LFS API and storage' do
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
- project.team << [user, :master]
+ project.add_master(user)
enable_lfs
end
@@ -952,7 +952,7 @@ describe 'Git LFS API and storage' do
describe 'when user has push access to the project' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'and the request bypassed workhorse' do
@@ -1050,7 +1050,7 @@ describe 'Git LFS API and storage' do
describe 'and user does not have push access' do
before do
- project.team << [user, :reporter]
+ project.add_reporter(user)
end
it_behaves_like 'forbidden'
@@ -1067,7 +1067,7 @@ describe 'Git LFS API and storage' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
put_authorize
end
@@ -1119,7 +1119,7 @@ describe 'Git LFS API and storage' do
describe 'when user has push access to the project' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'and request is sent by gitlab-workhorse to authorize the request' do
@@ -1206,7 +1206,7 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_user }
before do
- second_project.team << [user, :master]
+ second_project.add_master(user)
upstream_project.lfs_objects << lfs_object
end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 286d8a884a4..98f70e2101b 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -7,7 +7,7 @@ describe 'cycle analytics events' do
describe 'GET /:namespace/:project/cycle_analytics/events/issues' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
3.times do |count|
Timecop.freeze(Time.now + count.days) do
diff --git a/spec/rubocop/cop/active_record_dependent_spec.rb b/spec/rubocop/cop/active_record_dependent_spec.rb
deleted file mode 100644
index 599a032bfc5..00000000000
--- a/spec/rubocop/cop/active_record_dependent_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
-require_relative '../../../rubocop/cop/active_record_dependent'
-
-describe RuboCop::Cop::ActiveRecordDependent do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'inside the app/models directory' do
- it 'registers an offense when dependent: is used' do
- allow(cop).to receive(:in_model?).and_return(true)
-
- inspect_source(cop, 'belongs_to :foo, dependent: :destroy')
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
- end
-
- context 'outside the app/models directory' do
- it 'does nothing' do
- allow(cop).to receive(:in_model?).and_return(false)
-
- inspect_source(cop, 'belongs_to :foo, dependent: :destroy')
-
- expect(cop.offenses).to be_empty
- end
- end
-end
diff --git a/spec/rubocop/cop/active_record_serialize_spec.rb b/spec/rubocop/cop/active_record_serialize_spec.rb
deleted file mode 100644
index b94b25cecd0..00000000000
--- a/spec/rubocop/cop/active_record_serialize_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
-require_relative '../../../rubocop/cop/active_record_serialize'
-
-describe RuboCop::Cop::ActiveRecordSerialize do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'inside the app/models directory' do
- it 'registers an offense when serialize is used' do
- allow(cop).to receive(:in_model?).and_return(true)
-
- inspect_source(cop, 'serialize :foo')
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
- end
-
- context 'outside the app/models directory' do
- it 'does nothing' do
- allow(cop).to receive(:in_model?).and_return(false)
-
- inspect_source(cop, 'serialize :foo')
-
- expect(cop.offenses).to be_empty
- end
- end
-end
diff --git a/spec/rubocop/cop/custom_error_class_spec.rb b/spec/rubocop/cop/custom_error_class_spec.rb
deleted file mode 100644
index 381d7871a40..00000000000
--- a/spec/rubocop/cop/custom_error_class_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../rubocop/cop/custom_error_class'
-
-describe RuboCop::Cop::CustomErrorClass do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'when a class has a body' do
- it 'does nothing' do
- inspect_source(cop, 'class CustomError < StandardError; def foo; end; end')
-
- expect(cop.offenses).to be_empty
- end
- end
-
- context 'when a class has no explicit superclass' do
- it 'does nothing' do
- inspect_source(cop, 'class CustomError; end')
-
- expect(cop.offenses).to be_empty
- end
- end
-
- context 'when a class has a superclass that does not end in Error' do
- it 'does nothing' do
- inspect_source(cop, 'class CustomError < BasicObject; end')
-
- expect(cop.offenses).to be_empty
- end
- end
-
- context 'when a class is empty and inherits from a class ending in Error' do
- context 'when the class is on a single line' do
- let(:source) do
- <<-SOURCE
- module Foo
- class CustomError < Bar::Baz::BaseError; end
- end
- SOURCE
- end
-
- let(:expected) do
- <<-EXPECTED
- module Foo
- CustomError = Class.new(Bar::Baz::BaseError)
- end
- EXPECTED
- end
-
- it 'registers an offense' do
- expected_highlights = source.split("\n")[1].strip
-
- inspect_source(cop, source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([2])
- expect(cop.highlights).to contain_exactly(expected_highlights)
- end
- end
-
- it 'autocorrects to the right version' do
- autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
-
- expect(autocorrected).to eq(expected)
- end
- end
-
- context 'when the class is on multiple lines' do
- let(:source) do
- <<-SOURCE
- module Foo
- class CustomError < Bar::Baz::BaseError
- end
- end
- SOURCE
- end
-
- let(:expected) do
- <<-EXPECTED
- module Foo
- CustomError = Class.new(Bar::Baz::BaseError)
- end
- EXPECTED
- end
-
- it 'registers an offense' do
- expected_highlights = source.split("\n")[1..2].join("\n").strip
-
- inspect_source(cop, source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([2])
- expect(cop.highlights).to contain_exactly(expected_highlights)
- end
- end
-
- it 'autocorrects to the right version' do
- autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
-
- expect(autocorrected).to eq(expected)
- end
- end
- end
-end
diff --git a/spec/rubocop/cop/gem_fetcher_spec.rb b/spec/rubocop/cop/gem_fetcher_spec.rb
deleted file mode 100644
index c07f6a831dc..00000000000
--- a/spec/rubocop/cop/gem_fetcher_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../rubocop/cop/gem_fetcher'
-
-describe RuboCop::Cop::GemFetcher do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in Gemfile' do
- before do
- allow(cop).to receive(:gemfile?).and_return(true)
- end
-
- it 'registers an offense when a gem uses `git`' do
- inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"')
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq(['git: "https://gitlab.com/foo/bar.git"'])
- end
- end
-
- it 'registers an offense when a gem uses `github`' do
- inspect_source(cop, 'gem "foo", github: "foo/bar.git"')
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq(['github: "foo/bar.git"'])
- end
- end
- end
-
- context 'outside of Gemfile' do
- it 'registers no offense' do
- inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"')
-
- expect(cop.offenses.size).to eq(0)
- end
- end
-end
diff --git a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
new file mode 100644
index 00000000000..8e2d5f70353
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
@@ -0,0 +1,157 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/module_with_instance_variables'
+
+describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples('registering offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when instance variable is used in a module' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ shared_examples('not registering offense') do
+ it 'does not register offenses' do
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'when source is a regular module' do
+ it_behaves_like 'registering offense', offending_lines: [3] do
+ let(:source) do
+ <<~RUBY
+ module M
+ def f
+ @f = true
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is a nested module' do
+ it_behaves_like 'registering offense', offending_lines: [4] do
+ let(:source) do
+ <<~RUBY
+ module N
+ module M
+ def f
+ @f = true
+ end
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is a nested module with multiple offenses' do
+ it_behaves_like 'registering offense', offending_lines: [4, 12] do
+ let(:source) do
+ <<~RUBY
+ module N
+ module M
+ def f
+ @f = true
+ end
+
+ def g
+ true
+ end
+
+ def h
+ @h = true
+ end
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is using simple or ivar assignment' do
+ it_behaves_like 'not registering offense' do
+ let(:source) do
+ <<~RUBY
+ module M
+ def f
+ @f ||= true
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is using simple ivar' do
+ it_behaves_like 'not registering offense' do
+ let(:source) do
+ <<~RUBY
+ module M
+ def f?
+ @f
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is defining initialize' do
+ it_behaves_like 'not registering offense' do
+ let(:source) do
+ <<~RUBY
+ module M
+ def initialize
+ @a = 1
+ @b = 2
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is using simple or ivar assignment with other ivar' do
+ it_behaves_like 'registering offense', offending_lines: [3] do
+ let(:source) do
+ <<~RUBY
+ module M
+ def f
+ @f ||= g(@g)
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is using or ivar assignment with something else' do
+ it_behaves_like 'registering offense', offending_lines: [3, 4] do
+ let(:source) do
+ <<~RUBY
+ module M
+ def f
+ @f ||= true
+ @f.to_s
+ end
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/in_batches_spec.rb b/spec/rubocop/cop/in_batches_spec.rb
deleted file mode 100644
index 072481984c6..00000000000
--- a/spec/rubocop/cop/in_batches_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require 'spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
-require_relative '../../../rubocop/cop/in_batches'
-
-describe RuboCop::Cop::InBatches do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- it 'registers an offense when in_batches is used' do
- inspect_source(cop, 'foo.in_batches do; end')
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
-end
diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
new file mode 100644
index 00000000000..f5109287876
--- /dev/null
+++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/include_sidekiq_worker'
+
+describe RuboCop::Cop::IncludeSidekiqWorker do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when `Sidekiq::Worker` is included' do
+ let(:source) { 'include Sidekiq::Worker' }
+ let(:correct_source) { 'include ApplicationWorker' }
+
+ it 'registers an offense ' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['Sidekiq::Worker'])
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(source)
+
+ expect(autocorrected).to eq(correct_source)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb
deleted file mode 100644
index 8899dc85384..00000000000
--- a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb
+++ /dev/null
@@ -1,160 +0,0 @@
-require 'spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
-require_relative '../../../rubocop/cop/line_break_after_guard_clauses'
-
-describe RuboCop::Cop::LineBreakAfterGuardClauses do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- shared_examples 'examples with guard clause' do |title|
- %w[if unless].each do |conditional|
- it "flags violation for #{title} #{conditional} without line breaks" do
- source = <<~RUBY
- #{title} #{conditional} condition
- do_stuff
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses.size).to eq(1)
- offense = cop.offenses.first
-
- expect(offense.line).to eq(1)
- expect(cop.highlights).to eq(["#{title} #{conditional} condition"])
- expect(offense.message).to eq('Add a line break after guard clauses')
- end
-
- it "doesn't flag violation for #{title} #{conditional} with line break" do
- source = <<~RUBY
- #{title} #{conditional} condition
-
- do_stuff
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} on multiple lines without line break" do
- source = <<~RUBY
- #{conditional} condition
- #{title}
- end
- do_stuff
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by end keyword" do
- source = <<~RUBY
- def test
- #{title} #{conditional} condition
- end
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by elsif keyword" do
- source = <<~RUBY
- if model
- #{title} #{conditional} condition
- elsif
- do_something
- end
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by else keyword" do
- source = <<~RUBY
- if model
- #{title} #{conditional} condition
- else
- do_something
- end
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by when keyword" do
- source = <<~RUBY
- case model
- when condition_a
- #{title} #{conditional} condition
- when condition_b
- do_something
- end
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by rescue keyword" do
- source = <<~RUBY
- begin
- #{title} #{conditional} condition
- rescue StandardError
- do_something
- end
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by ensure keyword" do
- source = <<~RUBY
- def foo
- #{title} #{conditional} condition
- ensure
- do_something
- end
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by another guard clause" do
- source = <<~RUBY
- #{title} #{conditional} condition
- #{title} #{conditional} condition
-
- do_stuff
- RUBY
- inspect_source(cop, source)
-
- expect(cop.offenses).to be_empty
- end
-
- it "autocorrects #{title} #{conditional} guard clauses without line break" do
- source = <<~RUBY
- #{title} #{conditional} condition
- do_stuff
- RUBY
- autocorrected = autocorrect_source(cop, source)
-
- expected_source = <<~RUBY
- #{title} #{conditional} condition
-
- do_stuff
- RUBY
- expect(autocorrected).to eql(expected_source)
- end
- end
- end
-
- %w[return fail raise next break throw].each do |example|
- it_behaves_like 'examples with guard clause', example
- end
-end
diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
index 7cb24dc5646..1df1fffb94e 100644
--- a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
+
require 'rubocop'
require 'rubocop/rspec/support'
+
require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key'
describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
@@ -10,7 +12,7 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
context 'outside of a migration' do
it 'does not register any offenses' do
- inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
+ inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id); end')
expect(cop.offenses).to be_empty
end
@@ -22,7 +24,7 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
end
it 'registers an offense when using add_foreign_key' do
- inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
+ inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id); end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
index 19a5718b0b1..9c1ebcc0ced 100644
--- a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
@@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::AddConcurrentIndex do
end
it 'registers an offense when add_concurrent_index is used inside a change method' do
- inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+ inspect_source('def change; add_concurrent_index :table, :column; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::AddConcurrentIndex do
end
it 'registers no offense when add_concurrent_index is used inside an up method' do
- inspect_source(cop, 'def up; add_concurrent_index :table, :column; end')
+ inspect_source('def up; add_concurrent_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
@@ -33,7 +33,7 @@ describe RuboCop::Cop::Migration::AddConcurrentIndex do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+ inspect_source('def change; add_concurrent_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb
index 18df62dec3e..3a41c91add2 100644
--- a/spec/rubocop/cop/migration/add_timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb
@@ -53,7 +53,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do
end
it 'registers an offense when the "add_timestamps" method is used' do
- inspect_source(cop, migration_with_add_timestamps)
+ inspect_source(migration_with_add_timestamps)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -62,7 +62,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do
end
it 'does not register an offense when the "add_timestamps" method is not used' do
- inspect_source(cop, migration_without_add_timestamps)
+ inspect_source(migration_without_add_timestamps)
aggregate_failures do
expect(cop.offenses.size).to eq(0)
@@ -70,7 +70,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do
end
it 'does not register an offense when the "add_timestamps_with_timezone" method is used' do
- inspect_source(cop, migration_with_add_timestamps_with_timezone)
+ inspect_source(migration_with_add_timestamps_with_timezone)
aggregate_failures do
expect(cop.offenses.size).to eq(0)
@@ -80,9 +80,9 @@ describe RuboCop::Cop::Migration::AddTimestamps do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, migration_with_add_timestamps)
- inspect_source(cop, migration_without_add_timestamps)
- inspect_source(cop, migration_with_add_timestamps_with_timezone)
+ inspect_source(migration_with_add_timestamps)
+ inspect_source(migration_without_add_timestamps)
+ inspect_source(migration_with_add_timestamps_with_timezone)
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb
index b1dfcf1b048..9e844325371 100644
--- a/spec/rubocop/cop/migration/datetime_spec.rb
+++ b/spec/rubocop/cop/migration/datetime_spec.rb
@@ -67,7 +67,7 @@ describe RuboCop::Cop::Migration::Datetime do
end
it 'registers an offense when the ":datetime" data type is used' do
- inspect_source(cop, migration_with_datetime)
+ inspect_source(migration_with_datetime)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -77,7 +77,7 @@ describe RuboCop::Cop::Migration::Datetime do
end
it 'registers an offense when the ":timestamp" data type is used' do
- inspect_source(cop, migration_with_timestamp)
+ inspect_source(migration_with_timestamp)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -87,7 +87,7 @@ describe RuboCop::Cop::Migration::Datetime do
end
it 'does not register an offense when the ":datetime" data type is not used' do
- inspect_source(cop, migration_without_datetime)
+ inspect_source(migration_without_datetime)
aggregate_failures do
expect(cop.offenses.size).to eq(0)
@@ -95,7 +95,7 @@ describe RuboCop::Cop::Migration::Datetime do
end
it 'does not register an offense when the ":datetime_with_timezone" data type is used' do
- inspect_source(cop, migration_with_datetime_with_timezone)
+ inspect_source(migration_with_datetime_with_timezone)
aggregate_failures do
expect(cop.offenses.size).to eq(0)
@@ -105,10 +105,10 @@ describe RuboCop::Cop::Migration::Datetime do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, migration_with_datetime)
- inspect_source(cop, migration_with_timestamp)
- inspect_source(cop, migration_without_datetime)
- inspect_source(cop, migration_with_datetime_with_timezone)
+ inspect_source(migration_with_datetime)
+ inspect_source(migration_with_timestamp)
+ inspect_source(migration_without_datetime)
+ inspect_source(migration_with_datetime_with_timezone)
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/hash_index_spec.rb b/spec/rubocop/cop/migration/hash_index_spec.rb
index 9a8576a19e5..5d53dde9a79 100644
--- a/spec/rubocop/cop/migration/hash_index_spec.rb
+++ b/spec/rubocop/cop/migration/hash_index_spec.rb
@@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::HashIndex do
end
it 'registers an offense when creating a hash index' do
- inspect_source(cop, 'def change; add_index :table, :column, using: :hash; end')
+ inspect_source('def change; add_index :table, :column, using: :hash; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::HashIndex do
end
it 'registers an offense when creating a concurrent hash index' do
- inspect_source(cop, 'def change; add_concurrent_index :table, :column, using: :hash; end')
+ inspect_source('def change; add_concurrent_index :table, :column, using: :hash; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -34,7 +34,7 @@ describe RuboCop::Cop::Migration::HashIndex do
end
it 'registers an offense when creating a hash index using t.index' do
- inspect_source(cop, 'def change; t.index :table, :column, using: :hash; end')
+ inspect_source('def change; t.index :table, :column, using: :hash; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -45,7 +45,7 @@ describe RuboCop::Cop::Migration::HashIndex do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, 'def change; index :table, :column, using: :hash; end')
+ inspect_source('def change; index :table, :column, using: :hash; end')
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/remove_column_spec.rb b/spec/rubocop/cop/migration/remove_column_spec.rb
new file mode 100644
index 00000000000..f1a64f431bd
--- /dev/null
+++ b/spec/rubocop/cop/migration/remove_column_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/remove_column'
+
+describe RuboCop::Cop::Migration::RemoveColumn do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ def source(meth = 'change')
+ "def #{meth}; remove_column :table, :column; end"
+ end
+
+ context 'in a regular migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
+ end
+
+ it 'registers an offense when remove_column is used in the change method' do
+ inspect_source(source('change'))
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers an offense when remove_column is used in the up method' do
+ inspect_source(source('up'))
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers no offense when remove_column is used in the down method' do
+ inspect_source(source('down'))
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'in a post-deployment migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(true)
+ end
+
+ it 'registers no offense' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of a migration' do
+ it 'registers no offense' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
index a714bf4e5d5..a23d5d022e3 100644
--- a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
+++ b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
@@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::RemoveConcurrentIndex do
end
it 'registers an offense when remove_concurrent_index is used inside a change method' do
- inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end')
+ inspect_source('def change; remove_concurrent_index :table, :column; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::RemoveConcurrentIndex do
end
it 'registers no offense when remove_concurrent_index is used inside an up method' do
- inspect_source(cop, 'def up; remove_concurrent_index :table, :column; end')
+ inspect_source('def up; remove_concurrent_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
@@ -33,7 +33,7 @@ describe RuboCop::Cop::Migration::RemoveConcurrentIndex do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end')
+ inspect_source('def change; remove_concurrent_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/remove_index_spec.rb b/spec/rubocop/cop/migration/remove_index_spec.rb
index 31923cb7429..bbf2227e512 100644
--- a/spec/rubocop/cop/migration/remove_index_spec.rb
+++ b/spec/rubocop/cop/migration/remove_index_spec.rb
@@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::RemoveIndex do
end
it 'registers an offense when remove_index is used' do
- inspect_source(cop, 'def change; remove_index :table, :column; end')
+ inspect_source('def change; remove_index :table, :column; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -27,7 +27,7 @@ describe RuboCop::Cop::Migration::RemoveIndex do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, 'def change; remove_index :table, :column; end')
+ inspect_source('def change; remove_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
index 3723d635083..ba8cd2c6c4a 100644
--- a/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
+++ b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
@@ -16,7 +16,7 @@ describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do
end
it 'registers an offense when add_column_with_default is used inside a change method' do
- inspect_source(cop, 'def change; add_column_with_default :table, :column, default: false; end')
+ inspect_source('def change; add_column_with_default :table, :column, default: false; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do
end
it 'registers no offense when add_column_with_default is used inside an up method' do
- inspect_source(cop, 'def up; add_column_with_default :table, :column, default: false; end')
+ inspect_source('def up; add_column_with_default :table, :column, default: false; end')
expect(cop.offenses.size).to eq(0)
end
@@ -33,7 +33,7 @@ describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, 'def change; add_column_with_default :table, :column, default: false; end')
+ inspect_source('def change; add_column_with_default :table, :column, default: false; end')
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
index 1006594499a..1c4f18fbcc3 100644
--- a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
+++ b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
@@ -32,7 +32,7 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do
sources_and_offense.each do |source, offense|
context "given the source \"#{source}\"" do
it "registers the offense matching \"#{offense}\"" do
- inspect_source(cop, source)
+ inspect_source(source)
aggregate_failures do
expect(cop.offenses.first.message).to match(offense)
@@ -49,7 +49,7 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do
inoffensive_sources.each do |source|
context "given the source \"#{source}\"" do
it "registers no offense" do
- inspect_source(cop, source)
+ inspect_source(source)
aggregate_failures do
expect(cop.offenses).to be_empty
@@ -61,14 +61,14 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do
end
it 'registers no offense for tables not listed in SMALL_TABLES' do
- inspect_source(cop, "add_column :large_table, :column, :boolean")
+ inspect_source("add_column :large_table, :column, :boolean")
expect(cop.offenses).to be_empty
end
it 'registers no offense for non-boolean columns' do
table = described_class::SMALL_TABLES.sample
- inspect_source(cop, "add_column :#{table}, :column, :string")
+ inspect_source("add_column :#{table}, :column, :string")
expect(cop.offenses).to be_empty
end
@@ -77,7 +77,7 @@ describe RuboCop::Cop::Migration::SaferBooleanColumn do
context 'outside of migration' do
it 'registers no offense' do
table = described_class::SMALL_TABLES.sample
- inspect_source(cop, "add_column :#{table}, :column, :boolean")
+ inspect_source("add_column :#{table}, :column, :boolean")
expect(cop.offenses).to be_empty
end
diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb
index cdf1423d0c5..685bdb21803 100644
--- a/spec/rubocop/cop/migration/timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/timestamps_spec.rb
@@ -62,7 +62,7 @@ describe RuboCop::Cop::Migration::Timestamps do
end
it 'registers an offense when the "timestamps" method is used' do
- inspect_source(cop, migration_with_timestamps)
+ inspect_source(migration_with_timestamps)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -71,7 +71,7 @@ describe RuboCop::Cop::Migration::Timestamps do
end
it 'does not register an offense when the "timestamps" method is not used' do
- inspect_source(cop, migration_without_timestamps)
+ inspect_source(migration_without_timestamps)
aggregate_failures do
expect(cop.offenses.size).to eq(0)
@@ -79,7 +79,7 @@ describe RuboCop::Cop::Migration::Timestamps do
end
it 'does not register an offense when the "timestamps_with_timezone" method is used' do
- inspect_source(cop, migration_with_timestamps_with_timezone)
+ inspect_source(migration_with_timestamps_with_timezone)
aggregate_failures do
expect(cop.offenses.size).to eq(0)
@@ -89,9 +89,9 @@ describe RuboCop::Cop::Migration::Timestamps do
context 'outside of migration' do
it 'registers no offense' do
- inspect_source(cop, migration_with_timestamps)
- inspect_source(cop, migration_without_timestamps)
- inspect_source(cop, migration_with_timestamps_with_timezone)
+ inspect_source(migration_with_timestamps)
+ inspect_source(migration_without_timestamps)
+ inspect_source(migration_with_timestamps_with_timezone)
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
index 38b8f439a55..1c8ab0ad5d2 100644
--- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
+++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
+
require 'rubocop'
require 'rubocop/rspec/support'
+
require_relative '../../../../rubocop/cop/migration/update_column_in_batches'
describe RuboCop::Cop::Migration::UpdateColumnInBatches do
@@ -25,7 +27,7 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do
context 'outside of a migration' do
it 'does not register any offenses' do
- inspect_source(cop, migration_code)
+ inspect_source(migration_code)
expect(cop.offenses).to be_empty
end
@@ -49,7 +51,7 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do
let(:relative_spec_filepath) { Pathname.new(spec_filepath).relative_path_from(tmp_rails_root) }
it 'registers an offense when using update_column_in_batches' do
- inspect_source(cop, migration_code, @migration_file)
+ inspect_source(migration_code, @migration_file)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -72,7 +74,7 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do
end
it 'does not register any offenses' do
- inspect_source(cop, migration_code, @migration_file)
+ inspect_source(migration_code, @migration_file)
expect(cop.offenses).to be_empty
end
diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb
index 17b19e139e4..ef724fc8bad 100644
--- a/spec/rubocop/cop/migration/update_large_table_spec.rb
+++ b/spec/rubocop/cop/migration/update_large_table_spec.rb
@@ -18,7 +18,7 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
shared_examples 'large tables' do |update_method|
described_class::LARGE_TABLES.each do |table|
it "registers an offense for the #{table} table" do
- inspect_source(cop, "#{update_method} :#{table}, :column, default: true")
+ inspect_source("#{update_method} :#{table}, :column, default: true")
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -37,7 +37,7 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
end
it 'registers no offense for non-blacklisted tables' do
- inspect_source(cop, "add_column_with_default :table, :column, default: true")
+ inspect_source("add_column_with_default :table, :column, default: true")
expect(cop.offenses).to be_empty
end
@@ -45,7 +45,7 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
it 'registers no offense for non-blacklisted methods' do
table = described_class::LARGE_TABLES.sample
- inspect_source(cop, "some_other_method :#{table}, :column, default: true")
+ inspect_source("some_other_method :#{table}, :column, default: true")
expect(cop.offenses).to be_empty
end
@@ -55,13 +55,13 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
let(:table) { described_class::LARGE_TABLES.sample }
it 'registers no offense for add_column_with_default' do
- inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+ inspect_source("add_column_with_default :#{table}, :column, default: true")
expect(cop.offenses).to be_empty
end
it 'registers no offense for update_column_in_batches' do
- inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+ inspect_source("add_column_with_default :#{table}, :column, default: true")
expect(cop.offenses).to be_empty
end
diff --git a/spec/rubocop/cop/polymorphic_associations_spec.rb b/spec/rubocop/cop/polymorphic_associations_spec.rb
deleted file mode 100644
index 49959aa6419..00000000000
--- a/spec/rubocop/cop/polymorphic_associations_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
-require_relative '../../../rubocop/cop/polymorphic_associations'
-
-describe RuboCop::Cop::PolymorphicAssociations do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'inside the app/models directory' do
- it 'registers an offense when polymorphic: true is used' do
- allow(cop).to receive(:in_model?).and_return(true)
-
- inspect_source(cop, 'belongs_to :foo, polymorphic: true')
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
- end
-
- context 'outside the app/models directory' do
- it 'does nothing' do
- allow(cop).to receive(:in_model?).and_return(false)
-
- inspect_source(cop, 'belongs_to :foo, polymorphic: true')
-
- expect(cop.offenses).to be_empty
- end
- end
-end
diff --git a/spec/rubocop/cop/project_path_helper_spec.rb b/spec/rubocop/cop/project_path_helper_spec.rb
index bc47b45cad7..84e6eb7d87f 100644
--- a/spec/rubocop/cop/project_path_helper_spec.rb
+++ b/spec/rubocop/cop/project_path_helper_spec.rb
@@ -15,7 +15,7 @@ describe RuboCop::Cop::ProjectPathHelper do
let(:correct_source) { 'edit_project_issue_path(@issue.project, @issue)' }
it 'registers an offense' do
- inspect_source(cop, source)
+ inspect_source(source)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
@@ -25,7 +25,7 @@ describe RuboCop::Cop::ProjectPathHelper do
end
it 'autocorrects to the right version' do
- autocorrected = autocorrect_source(cop, source)
+ autocorrected = autocorrect_source(source)
expect(autocorrected).to eq(correct_source)
end
@@ -33,7 +33,7 @@ describe RuboCop::Cop::ProjectPathHelper do
context 'when using namespace_project with a different namespace' do
it 'registers no offense' do
- inspect_source(cop, 'edit_namespace_project_issue_path(namespace, project)')
+ inspect_source('edit_namespace_project_issue_path(namespace, project)')
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/redirect_with_status_spec.rb b/spec/rubocop/cop/redirect_with_status_spec.rb
deleted file mode 100644
index 5ad63567f84..00000000000
--- a/spec/rubocop/cop/redirect_with_status_spec.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../rubocop/cop/redirect_with_status'
-
-describe RuboCop::Cop::RedirectWithStatus do
- include CopHelper
-
- subject(:cop) { described_class.new }
- let(:controller_fixture_without_status) do
- %q(
- class UserController < ApplicationController
- def show
- user = User.find(params[:id])
- redirect_to user_path if user.name == 'John Wick'
- end
-
- def destroy
- user = User.find(params[:id])
-
- if user.destroy
- redirect_to root_path
- else
- render :show
- end
- end
- end
- )
- end
-
- let(:controller_fixture_with_status) do
- %q(
- class UserController < ApplicationController
- def show
- user = User.find(params[:id])
- redirect_to user_path if user.name == 'John Wick'
- end
-
- def destroy
- user = User.find(params[:id])
-
- if user.destroy
- redirect_to root_path, status: 302
- else
- render :show
- end
- end
- end
- )
- end
-
- context 'in controller' do
- before do
- allow(cop).to receive(:in_controller?).and_return(true)
- end
-
- it 'registers an offense when a "destroy" action uses "redirect_to" without "status"' do
- inspect_source(cop, controller_fixture_without_status)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([12]) # 'redirect_to' is located on 12th line in controller_fixture.
- expect(cop.highlights).to eq(['redirect_to'])
- end
- end
-
- it 'does not register an offense when a "destroy" action uses "redirect_to" with "status"' do
- inspect_source(cop, controller_fixture_with_status)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(0)
- end
- end
- end
-
- context 'outside of controller' do
- it 'registers no offense' do
- inspect_source(cop, controller_fixture_without_status)
- inspect_source(cop, controller_fixture_with_status)
-
- expect(cop.offenses.size).to eq(0)
- end
- end
-end
diff --git a/spec/rubocop/cop/rspec/env_assignment_spec.rb b/spec/rubocop/cop/rspec/env_assignment_spec.rb
index 4e859b6f6fa..659633f6467 100644
--- a/spec/rubocop/cop/rspec/env_assignment_spec.rb
+++ b/spec/rubocop/cop/rspec/env_assignment_spec.rb
@@ -17,7 +17,7 @@ describe RuboCop::Cop::RSpec::EnvAssignment do
shared_examples 'an offensive ENV#[]= call' do |content|
it "registers an offense for `#{content}`" do
- inspect_source(cop, content, source_file)
+ inspect_source(content, source_file)
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([1])
@@ -27,7 +27,7 @@ describe RuboCop::Cop::RSpec::EnvAssignment do
shared_examples 'an autocorrected ENV#[]= call' do |content, autocorrected_content|
it "registers an offense for `#{content}` and autocorrects it to `#{autocorrected_content}`" do
- autocorrected = autocorrect_source(cop, content, source_file)
+ autocorrected = autocorrect_source(content, source_file)
expect(autocorrected).to eql(autocorrected_content)
end
@@ -51,7 +51,7 @@ describe RuboCop::Cop::RSpec::EnvAssignment do
context 'outside of a spec file' do
it "does not register an offense for `#{OFFENSE_CALL_SINGLE_QUOTES_KEY}` in a non-spec file" do
- inspect_source(cop, OFFENSE_CALL_SINGLE_QUOTES_KEY)
+ inspect_source(OFFENSE_CALL_SINGLE_QUOTES_KEY)
expect(cop.offenses.size).to eq(0)
end
diff --git a/spec/rubocop/cop/rspec/single_line_hook_spec.rb b/spec/rubocop/cop/rspec/single_line_hook_spec.rb
deleted file mode 100644
index 6cf0831d3ad..00000000000
--- a/spec/rubocop/cop/rspec/single_line_hook_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/rspec/single_line_hook'
-
-describe RuboCop::Cop::RSpec::SingleLineHook do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- # Override `CopHelper#inspect_source` to always appear to be in a spec file,
- # so that our RSpec-only cop actually runs
- def inspect_source(*args)
- super(*args, 'foo_spec.rb')
- end
-
- it 'registers an offense for a single-line `before` block' do
- inspect_source(cop, 'before { do_something }')
-
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq(['before { do_something }'])
- end
-
- it 'registers an offense for a single-line `after` block' do
- inspect_source(cop, 'after(:each) { undo_something }')
-
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq(['after(:each) { undo_something }'])
- end
-
- it 'registers an offense for a single-line `around` block' do
- inspect_source(cop, 'around { |ex| do_something_else }')
-
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq(['around { |ex| do_something_else }'])
- end
-
- it 'ignores a multi-line `before` block' do
- inspect_source(cop, ['before do',
- ' do_something',
- 'end'])
-
- expect(cop.offenses.size).to eq(0)
- end
-
- it 'ignores a multi-line `after` block' do
- inspect_source(cop, ['after(:each) do',
- ' undo_something',
- 'end'])
-
- expect(cop.offenses.size).to eq(0)
- end
-
- it 'ignores a multi-line `around` block' do
- inspect_source(cop, ['around do |ex|',
- ' do_something_else',
- 'end'])
-
- expect(cop.offenses.size).to eq(0)
- end
-end
diff --git a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
deleted file mode 100644
index 278662d32ea..00000000000
--- a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
-require_relative '../../../../rubocop/cop/rspec/verbose_include_metadata'
-
-describe RuboCop::Cop::RSpec::VerboseIncludeMetadata do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- let(:source_file) { 'foo_spec.rb' }
-
- # Override `CopHelper#inspect_source` to always appear to be in a spec file,
- # so that our RSpec-only cop actually runs
- def inspect_source(*args)
- super(*args, source_file)
- end
-
- shared_examples 'examples with include syntax' do |title|
- it "flags violation for #{title} examples that uses verbose include syntax" do
- inspect_source(cop, "#{title} 'Test', js: true do; end")
-
- expect(cop.offenses.size).to eq(1)
- offense = cop.offenses.first
-
- expect(offense.line).to eq(1)
- expect(cop.highlights).to eq(["#{title} 'Test', js: true"])
- expect(offense.message).to eq('Use `:js` instead of `js: true`.')
- end
-
- it "doesn't flag violation for #{title} examples that uses compact include syntax", :aggregate_failures do
- inspect_source(cop, "#{title} 'Test', :js do; end")
-
- expect(cop.offenses).to be_empty
- end
-
- it "doesn't flag violation for #{title} examples that uses flag: symbol" do
- inspect_source(cop, "#{title} 'Test', flag: :symbol do; end")
-
- expect(cop.offenses).to be_empty
- end
-
- it "autocorrects #{title} examples that uses verbose syntax into compact syntax" do
- autocorrected = autocorrect_source(cop, "#{title} 'Test', js: true do; end", source_file)
-
- expect(autocorrected).to eql("#{title} 'Test', :js do; end")
- end
- end
-
- %w(describe context feature example_group it specify example scenario its).each do |example|
- it_behaves_like 'examples with include syntax', example
- end
-end
diff --git a/spec/rubocop/cop/sidekiq_options_queue_spec.rb b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
new file mode 100644
index 00000000000..7f237d5ffbb
--- /dev/null
+++ b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/sidekiq_options_queue'
+
+describe RuboCop::Cop::SidekiqOptionsQueue do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'registers an offense when `sidekiq_options` is used with the `queue` option' do
+ inspect_source('sidekiq_options queue: "some_queue"')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['queue: "some_queue"'])
+ end
+ end
+
+ it 'does not register an offense when `sidekiq_options` is used with another option' do
+ inspect_source('sidekiq_options retry: false')
+
+ expect(cop.offenses).to be_empty
+ end
+end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index 87c7b2ad36e..b5a55b4ef6e 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe ClusterApplicationEntity do
describe '#as_json' do
- let(:application) { build(:cluster_applications_helm) }
+ let(:application) { build(:clusters_applications_helm) }
subject { described_class.new(application).as_json }
it 'has name' do
@@ -18,7 +18,7 @@ describe ClusterApplicationEntity do
end
context 'when application is errored' do
- let(:application) { build(:cluster_applications_helm, :errored) }
+ let(:application) { build(:clusters_applications_helm, :errored) }
it 'has corresponded data' do
expect(subject[:status]).to eq(:errored)
diff --git a/spec/serializers/event_entity_spec.rb b/spec/serializers/event_entity_spec.rb
deleted file mode 100644
index bb54597c967..00000000000
--- a/spec/serializers/event_entity_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'spec_helper'
-
-describe EventEntity do
- subject { described_class.represent(create(:event)).as_json }
-
- it 'exposes author' do
- expect(subject).to include(:author)
- end
-
- it 'exposes core elements of event' do
- expect(subject).to include(:updated_at)
- end
-end
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index 452754d7a79..505a9eaac5a 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -22,6 +22,7 @@ describe GroupChildEntity do
avatar_url
name
description
+ markdown_description
visibility
type
can_edit
@@ -60,9 +61,10 @@ describe GroupChildEntity do
end
describe 'for a group', :nested_groups do
+ let(:description) { 'Awesomeness' }
let(:object) do
create(:group, :nested, :with_avatar,
- description: 'Awesomeness')
+ description: description)
end
before do
@@ -96,6 +98,14 @@ describe GroupChildEntity do
expect(json[:edit_path]).to eq(edit_group_path(object))
end
+ context 'emoji in description' do
+ let(:description) { ':smile:' }
+
+ it 'has the correct markdown_description' do
+ expect(json[:markdown_description]).to eq('<p dir="auto"><gl-emoji title="smiling face with open mouth and smiling eyes" data-name="smile" data-unicode-version="6.0">😄</gl-emoji></p>')
+ end
+ end
+
it_behaves_like 'group child json'
end
end
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index e3abefa6d63..b259cb92962 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -1,37 +1,43 @@
require 'spec_helper'
describe MergeRequestSerializer do
- let(:user) { build_stubbed(:user) }
- let(:merge_request) { build_stubbed(:merge_request) }
-
- let(:serializer) do
+ let(:user) { create(:user) }
+ let(:resource) { create(:merge_request) }
+ let(:json_entity) do
described_class.new(current_user: user)
+ .represent(resource, serializer: serializer)
+ .with_indifferent_access
end
- describe '#represent' do
- let(:opts) { { serializer: serializer_entity } }
- subject { serializer.represent(merge_request, serializer: serializer_entity) }
+ context 'widget merge request serialization' do
+ let(:serializer) { 'widget' }
- context 'when passing basic serializer param' do
- let(:serializer_entity) { 'basic' }
+ it 'matches issue json schema' do
+ expect(json_entity).to match_schema('entities/merge_request_widget')
+ end
+ end
- it 'calls super class #represent with correct params' do
- expect_any_instance_of(BaseSerializer).to receive(:represent)
- .with(merge_request, opts, MergeRequestBasicEntity)
+ context 'sidebar merge request serialization' do
+ let(:serializer) { 'sidebar' }
- subject
- end
+ it 'matches basic merge request json schema' do
+ expect(json_entity).to match_schema('entities/merge_request_basic')
end
+ end
- context 'when serializer param is falsy' do
- let(:serializer_entity) { nil }
+ context 'basic merge request serialization' do
+ let(:serializer) { 'basic' }
+
+ it 'matches basic merge request json schema' do
+ expect(json_entity).to match_schema('entities/merge_request_basic')
+ end
+ end
- it 'calls super class #represent with correct params' do
- expect_any_instance_of(BaseSerializer).to receive(:represent)
- .with(merge_request, opts, MergeRequestEntity)
+ context 'no serializer' do
+ let(:serializer) { nil }
- subject
- end
+ it 'falls back to the widget entity' do
+ expect(json_entity).to match_schema('entities/merge_request_widget')
end
end
end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 1ad672fd355..80a271ba7fb 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequestEntity do
+describe MergeRequestWidgetEntity do
let(:project) { create :project, :repository }
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -35,31 +35,79 @@ describe MergeRequestEntity do
end
end
- it 'includes issues_links' do
- issues_links = subject[:issues_links]
+ describe 'metrics' do
+ context 'when metrics record exists with merged data' do
+ before do
+ resource.mark_as_merged!
+ resource.metrics.update!(merged_by: user)
+ end
- expect(issues_links).to include(:closing, :mentioned_but_not_closing,
- :assign_to_closing)
- end
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
- it 'has Issuable attributes' do
- expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
- :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
- end
+ it 'returns values from metrics record' do
+ expect(subject.dig(:metrics, :merged_by, :id))
+ .to eq(resource.metrics.merged_by_id)
+ end
+ end
- it 'has time estimation attributes' do
- expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
- end
+ context 'when metrics record exists with closed data' do
+ before do
+ resource.close!
+ resource.metrics.update!(latest_closed_by: user)
+ end
+
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
+
+ it 'returns values from metrics record' do
+ expect(subject.dig(:metrics, :closed_by, :id))
+ .to eq(resource.metrics.latest_closed_by_id)
+ end
+ end
+
+ context 'when metrics does not exists' do
+ before do
+ resource.mark_as_merged!
+ resource.metrics.destroy!
+ resource.reload
+ end
+
+ context 'when events exists' do
+ let!(:closed_event) { create(:event, :closed, project: project, target: resource) }
+ let!(:merge_event) { create(:event, :merged, project: project, target: resource) }
+
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
+
+ it 'returns values from events record' do
+ expect(subject.dig(:metrics, :merged_by, :id))
+ .to eq(merge_event.author_id)
- it 'has important MergeRequest attributes' do
- expect(subject).to include(:state, :deleted_at, :diff_head_sha, :merge_commit_message,
- :has_conflicts, :has_ci, :merge_path,
- :conflict_resolution_path,
- :cancel_merge_when_pipeline_succeeds_path,
- :create_issue_to_resolve_discussions_path,
- :source_branch_path, :target_branch_commits_path,
- :target_branch_tree_path, :commits_count, :merge_ongoing,
- :ff_only_enabled)
+ expect(subject.dig(:metrics, :closed_by, :id))
+ .to eq(closed_event.author_id)
+
+ expect(subject.dig(:metrics, :merged_at).to_s)
+ .to eq(merge_event.updated_at.to_s)
+
+ expect(subject.dig(:metrics, :closed_at).to_s)
+ .to eq(closed_event.updated_at.to_s)
+ end
+ end
+
+ context 'when events does not exists' do
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
+ end
+ end
end
it 'has email_patches_path' do
@@ -116,18 +164,6 @@ describe MergeRequestEntity do
end
end
- it 'includes merge_event' do
- create(:event, :merged, author: user, project: resource.project, target: resource)
-
- expect(subject[:merge_event]).to include(:author, :updated_at)
- end
-
- it 'includes closed_event' do
- create(:event, :closed, author: user, project: resource.project, target: resource)
-
- expect(subject[:closed_event]).to include(:author, :updated_at)
- end
-
describe 'diverged_commits_count' do
context 'when MR open and its diverging' do
it 'returns diverged commits count' do
@@ -154,4 +190,20 @@ describe MergeRequestEntity do
end
end
end
+
+ describe 'when source project is deleted' do
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
+
+ it 'returns a blank rebase_path' do
+ allow(merge_request).to receive(:should_be_rebased?).and_return(true)
+ fork_project.destroy
+ merge_request.reload
+
+ entity = described_class.new(merge_request, request: request).as_json
+
+ expect(entity[:rebase_path]).to be_nil
+ end
+ end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index e40edbfb421..14c9f5cb58c 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe PipelineSerializer do
+ set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:serializer) do
@@ -16,7 +17,7 @@ describe PipelineSerializer do
end
context 'when a single object is being serialized' do
- let(:resource) { create(:ci_empty_pipeline) }
+ let(:resource) { create(:ci_empty_pipeline, project: project) }
it 'serializers the pipeline object' do
expect(subject[:id]).to eq resource.id
@@ -24,7 +25,7 @@ describe PipelineSerializer do
end
context 'when multiple objects are being serialized' do
- let(:resource) { create_list(:ci_pipeline, 2) }
+ let(:resource) { create_list(:ci_pipeline, 2, project: project) }
it 'serializers the array of pipelines' do
expect(subject).not_to be_empty
@@ -100,7 +101,6 @@ describe PipelineSerializer do
context 'number of queries' do
let(:resource) { Ci::Pipeline.all }
- let(:project) { create(:project) }
before do
# Since RequestStore.active? is true we have to allow the
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index 1a56164dba4..f0179e35652 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -11,7 +11,7 @@ describe Boards::Issues::CreateService do
subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'delegates the create proceedings to Issues::CreateService' do
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 01ee3856c99..ff5733b7064 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -34,7 +34,7 @@ describe Boards::Issues::ListService do
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'delegates search to IssuesFinder' do
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 464ff9f94b3..280e411683e 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -15,7 +15,7 @@ describe Boards::Issues::MoveService do
let!(:closed) { create(:closed_list, board: board1) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when moving an issue between lists' do
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index 7d0b396cd06..d5322e1bb21 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Lists::CreateService do
subject(:service) { described_class.new(project, user, label_id: label.id) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when board lists is empty' do
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
index 592f25059ac..82dbd1ee744 100644
--- a/spec/services/boards/lists/generate_service_spec.rb
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -9,7 +9,7 @@ describe Boards::Lists::GenerateService do
subject(:service) { described_class.new(project, user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when board lists is empty' do
diff --git a/spec/services/check_gcp_project_billing_service_spec.rb b/spec/services/check_gcp_project_billing_service_spec.rb
new file mode 100644
index 00000000000..f0e39ba6f49
--- /dev/null
+++ b/spec/services/check_gcp_project_billing_service_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe CheckGcpProjectBillingService do
+ let(:service) { described_class.new }
+ let(:projects) { [double(name: 'first_project'), double(name: 'second_project')] }
+
+ describe '#execute' do
+ before do
+ expect_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_list).and_return(projects)
+
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive_message_chain(:projects_get_billing_info, :billingEnabled)
+ .and_return(project_billing_enabled)
+ end
+
+ subject { service.execute('bogustoken') }
+
+ context 'google account has a billing enabled gcp project' do
+ let(:project_billing_enabled) { true }
+
+ it { is_expected.to eq(projects) }
+ end
+
+ context 'google account does not have a billing enabled gcp project' do
+ let(:project_billing_enabled) { false }
+
+ 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 b0de8d447a2..267258b33a8 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -64,6 +64,18 @@ describe Ci::CreatePipelineService do
create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
end
+ context 'when related merge request is already merged' do
+ let!(:merged_merge_request) do
+ create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project, state: 'merged')
+ end
+
+ it 'does not schedule update head pipeline job' do
+ expect(UpdateHeadPipelineForMergeRequestWorker).not_to receive(:perform_async).with(merged_merge_request.id)
+
+ execute_service
+ end
+ end
+
context 'when the head pipeline sha equals merge request sha' do
it 'updates head pipeline of each merge request' do
merge_request_1
@@ -77,13 +89,13 @@ describe Ci::CreatePipelineService do
end
context 'when the head pipeline sha does not equal merge request sha' do
- it 'raises the ArgumentError error from worker and does not update the head piepeline of MRs' do
+ it 'does not update the head piepeline of MRs' do
merge_request_1
merge_request_2
allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true)
- expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.to raise_error(ArgumentError)
+ expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.not_to raise_error
last_pipeline = Ci::Pipeline.last
@@ -518,5 +530,20 @@ describe Ci::CreatePipelineService do
end
end
end
+
+ context 'when pipeline is running for a tag' do
+ before do
+ config = YAML.dump(test: { script: 'test', only: ['branches'] },
+ deploy: { script: 'deploy', only: ['tags'] })
+
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates a tagged pipeline' do
+ pipeline = execute_service(ref: 'v1.0.0')
+
+ expect(pipeline.tag?).to be true
+ end
+ end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 3ee59014b5b..97a563c1ce1 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
module Ci
describe RegisterJobService do
- let!(:project) { FactoryGirl.create :project, shared_runners_enabled: false }
- let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
- let!(:pending_job) { FactoryGirl.create :ci_build, pipeline: pipeline }
- let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
- let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
+ let!(:project) { FactoryBot.create :project, shared_runners_enabled: false }
+ let!(:pipeline) { FactoryBot.create :ci_pipeline, project: project }
+ let!(:pending_job) { FactoryBot.create :ci_build, pipeline: pipeline }
+ let!(:shared_runner) { FactoryBot.create(:ci_runner, is_shared: true) }
+ let!(:specific_runner) { FactoryBot.create(:ci_runner, is_shared: false) }
before do
specific_runner.assign_to(project)
@@ -74,11 +74,11 @@ module Ci
let!(:project3) { create :project, shared_runners_enabled: true }
let!(:pipeline3) { create :ci_pipeline, project: project3 }
let!(:build1_project1) { pending_job }
- let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
- let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
- let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
- let!(:build2_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
- let!(:build1_project3) { FactoryGirl.create :ci_build, pipeline: pipeline3 }
+ let!(:build2_project1) { FactoryBot.create :ci_build, pipeline: pipeline }
+ let!(:build3_project1) { FactoryBot.create :ci_build, pipeline: pipeline }
+ let!(:build1_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 }
+ let!(:build2_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 }
+ let!(:build1_project3) { FactoryBot.create :ci_build, pipeline: pipeline3 }
it 'prefers projects without builds first' do
# it gets for one build from each of the projects
@@ -287,9 +287,9 @@ module Ci
shared_examples 'validation is active' do
context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
- it_behaves_like 'not pick'
+ it { expect(subject).to eq(pending_job) }
end
context 'when artifacts of depended job has been expired' do
@@ -307,15 +307,27 @@ module Ci
it_behaves_like 'not pick'
end
+
+ context 'when job object is staled' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:drop!)
+ .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ end
+
+ it 'does not drop nor pick' do
+ expect(subject).to be_nil
+ end
+ end
end
shared_examples 'validation is not active' do
context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
it { expect(subject).to eq(pending_job) }
end
-
context 'when artifacts of depended job has been expired' do
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 9e02644ae0a..ea5d1f67b0c 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -41,7 +41,7 @@ describe Ci::RetryBuildService do
description: 'my-job', stage: 'test', pipeline: pipeline,
auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build|
##
- # TODO, workaround for FactoryGirl limitation when having both
+ # TODO, workaround for FactoryBot limitation when having both
# stage (text) and stage_id (integer) columns in the table.
build.stage_id = stage.id
end
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index e2a9ed27e87..3fc4e499b0c 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -15,7 +15,7 @@ describe Ci::StopEnvironmentsService do
context 'when user has permission to stop environment' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when environment is associated with removed branch' do
@@ -57,7 +57,7 @@ describe Ci::StopEnvironmentsService do
context 'when user does not have permission to stop environment' do
context 'when user has no access to manage deployments' do
before do
- project.team << [user, :guest]
+ project.add_guest(user)
end
it 'does not stop environment' do
@@ -86,7 +86,7 @@ describe Ci::StopEnvironmentsService do
context 'when user has permission to stop environments' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'does not stop environment' do
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
index 75fc05d36e9..6894c1797b0 100644
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Clusters::Applications::CheckInstallationProgressService do
RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
- let(:application) { create(:cluster_applications_helm, :installing) }
+ let(:application) { create(:clusters_applications_helm, :installing) }
let(:service) { described_class.new(application) }
let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
let(:errors) { nil }
@@ -33,7 +33,7 @@ describe Clusters::Applications::CheckInstallationProgressService do
end
context 'when timeouted' do
- let(:application) { create(:cluster_applications_helm, :timeouted) }
+ let(:application) { create(:clusters_applications_helm, :timeouted) }
it_behaves_like 'a terminated installation'
diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb
index 054a49ffedf..ad175226e92 100644
--- a/spec/services/clusters/applications/install_service_spec.rb
+++ b/spec/services/clusters/applications/install_service_spec.rb
@@ -2,17 +2,19 @@ require 'spec_helper'
describe Clusters::Applications::InstallService do
describe '#execute' do
- let(:application) { create(:cluster_applications_helm, :scheduled) }
+ let(:application) { create(:clusters_applications_helm, :scheduled) }
+ let!(:install_command) { application.install_command }
let(:service) { described_class.new(application) }
- let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm) }
+ let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
before do
+ allow(service).to receive(:install_command).and_return(install_command)
allow(service).to receive(:helm_api).and_return(helm_client)
end
context 'when there are no errors' do
before do
- expect(helm_client).to receive(:install).with(application.install_command)
+ expect(helm_client).to receive(:install).with(install_command)
allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
end
@@ -33,7 +35,7 @@ describe Clusters::Applications::InstallService do
context 'when k8s cluster communication fails' do
before do
error = KubeException.new(500, 'system failure', nil)
- expect(helm_client).to receive(:install).with(application.install_command).and_raise(error)
+ expect(helm_client).to receive(:install).with(install_command).and_raise(error)
end
it 'make the application errored' do
@@ -45,7 +47,7 @@ describe Clusters::Applications::InstallService do
end
context 'when application cannot be persisted' do
- let(:application) { build(:cluster_applications_helm, :scheduled) }
+ let(:application) { build(:clusters_applications_helm, :scheduled) }
it 'make the application errored' do
expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid)
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
index 047a6e44dab..473dbcbb692 100644
--- a/spec/services/clusters/applications/schedule_installation_service_spec.rb
+++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb
@@ -34,7 +34,7 @@ describe Clusters::Applications::ScheduleInstallationService do
end
context 'when installation is already in progress' do
- let(:application) { create(:cluster_applications_helm, :installing) }
+ let(:application) { create(:clusters_applications_helm, :installing) }
let(:cluster) { application.cluster }
it_behaves_like 'a failing service'
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 08267d6e6a0..b9bfbb11511 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -266,7 +266,7 @@ describe CreateDeploymentService do
context "while updating the 'first_deployed_to_production_at' time" do
before do
- merge_request.mark_as_merged
+ merge_request.metrics.update!(merged_at: Time.now)
end
context "for merge requests merged before the current deploy" do
diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb
index 19855c9bee2..9c9fba030e7 100644
--- a/spec/services/delete_branch_service_spec.rb
+++ b/spec/services/delete_branch_service_spec.rb
@@ -9,7 +9,7 @@ describe DeleteBranchService do
describe '#execute' do
context 'when user has access to push to repository' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'removes the branch' do
diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb
index ab8df7b74cd..3895a0b3aea 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.team << [user, :master]
+ project.add_master(user)
end
it "doesn't resolve discussions the user can't resolve" do
diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb
new file mode 100644
index 00000000000..ace5f293097
--- /dev/null
+++ b/spec/services/files/delete_service_spec.rb
@@ -0,0 +1,64 @@
+require "spec_helper"
+
+describe Files::DeleteService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:branch_name) { project.default_branch }
+ let(:last_commit_sha) { nil }
+
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Delete File",
+ last_commit_sha: last_commit_sha,
+ start_project: project,
+ start_branch: project.default_branch,
+ branch_name: branch_name
+ }
+ end
+
+ shared_examples 'successfully deletes the file' do
+ it 'returns a hash with the :success status' do
+ results = subject.execute
+
+ expect(results[:status]).to match(:success)
+ end
+
+ it 'deletes the file' do
+ subject.execute
+
+ blob = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(blob).to be_nil
+ end
+ end
+
+ before do
+ project.add_master(user)
+ end
+
+ describe "#execute" do
+ context "when the file's last commit sha does not match the supplied last_commit_sha" do
+ let(:last_commit_sha) { "foo" }
+
+ it "returns a hash with the correct error message and a :error status " do
+ expect { subject.execute }
+ .to raise_error(Files::UpdateService::FileChangedError,
+ "You are attempting to delete a file that has been previously updated.")
+ end
+ end
+
+ context "when the file's last commit sha does match the supplied last_commit_sha" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha }
+
+ it_behaves_like 'successfully deletes the file'
+ end
+
+ context "when the last_commit_sha is not supplied" do
+ it_behaves_like 'successfully deletes the file'
+ end
+ end
+end
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
new file mode 100644
index 00000000000..b9971776b33
--- /dev/null
+++ b/spec/services/files/multi_service_spec.rb
@@ -0,0 +1,144 @@
+require "spec_helper"
+
+describe Files::MultiService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:branch_name) { project.default_branch }
+ let(:original_file_path) { 'files/ruby/popen.rb' }
+ let(:new_file_path) { 'files/ruby/popen.rb' }
+ let(:action) { 'update' }
+
+ let!(:original_commit_id) do
+ Gitlab::Git::Commit.last_for_path(project.repository, branch_name, original_file_path).sha
+ end
+
+ let(:actions) do
+ [
+ {
+ action: action,
+ file_path: new_file_path,
+ previous_path: original_file_path,
+ content: 'New content',
+ last_commit_id: original_commit_id
+ }
+ ]
+ end
+
+ let(:commit_params) do
+ {
+ commit_message: "Update File",
+ branch_name: branch_name,
+ start_branch: branch_name,
+ actions: actions
+ }
+ end
+
+ before do
+ project.add_master(user)
+ end
+
+ describe '#execute' do
+ context 'with a valid action' do
+ it 'returns a hash with the :success status' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:success)
+ end
+ end
+
+ context 'with an invalid action' do
+ let(:action) { 'rename' }
+
+ it 'returns a hash with the :error status' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(/Unknown action/)
+ end
+ end
+
+ describe 'Updating files' do
+ context 'when the file has been previously updated' do
+ before do
+ update_file(original_file_path)
+ end
+
+ it 'rejects the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(new_file_path)
+ end
+ end
+
+ context 'when the file have not been modified' do
+ it 'accepts the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:success)
+ end
+ end
+ end
+
+ context 'when moving a file' do
+ let(:action) { 'move' }
+ let(:new_file_path) { 'files/ruby/new_popen.rb' }
+
+ context 'when original file has been updated' do
+ before do
+ update_file(original_file_path)
+ end
+
+ it 'rejects the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(original_file_path)
+ end
+ end
+
+ context 'when original file have not been updated' do
+ it 'moves the file' do
+ results = subject.execute
+ blob = project.repository.blob_at_branch(branch_name, new_file_path)
+
+ expect(results[:status]).to eq(:success)
+ expect(blob).to be_present
+ end
+ end
+ end
+
+ context 'when file status validation is skipped' do
+ let(:action) { 'create' }
+ let(:new_file_path) { 'files/ruby/new_file.rb' }
+
+ it 'does not check the last commit' do
+ expect(Gitlab::Git::Commit).not_to receive(:last_for_path)
+
+ subject.execute
+ end
+
+ it 'creates the file' do
+ subject.execute
+
+ blob = project.repository.blob_at_branch(branch_name, new_file_path)
+
+ expect(blob).to be_present
+ end
+ end
+ end
+
+ def update_file(path)
+ params = {
+ file_path: path,
+ start_branch: branch_name,
+ branch_name: branch_name,
+ commit_message: 'Update file',
+ file_content: 'New content'
+ }
+
+ Files::UpdateService.new(project, user, params).execute
+ end
+end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index 2b4f8cd42ee..43b0c9a63a9 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.team << [user, :master]
+ project.add_master(user)
end
describe "#execute" do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index cc3d4e7da49..26fdf8d4b24 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.team << [user, :master]
+ project.add_master(user)
end
describe 'Push branches' do
@@ -266,8 +266,8 @@ describe GitPushService, services: true do
let(:commit) { project.commit }
before do
- project.team << [commit_author, :developer]
- project.team << [user, :developer]
+ project.add_developer(commit_author)
+ project.add_developer(user)
allow(commit).to receive_messages(
safe_message: "this commit \n mentions #{issue.to_reference}",
@@ -323,8 +323,8 @@ describe GitPushService, services: true do
let(:commit_time) { Time.now }
before do
- project.team << [commit_author, :developer]
- project.team << [user, :developer]
+ project.add_developer(commit_author)
+ project.add_developer(user)
allow(commit).to receive_messages(
safe_message: "this commit \n mentions #{issue.to_reference}",
@@ -376,7 +376,7 @@ describe GitPushService, services: true do
allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit)
.and_return(closing_commit)
- project.team << [commit_author, :master]
+ project.add_master(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 05695aa8188..33405d7a7ec 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -35,7 +35,7 @@ describe GitTagPushService do
before do
stub_ci_pipeline_to_return_yaml_file
- project.team << [user, :developer]
+ project.add_developer(user)
end
it "creates a new pipeline" do
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index bdaab88d673..53c85f73cde 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -54,7 +54,7 @@ describe Issuable::BulkUpdateService do
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
new_assignee = create(:user)
- project.team << [new_assignee, :developer]
+ project.add_developer(new_assignee)
result = bulk_update(merge_request, assignee_id: new_assignee.id)
@@ -64,7 +64,7 @@ describe Issuable::BulkUpdateService do
it 'updates the assignee to the user ID passed' do
assignee = create(:user)
- project.team << [assignee, :developer]
+ project.add_developer(assignee)
expect { bulk_update(merge_request, assignee_id: assignee.id) }
.to change { merge_request.reload.assignee }.from(user).to(assignee)
@@ -92,7 +92,7 @@ describe Issuable::BulkUpdateService do
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
new_assignee = create(:user)
- project.team << [new_assignee, :developer]
+ project.add_developer(new_assignee)
result = bulk_update(issue, assignee_ids: [new_assignee.id])
@@ -102,7 +102,7 @@ describe Issuable::BulkUpdateService do
it 'updates the assignee to the user ID passed' do
assignee = create(:user)
- project.team << [assignee, :developer]
+ project.add_developer(assignee)
expect { bulk_update(issue, assignee_ids: [assignee.id]) }
.to change { issue.reload.assignees.first }.from(user).to(assignee)
end
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
index d74d98c6079..0a3647a814f 100644
--- a/spec/services/issuable/destroy_service_spec.rb
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Issuable::DestroyService do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
subject(:service) { described_class.new(project, user) }
@@ -19,6 +19,13 @@ describe Issuable::DestroyService do
service.execute(issue)
end
+
+ it 'updates the todo caches for users with todos on the issue' do
+ create(:todo, target: issue, user: user, author: user, project: project)
+
+ expect { service.execute(issue) }
+ .to change { user.todos_pending_count }.from(1).to(0)
+ end
end
context 'when issuable is a merge request' do
@@ -33,6 +40,13 @@ describe Issuable::DestroyService do
service.execute(merge_request)
end
+
+ it 'updates the todo caches for users with todos on the merge request' do
+ create(:todo, target: merge_request, user: user, author: user, project: project)
+
+ expect { service.execute(merge_request) }
+ .to change { user.todos_pending_count }.from(1).to(0)
+ end
end
end
end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 03f76bd428d..248e7d5a389 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -5,7 +5,7 @@ describe Issues::BuildService do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'for a single discussion' do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 5c27e8fd561..8897a64a138 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -9,9 +9,9 @@ describe Issues::CloseService do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
- project.team << [guest, :guest]
+ project.add_master(user)
+ project.add_developer(user2)
+ project.add_guest(guest)
end
describe '#execute' do
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index d86da244520..79bcdc41fb0 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.team << [user, :master]
- project.team << [assignee, :master]
+ project.add_master(user)
+ project.add_master(assignee)
end
let(:opts) do
@@ -43,7 +43,7 @@ describe Issues::CreateService do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
end
it 'filters out params that cannot be set without the :admin_issue permission' do
@@ -130,7 +130,7 @@ describe Issues::CreateService do
end
it 'invalidates open issues counter for assignees when issue is assigned' do
- project.team << [assignee, :master]
+ project.add_master(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.team << [user, :master]
+ project.add_master(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.team << [assignee, :master]
+ project.add_master(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.team << [user, :master]
- project.team << [assignee, :master]
+ project.add_master(user)
+ project.add_master(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.team << [user, :master]
+ project.add_master(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 f2b35a8fadf..53ea88332fb 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -20,8 +20,8 @@ describe Issues::MoveService do
shared_context 'user can move issue' do
before do
- old_project.team << [user, :reporter]
- new_project.team << [user, :reporter]
+ old_project.add_reporter(user)
+ new_project.add_reporter(user)
labels = Array.new(2) { |x| "label%d" % (x + 1) }
@@ -289,6 +289,18 @@ describe Issues::MoveService do
.to raise_error(StandardError, /Cannot move issue/)
end
end
+
+ context 'project issue hooks' do
+ let(:hook) { create(:project_hook, project: old_project, issues_events: true) }
+
+ it 'executes project issue hooks' do
+ # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1,
+ # but since the entire spec run takes place in a transaction, we never
+ # actually get to the `after_commit` hook that queues these jobs.
+ expect { move_service.execute(old_issue, new_project) }
+ .not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
+ end
+ end
end
describe 'move permissions' do
@@ -301,7 +313,7 @@ describe Issues::MoveService do
context 'user is reporter only in new project' do
before do
- new_project.team << [user, :reporter]
+ new_project.add_reporter(user)
end
it { expect { move }.to raise_error(StandardError, /permissions/) }
@@ -309,7 +321,7 @@ describe Issues::MoveService do
context 'user is reporter only in old project' do
before do
- old_project.team << [user, :reporter]
+ old_project.add_reporter(user)
end
it { expect { move }.to raise_error(StandardError, /permissions/) }
@@ -317,8 +329,8 @@ describe Issues::MoveService do
context 'user is reporter in one project and guest in another' do
before do
- new_project.team << [user, :guest]
- old_project.team << [user, :reporter]
+ new_project.add_guest(user)
+ old_project.add_reporter(user)
end
it { expect { move }.to raise_error(StandardError, /permissions/) }
@@ -346,8 +358,8 @@ describe Issues::MoveService do
context 'movable issue with no assigned labels' do
before do
- old_project.team << [user, :reporter]
- new_project.team << [user, :reporter]
+ old_project.add_reporter(user)
+ new_project.add_reporter(user)
labels = Array.new(2) { |x| "label%d" % (x + 1) }
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 48fc98b3b2f..42e5d544f4c 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -8,7 +8,7 @@ describe Issues::ReopenService do
context 'when user is not authorized to reopen issue' do
before do
guest = create(:user)
- project.team << [guest, :guest]
+ project.add_guest(guest)
perform_enqueued_jobs do
described_class.new(project, guest).execute(issue)
@@ -24,7 +24,7 @@ describe Issues::ReopenService do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(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 67a86c50fc1..13accc6ae1b 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -14,7 +14,7 @@ describe Issues::ResolveDiscussions do
let(:user) { create(:user) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
describe "for resolving discussions" do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index f07b81e842a..1cb6f2e097f 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -17,9 +17,9 @@ describe Issues::UpdateService, :mailer do
end
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
- project.team << [user3, :developer]
+ project.add_master(user)
+ project.add_developer(user2)
+ project.add_developer(user3)
end
describe 'execute' do
@@ -99,7 +99,7 @@ describe Issues::UpdateService, :mailer do
context 'when current user cannot admin issues in the project' do
let(:guest) { create(:user) }
before do
- project.team << [guest, :guest]
+ project.add_guest(guest)
end
it 'filters out params that cannot be set without the :admin_issue permission' do
@@ -318,7 +318,7 @@ describe Issues::UpdateService, :mailer do
let!(:subscriber) do
create(:user).tap do |u|
label.toggle_subscription(u, project)
- project.team << [u, :developer]
+ project.add_developer(u)
end
end
@@ -556,7 +556,7 @@ describe Issues::UpdateService, :mailer do
context 'valid project' do
before do
- target_project.team << [user, :master]
+ target_project.add_master(user)
end
it 'calls the move service with the proper issue and project' do
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
index a781fbc7f7d..78aa5d442e7 100644
--- a/spec/services/labels/find_or_create_service_spec.rb
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -17,7 +17,7 @@ describe Labels::FindOrCreateService do
let(:user) { create(:user) }
subject(:service) { described_class.new(user, project, params) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
context 'when label does not exist at group level' do
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 302c488d6c6..b3018169a1c 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -123,7 +123,7 @@ describe Members::ApproveAccessRequestService do
context 'when current user can approve access request to the project' do
before do
- project.team << [user, :master]
+ project.add_master(user)
group.add_owner(user)
end
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
index 2d04d824180..757c45708b9 100644
--- a/spec/services/members/authorized_destroy_service_spec.rb
+++ b/spec/services/members/authorized_destroy_service_spec.rb
@@ -13,7 +13,7 @@ describe Members::AuthorizedDestroyService do
context 'Invited users' do
# Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504
it 'destroys invited project member' do
- project.team << [member_user, :developer]
+ project.add_developer(member_user)
member = create :project_member, :invited, project: project
@@ -45,14 +45,14 @@ describe Members::AuthorizedDestroyService do
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
- expect(issue.reload.assignee_id).to be_nil
+ expect(issue.reload.assignee_ids).to be_empty
expect(merge_request.reload.assignee_id).to be_nil
end
end
context 'Project member' do
it "unassigns issues and merge requests" do
- project.team << [member_user, :developer]
+ project.add_developer(member_user)
create :issue, project: project, assignees: [member_user]
create :merge_request, target_project: project, source_project: project, assignee: member_user
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 2a793e0eb4d..6bd4718e780 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.team << [user, :master]
+ project.add_master(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 72f5e27180d..91152df3ad9 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -71,7 +71,7 @@ describe Members::DestroyService do
context 'when a member is found' do
before do
- project.team << [member_user, :developer]
+ project.add_developer(member_user)
group.add_developer(member_user)
end
let(:params) { { user_id: member_user.id } }
@@ -88,7 +88,7 @@ describe Members::DestroyService do
context 'when current user can destroy the given member' do
before do
- project.team << [user, :master]
+ project.add_master(user)
group.add_owner(user)
end
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index fcbe0e5985f..bda6383a346 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -8,7 +8,7 @@ describe MergeRequests::AssignIssuesService do
let(:service) { described_class.new(project, user, merge_request: merge_request) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'finds unassigned issues fixed in merge request' do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index b5c92e681fb..a9605c6e4c6 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -28,7 +28,7 @@ describe MergeRequests::BuildService do
end
before do
- project.team << [user, :guest]
+ project.add_guest(user)
end
def stub_compare
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index b3886987316..4d12de3ecce 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -9,9 +9,9 @@ describe MergeRequests::CloseService do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
- project.team << [guest, :guest]
+ project.add_master(user)
+ project.add_developer(user2)
+ project.add_guest(guest)
end
describe '#execute' do
@@ -52,6 +52,19 @@ describe MergeRequests::CloseService do
end
end
+ it 'updates metrics' do
+ metrics = merge_request.metrics
+ metrics_service = double(MergeRequestMetricsService)
+ allow(MergeRequestMetricsService)
+ .to receive(:new)
+ .with(metrics)
+ .and_return(metrics_service)
+
+ expect(metrics_service).to receive(:close)
+
+ described_class.new(project, user, {}).execute(merge_request)
+ end
+
it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
service = described_class.new(project, user, {})
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
index 0b32c51a16f..6cadcd438c3 100644
--- a/spec/services/merge_requests/conflicts/list_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -32,14 +32,6 @@ describe MergeRequests::Conflicts::ListService do
expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
end
- 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)
-
- expect(service.can_be_resolved_in_ui?).to be_falsey
- end
-
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)
@@ -76,5 +68,23 @@ describe MergeRequests::Conflicts::ListService do
expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy
end
+
+ 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(Gitlab::GitalyClient::ConflictsService).to receive(:list_conflict_files).and_raise(GRPC::Unknown)
+
+ 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)
+
+ expect(service.can_be_resolved_in_ui?).to be_falsey
+ end
+ 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 5376083e7f5..cff09237005 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -111,15 +111,6 @@ describe MergeRequests::Conflicts::ResolveService do
described_class.new(merge_request_from_fork).execute(user, params)
end
- it 'gets conflicts from the source project' do
- # REFACTOR NOTE: We used to test that `project.repository.rugged` wasn't
- # used in this case, but since the refactor, for simplification,
- # we always use that repository for read only operations.
- expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original
-
- subject
- end
-
it 'creates a commit with the message' do
subject
@@ -132,6 +123,17 @@ 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
@@ -213,7 +215,7 @@ describe MergeRequests::Conflicts::ResolveService do
MergeRequests::Conflicts::ListService.new(merge_request).conflicts.resolver
end
let(:regex_conflict) do
- resolver.conflict_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb')
+ resolver.conflict_for_path(resolver.conflicts, 'files/ruby/regex.rb', 'files/ruby/regex.rb')
end
let(:invalid_params) do
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index a7ab389b357..623b182b205 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -100,5 +100,17 @@ describe MergeRequests::CreateFromIssueService do
expect(result[:merge_request].target_branch).to eq(project.default_branch)
end
+
+ it 'executes quick actions if the build service sets them in the description' do
+ allow(service).to receive(:merge_request).and_wrap_original do |m, *args|
+ m.call(*args).tap do |merge_request|
+ merge_request.description = "/assign #{user.to_reference}"
+ end
+ end
+
+ result = service.execute
+
+ expect(result[:merge_request].assignee).to eq(user)
+ end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index a047f891ab2..dd8c803a2f7 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -21,8 +21,8 @@ describe MergeRequests::CreateService do
let(:merge_request) { service.execute }
before do
- project.team << [user, :master]
- project.team << [assignee, :developer]
+ project.add_master(user)
+ project.add_developer(assignee)
allow(service).to receive(:execute_hooks)
end
@@ -148,8 +148,8 @@ describe MergeRequests::CreateService do
end
before do
- project.team << [user, :master]
- project.team << [assignee, :master]
+ project.add_master(user)
+ project.add_master(assignee)
end
it 'assigns and sets milestone to issuable from command' do
@@ -165,7 +165,7 @@ describe MergeRequests::CreateService do
let(:assignee) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
end
it 'removes assignee_id when user id is invalid' do
@@ -185,7 +185,7 @@ describe MergeRequests::CreateService do
end
it 'saves assignee when user id is valid' do
- project.team << [assignee, :master]
+ project.add_master(assignee)
opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
merge_request = described_class.new(project, user, opts).execute
@@ -205,7 +205,7 @@ describe MergeRequests::CreateService do
end
it 'invalidates open merge request counter for assignees when merge request is assigned' do
- project.team << [assignee, :master]
+ project.add_master(assignee)
described_class.new(project, user, opts).execute
@@ -249,8 +249,8 @@ describe MergeRequests::CreateService do
end
before do
- project.team << [user, :master]
- project.team << [assignee, :developer]
+ project.add_master(user)
+ project.add_developer(assignee)
end
it 'creates a `MergeRequestsClosingIssues` record for each issue' do
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index aaabf3ed2b0..aa90feeef89 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -12,8 +12,8 @@ describe MergeRequests::FfMergeService do
let(:project) { merge_request.project }
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
+ project.add_master(user)
+ project.add_developer(user2)
end
describe '#execute' do
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index f86f1ac2443..c38ddf4612b 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
describe MergeRequests::MergeService do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) }
let(:project) { merge_request.project }
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
+ project.add_master(user)
+ project.add_developer(user2)
end
describe '#execute' do
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index d2bd05d921f..70957431942 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.team << [user, :master]
+ project.add_master(user)
end
describe '#execute' do
@@ -22,5 +22,18 @@ describe MergeRequests::PostMergeService do
expect { service.execute(merge_request) }
.to change { project.open_merge_requests_count }.from(1).to(0)
end
+
+ it 'updates metrics' do
+ metrics = merge_request.metrics
+ metrics_service = double(MergeRequestMetricsService)
+ allow(MergeRequestMetricsService)
+ .to receive(:new)
+ .with(metrics)
+ .and_return(metrics_service)
+
+ expect(metrics_service).to receive(:merge)
+
+ described_class.new(project, user, {}).execute(merge_request)
+ end
end
end
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
new file mode 100644
index 00000000000..d1b37cdd073
--- /dev/null
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe MergeRequests::RebaseService do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:merge_request) do
+ create(:merge_request,
+ source_branch: 'feature_conflict',
+ target_branch: 'master')
+ end
+ let(:project) { merge_request.project }
+ let(:repository) { project.repository.raw }
+
+ subject(:service) { described_class.new(project, user, {}) }
+
+ before do
+ project.add_master(user)
+ end
+
+ describe '#execute' do
+ context 'when another rebase is already in progress' do
+ before do
+ allow(merge_request).to receive(:rebase_in_progress?).and_return(true)
+ end
+
+ it 'saves the error message' do
+ subject.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress'
+ end
+
+ it 'returns an error' do
+ expect(service.execute(merge_request)).to match(status: :error,
+ message: 'Failed to rebase. Should be done manually')
+ end
+ end
+
+ context 'when unexpected error occurs' do
+ before do
+ allow(repository).to receive(:run_git!).and_raise('Something went wrong')
+ end
+
+ it 'saves the error message' do
+ subject.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq 'Something went wrong'
+ end
+
+ it 'returns an error' do
+ expect(service.execute(merge_request)).to match(status: :error,
+ message: 'Failed to rebase. Should be done manually')
+ end
+ end
+
+ context 'with git command failure' do
+ before do
+ allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong')
+ end
+
+ it 'saves the error message' do
+ subject.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq 'Something went wrong'
+ end
+
+ it 'returns an error' do
+ expect(service.execute(merge_request)).to match(status: :error,
+ message: 'Failed to rebase. Should be done manually')
+ end
+ end
+
+ context 'valid params' do
+ before do
+ service.execute(merge_request)
+ end
+
+ it 'rebases source branch' do
+ parent_sha = merge_request.source_project.repository.commit(merge_request.source_branch).parents.first.sha
+ target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha
+ expect(parent_sha).to eq(target_branch_sha)
+ end
+
+ it 'records the new SHA on the merge request' do
+ head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
+ expect(merge_request.reload.rebase_commit_sha).to eq(head_sha)
+ end
+
+ it 'logs correct author and commiter' do
+ head_commit = merge_request.source_project.repository.commit(merge_request.source_branch)
+
+ expect(head_commit.author_email).to eq('dmitriy.zaporozhets@gmail.com')
+ expect(head_commit.author_name).to eq('Dmitriy Zaporozhets')
+ expect(head_commit.committer_email).to eq(user.email)
+ expect(head_commit.committer_name).to eq(user.name)
+ end
+
+ context 'git commands' 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'))
+ .and_return(['', 0])
+
+ service.execute(merge_request)
+ end
+ end
+
+ context 'fork' do
+ let(:forked_project) do
+ fork_project(project, user, repository: true)
+ end
+
+ let(:merge_request_from_fork) do
+ forked_project.repository.create_file(
+ user,
+ 'new-file-to-target',
+ '',
+ message: 'Add new file to target',
+ branch_name: 'master')
+
+ create(:merge_request,
+ source_branch: 'master', source_project: forked_project,
+ target_branch: 'master', target_project: project)
+ end
+
+ it 'rebases source branch' do
+ parent_sha = forked_project.repository.commit(merge_request_from_fork.source_branch).parents.first.sha
+ target_branch_sha = project.repository.commit(merge_request_from_fork.target_branch).sha
+ expect(parent_sha).to eq(target_branch_sha)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 61ec4709c59..7a01d3dd698 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -300,8 +300,8 @@ describe MergeRequests::RefreshService do
let(:commit) { project.commit }
before do
- project.team << [commit_author, :developer]
- project.team << [user, :developer]
+ project.add_developer(commit_author)
+ project.add_developer(user)
allow(commit).to receive_messages(
safe_message: "Closes #{issue.to_reference}",
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index fa652611c6b..a44d63e5f9f 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -8,9 +8,9 @@ describe MergeRequests::ReopenService do
let(:project) { merge_request.project }
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
- project.team << [guest, :guest]
+ project.add_master(user)
+ project.add_developer(user2)
+ project.add_guest(guest)
end
describe '#execute' do
@@ -47,6 +47,19 @@ describe MergeRequests::ReopenService do
end
end
+ it 'updates metrics' do
+ metrics = merge_request.metrics
+ service = double(MergeRequestMetricsService)
+ allow(MergeRequestMetricsService)
+ .to receive(:new)
+ .with(metrics)
+ .and_return(service)
+
+ expect(service).to receive(:reopen)
+
+ described_class.new(project, user, {}).execute(merge_request)
+ end
+
it 'refreshes the number of open merge requests for a valid MR' do
service = described_class.new(project, user, {})
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 7a66b809550..2238da2d14d 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -16,9 +16,9 @@ describe MergeRequests::UpdateService, :mailer do
end
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
- project.team << [user3, :developer]
+ project.add_master(user)
+ project.add_developer(user2)
+ project.add_developer(user3)
end
describe 'execute' do
@@ -356,8 +356,8 @@ describe MergeRequests::UpdateService, :mailer do
let!(:subscriber) { create(:user) { |u| label.toggle_subscription(u, project) } }
before do
- project.team << [non_subscriber, :developer]
- project.team << [subscriber, :developer]
+ project.add_developer(non_subscriber)
+ project.add_developer(subscriber)
end
it 'sends notifications for subscribers of newly added labels' do
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 2bdf224804d..adad73f7e11 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.team << [user, :master]
+ project.add_master(user)
end
describe '#execute' do
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
index 8837b91051d..f2a18c7295a 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.team << [user, :master]
+ project.add_master(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 af35e17bfa7..9703780b0e9 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.team << [user, :master]
+ project.add_master(user)
end
def service
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 661d26946e7..0ae26e87154 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.team << [user, :master]
+ project.add_master(user)
end
context "valid params" do
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index c9a99a43edb..64445be560e 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -1,15 +1,25 @@
require 'spec_helper'
describe Notes::DestroyService do
+ set(:project) { create(:project, :public) }
+ set(:issue) { create(:issue, project: project) }
+ let(:user) { issue.author }
+
describe '#execute' do
it 'deletes a note' do
- project = create(:project)
- issue = create(:issue, project: project)
note = create(:note, project: project, noteable: issue)
- described_class.new(project, note.author).execute(note)
+ described_class.new(project, user).execute(note)
expect(project.issues.find(issue.id).notes).not_to include(note)
end
+
+ it 'updates the todo counts for users with todos for the note' do
+ note = create(:note, project: project, noteable: issue)
+ create(:todo, note: note, target: issue, user: user, author: user, project: project)
+
+ expect { described_class.new(project, user).execute(note) }
+ .to change { user.todos_pending_count }.from(1).to(0)
+ end
end
end
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index a2b3638059f..6ef5e93cb20 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.team << [user, :master]
+ project.add_master(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 0280a19098b..5eafe56c99d 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.team << [u, :master] } }
+ let(:master) { create(:user).tap { |u| project.add_master(u) } }
let(:assignee) { create(:user) }
before do
- project.team << [assignee, :master]
+ project.add_master(assignee)
end
end
@@ -226,7 +226,7 @@ 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.team << [u, :master] } }
+ let(:master) { create(:user).tap { |u| project.add_master(u) } }
let(:assignee) { create(:user) }
let(:master) { create(:user) }
let(:service) { described_class.new(project, master) }
@@ -237,8 +237,8 @@ describe Notes::QuickActionsService do
end
before do
- project.team << [master, :master]
- project.team << [assignee, :master]
+ project.add_master(master)
+ project.add_master(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 3210539f3ee..65b1d613998 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -9,9 +9,9 @@ describe Notes::UpdateService do
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
- project.team << [user3, :developer]
+ project.add_master(user)
+ project.add_developer(user2)
+ project.add_developer(user3)
end
describe '#execute' do
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 426593be428..7a8c54673f7 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -34,7 +34,7 @@ describe Projects::AutocompleteService do
end
it 'does not list project confidential issues for project members with guest role' do
- project.team << [member, :guest]
+ project.add_guest(member)
autocomplete = described_class.new(project, non_member)
issues = autocomplete.issues.map(&:iid)
@@ -66,7 +66,7 @@ describe Projects::AutocompleteService do
end
it 'lists project confidential issues for project members' do
- project.team << [member, :developer]
+ project.add_developer(member)
autocomplete = described_class.new(project, member)
issues = autocomplete.issues.map(&:iid)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index dc89fdebce7..9a44dfde41b 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -252,6 +252,12 @@ describe Projects::CreateService, '#execute' do
end
end
+ it 'writes project full path to .git/config' do
+ project = create_project(user, opts)
+
+ expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path
+ end
+
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 4057caca2ac..409d5de8d43 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -139,10 +139,10 @@ describe Projects::ForkService do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
end
- it "creates fork with highest allowed level" do
+ it "creates fork with lowest level" do
forked_project = fork_project(@from_project, @to_user)
- expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
@@ -209,6 +209,19 @@ describe Projects::ForkService do
expect(to_project.errors[:path]).to eq(['has already been taken'])
end
end
+
+ context 'when the namespace has a lower visibility level than the project' do
+ it 'creates the project with the lower visibility level' do
+ public_project = create(:project, :public)
+ private_group = create(:group, :private)
+ group_owner = create(:user)
+ private_group.add_owner(group_owner)
+
+ forked_project = fork_project(public_project, group_owner, namespace: private_group)
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
end
end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 3a3e47fd9c0..7b536cc05cb 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::HashedStorage::MigrateRepositoryService do
let(:gitlab_shell) { Gitlab::Shell.new }
- let(:project) { create(:project, :empty_repo, :wiki_repo) }
+ let(:project) { create(:project, :repository, :wiki_repo) }
let(:service) { described_class.new(project) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
@@ -33,6 +33,12 @@ describe Projects::HashedStorage::MigrateRepositoryService do
service.execute
end
+
+ it 'writes project full path to .git/config' do
+ service.execute
+
+ expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path
+ end
end
context 'when one move fails' do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 2b1337bee7e..39f6388c25e 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -54,6 +54,12 @@ describe Projects::TransferService do
expect(project.disk_path).not_to eq(old_path)
expect(project.disk_path).to start_with(group.path)
end
+
+ 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}"
+ end
end
context 'when transfer fails' do
@@ -86,6 +92,12 @@ describe Projects::TransferService do
expect(original_path).to eq current_path
end
+ 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
+ end
+
it "doesn't send move notifications" do
expect_any_instance_of(NotificationService).not_to receive(:project_was_moved)
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 2bba71fef4f..3ec6139bfa6 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -62,6 +62,26 @@ describe Projects::UnlinkForkService do
expect(source.forks_count).to be_zero
end
+ context 'when the source has LFS objects' do
+ let(:lfs_object) { create(:lfs_object) }
+
+ before do
+ lfs_object.projects << project
+ end
+
+ it 'links the fork to the lfs object before unlinking' do
+ subject.execute
+
+ expect(lfs_object.projects).to include(forked_project)
+ end
+
+ it 'does not fail if the lfs objects were already linked' do
+ lfs_object.projects << forked_project
+
+ expect { subject.execute }.not_to raise_error
+ end
+ end
+
context 'when the original project was deleted' do
it 'does not fail when the original project is deleted' do
source = forked_project.forked_from_project
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index d887f70efae..fc6aa713d6f 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -61,7 +61,7 @@ describe Projects::UpdateService do
end
end
- context 'When project visibility is higher than parent group' do
+ context 'when project visibility is higher than parent group' do
let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
before do
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 835e83d6dba..53b3e5e365d 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -19,5 +19,21 @@ describe ProtectedBranches::CreateService do
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])
end
+
+ context 'when user does not have permission' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'creates a new protected branch if we skip authorization step' do
+ expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1)
+ end
+
+ it 'raises Gitlab::Access:AccessDeniedError' do
+ expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index c35177f6ebc..ae160d104f1 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -12,7 +12,7 @@ describe QuickActions::InterpretService do
let(:service) { described_class.new(project, developer) }
before do
- project.team << [developer, :developer]
+ project.add_developer(developer)
end
describe '#execute' do
@@ -209,7 +209,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: 3600,
- user: developer,
+ user_id: developer.id,
spent_at: DateTime.now.to_date
})
end
@@ -221,7 +221,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: -1800,
- user: developer,
+ user_id: developer.id,
spent_at: DateTime.now.to_date
})
end
@@ -233,7 +233,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: 1800,
- user: developer,
+ user_id: developer.id,
spent_at: Date.parse(date)
})
end
@@ -267,7 +267,7 @@ describe QuickActions::InterpretService do
it 'populates spend_time: :reset if content contains /remove_time_spent' do
_, updates = service.execute(content, issuable)
- expect(updates).to eq(spend_time: { duration: :reset, user: developer })
+ expect(updates).to eq(spend_time: { duration: :reset, user_id: developer.id })
end
end
@@ -440,7 +440,7 @@ describe QuickActions::InterpretService do
let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
before do
- project.team << [developer2, :developer]
+ project.add_developer(developer2)
end
context 'Issue' do
diff --git a/spec/services/reset_project_cache_service_spec.rb b/spec/services/reset_project_cache_service_spec.rb
new file mode 100644
index 00000000000..de475d16586
--- /dev/null
+++ b/spec/services/reset_project_cache_service_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ResetProjectCacheService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(project, user).execute }
+
+ context 'when project cache_index is nil' do
+ before do
+ project.jobs_cache_index = nil
+ end
+
+ it 'sets project cache_index to one' do
+ expect { subject }.to change { project.reload.jobs_cache_index }.from(nil).to(1)
+ end
+ end
+
+ context 'when project cache_index is a numeric value' do
+ before do
+ project.update_attributes(jobs_cache_index: 1)
+ end
+
+ it 'increments project cache index' do
+ expect { subject }.to change { project.reload.jobs_cache_index }.by(1)
+ end
+ end
+end
diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb
index eae9bd4f5cf..bc7885b03d9 100644
--- a/spec/services/search/snippet_service_spec.rb
+++ b/spec/services/search/snippet_service_spec.rb
@@ -33,7 +33,7 @@ describe Search::SnippetService do
it 'returns public, internal snippets and project private snippets for project members' do
member = create(:user)
- project.team << [member, :developer]
+ project.add_developer(member)
search = described_class.new(member, search: 'password')
results = search.execute
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 47412110b4b..4e640a82dfc 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe SystemNoteService do
include Gitlab::Routing
+ include RepoHelpers
set(:group) { create(:group) }
set(:project) { create(:project, :repository, group: group) }
@@ -927,7 +928,7 @@ describe SystemNoteService do
# We need a custom noteable in order to the shared examples to be green.
let(:noteable) do
mr = create(:merge_request, source_project: project)
- mr.spend_time(duration: 360000, user: author)
+ mr.spend_time(duration: 360000, user_id: author.id)
mr.save!
mr
end
@@ -965,7 +966,7 @@ describe SystemNoteService do
end
def spend_time!(seconds)
- noteable.spend_time(duration: seconds, user: author)
+ noteable.spend_time(duration: seconds, user_id: author.id)
noteable.save!
end
end
@@ -1070,17 +1071,32 @@ describe SystemNoteService do
let(:action) { 'outdated' }
end
- it 'creates a new note in the discussion' do
- # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
- expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
+ context 'when the change_position is valid for the discussion' do
+ it 'creates a new note in the discussion' do
+ # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
+ expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
+ end
+
+ it 'links to the diff in the system note' do
+ expect(subject.note).to include('version 1')
+
+ diff_id = merge_request.merge_request_diff.id
+ line_code = change_position.line_code(project.repository)
+ expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code))
+ end
end
- it 'links to the diff in the system note' do
- expect(subject.note).to include('version 1')
+ context 'when the change_position is invalid for the discussion' do
+ let(:change_position) { project.commit(sample_commit.id) }
- diff_id = merge_request.merge_request_diff.id
- line_code = change_position.line_code(project.repository)
- expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code))
+ it 'creates a new note in the discussion' do
+ # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
+ expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
+ end
+
+ it 'does not create a link' do
+ expect(subject.note).to eq('changed this line in version 1 of the diff')
+ end
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index dc2673abc73..5e6c24f5730 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -17,11 +17,11 @@ describe TodoService do
let(:service) { described_class.new }
before do
- project.team << [guest, :guest]
- project.team << [author, :developer]
- project.team << [member, :developer]
- project.team << [john_doe, :developer]
- project.team << [skipped, :developer]
+ project.add_guest(guest)
+ project.add_developer(author)
+ project.add_developer(member)
+ project.add_developer(john_doe)
+ project.add_developer(skipped)
end
describe 'Issues' do
@@ -248,11 +248,26 @@ describe TodoService do
end
end
- describe '#destroy_issuable' do
- it 'refresh the todos count cache for the user' do
- expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+ describe '#destroy_target' do
+ it 'refreshes the todos count cache for users with todos on the target' do
+ create(:todo, target: issue, user: john_doe, author: john_doe, project: issue.project)
- service.destroy_issuable(issue, john_doe)
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ service.destroy_target(issue) { }
+ end
+
+ it 'does not refresh the todos count cache for users with only done todos on the target' do
+ create(:todo, :done, target: issue, user: john_doe, author: john_doe, project: issue.project)
+
+ expect_any_instance_of(User).not_to receive(:update_todos_count_cache)
+
+ service.destroy_target(issue) { }
+ end
+
+ it 'yields the target to the caller' do
+ expect { |b| service.destroy_target(issue, &b) }
+ .to yield_with_args(issue)
end
end
diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb
new file mode 100644
index 00000000000..b5fb999381d
--- /dev/null
+++ b/spec/services/update_merge_request_metrics_service_spec.rb
@@ -0,0 +1,42 @@
+require 'rails_helper'
+
+describe MergeRequestMetricsService do
+ let(:metrics) { create(:merge_request).metrics }
+
+ describe '#merge' do
+ it 'updates metrics' do
+ user = create(:user)
+ service = described_class.new(metrics)
+ event = double(Event, author_id: user.id, created_at: Time.now)
+
+ service.merge(event)
+
+ expect(metrics.merged_by).to eq(user)
+ expect(metrics.merged_at).to eq(event.created_at)
+ end
+ end
+
+ describe '#close' do
+ it 'updates metrics' do
+ user = create(:user)
+ service = described_class.new(metrics)
+ event = double(Event, author_id: user.id, created_at: Time.now)
+
+ service.close(event)
+
+ expect(metrics.latest_closed_by).to eq(user)
+ expect(metrics.latest_closed_at).to eq(event.created_at)
+ end
+ end
+
+ describe '#reopen' do
+ it 'updates metrics' do
+ service = described_class.new(metrics)
+
+ service.reopen
+
+ expect(metrics.latest_closed_by).to be_nil
+ expect(metrics.latest_closed_at).to be_nil
+ end
+ end
+end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 58a5bede3de..aeba9cd60bc 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -188,5 +188,22 @@ describe Users::DestroyService do
end
end
end
+
+ describe "calls the before/after callbacks" do
+ it 'of project_members' do
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
+
+ service.execute(user)
+ end
+
+ it 'of group_members' do
+ group_member = create(:group_member)
+ group_member.group.group_members.create(user: user, access_level: 40)
+
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
+
+ service.execute(user)
+ end
+ end
end
end
diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb
index a188cf86772..bee8380e8b7 100644
--- a/spec/services/users/keys_count_service_spec.rb
+++ b/spec/services/users/keys_count_service_spec.rb
@@ -15,14 +15,12 @@ describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
expect(service.count).to eq(1)
end
- it 'caches the number of keys in Redis' do
+ it 'caches the number of keys in Redis', :request_store do
+ service.delete_cache
+ control_count = ActiveRecord::QueryRecorder.new { service.count }.count
service.delete_cache
- recorder = ActiveRecord::QueryRecorder.new do
- 2.times { service.count }
- end
-
- expect(recorder.count).to eq(1)
+ expect { 2.times { service.count } }.not_to exceed_query_limit(control_count)
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index f94fb8733d5..f51bb44086b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -183,7 +183,7 @@ RSpec::Matchers.define :match_asset_path do |expected|
end
end
-FactoryGirl::SyntaxRunner.class_eval do
+FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb
new file mode 100644
index 00000000000..943c1f6ffd7
--- /dev/null
+++ b/spec/support/api/boards_shared_examples.rb
@@ -0,0 +1,180 @@
+shared_examples_for 'group and project boards' do |route_definition, ee = false|
+ let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) }
+
+ before do
+ board_parent.add_reporter(user)
+ board_parent.add_guest(guest)
+ end
+
+ def expect_schema_match_for(response, schema_file, ee)
+ if ee
+ expect(response).to match_response_schema(schema_file, dir: "ee")
+ else
+ expect(response).to match_response_schema(schema_file)
+ end
+ end
+
+ describe "GET #{route_definition}" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get api(root_url)
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns the issue boards" do
+ get api(root_url, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect_schema_match_for(response, 'public_api/v4/boards', ee)
+ end
+
+ describe "GET #{route_definition}/:board_id" do
+ let(:url) { "#{root_url}/#{board.id}" }
+
+ it 'get a single board by id' do
+ get api(url, user)
+
+ expect_schema_match_for(response, 'public_api/v4/board', ee)
+ end
+ end
+ end
+ end
+
+ describe "GET #{route_definition}/:board_id/lists" do
+ let(:url) { "#{root_url}/#{board.id}/lists" }
+
+ it 'returns issue board lists' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['label']['name']).to eq(dev_label.title)
+ end
+
+ it 'returns 404 if board not found' do
+ get api("#{root_url}/22343/lists", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe "GET #{route_definition}/:board_id/lists/:list_id" do
+ let(:url) { "#{root_url}/#{board.id}/lists" }
+
+ it 'returns a list' do
+ get api("#{url}/#{dev_list.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(dev_list.id)
+ expect(json_response['label']['name']).to eq(dev_label.title)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it 'returns 404 if list not found' do
+ get api("#{url}/5324", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe "POST #{route_definition}/lists" do
+ let(:url) { "#{root_url}/#{board.id}/lists" }
+
+ it 'creates a new issue board list for labels' do
+ post api(url, user), label_id: ux_label.id
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['label']['name']).to eq(ux_label.title)
+ expect(json_response['position']).to eq(3)
+ end
+
+ it 'returns 400 when creating a new list if label_id is invalid' do
+ post api(url, user), label_id: 23423
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 403 for members with guest role' do
+ put api("#{url}/#{test_list.id}", guest), position: 1
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ describe "PUT #{route_definition}/:board_id/lists/:list_id to update only position" do
+ let(:url) { "#{root_url}/#{board.id}/lists" }
+
+ it "updates a list" do
+ put api("#{url}/#{test_list.id}", user),
+ position: 1
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it "returns 404 error if list id not found" do
+ put api("#{url}/44444", user),
+ position: 1
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it "returns 403 for members with guest role" do
+ put api("#{url}/#{test_list.id}", guest),
+ position: 1
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ describe "DELETE #{route_definition}/lists/:list_id" do
+ let(:url) { "#{root_url}/#{board.id}/lists" }
+
+ it "rejects a non member from deleting a list" do
+ delete api("#{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 api("#{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 api("#{url}/44444", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ context "when the user is parent owner" do
+ set(:owner) { create(:user) }
+
+ before do
+ if board_parent.try(:namespace)
+ board_parent.update(namespace: owner.namespace)
+ else
+ board.parent.add_owner(owner)
+ end
+ end
+
+ it "deletes the list if an admin requests it" do
+ delete api("#{url}/#{dev_list.id}", owner)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("#{url}/#{dev_list.id}", owner) }
+ end
+ end
+ end
+end
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index d9080b02541..0388f110d71 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -258,7 +258,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
# Add public project to the group in context
setup_for_group if context_group
- public_project.team << [user, :developer]
+ public_project.add_developer(user)
milestone.issues << issue << confidential_issue
end
@@ -275,7 +275,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'does not return confidential issues to team members with guest role' do
member = create(:user)
- public_project.team << [member, :guest]
+ public_project.add_guest(member)
get api(issues_route, member)
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index af1083f4bfd..dd3089d22e5 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -79,7 +79,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'when subtracting time' do
it 'subtracts time of the total spent time' do
- issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1h'
@@ -91,7 +91,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: user })
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1w'
@@ -119,7 +119,7 @@ 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: user },
+ issuable.update_attributes!(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
index afe0f4cecda..f27a2d06c83 100644
--- a/spec/support/api/v3/time_tracking_shared_examples.rb
+++ b/spec/support/api/v3/time_tracking_shared_examples.rb
@@ -75,7 +75,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
context 'when subtracting time' do
it 'subtracts time of the total spent time' do
- issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+ 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'
@@ -87,7 +87,7 @@ shared_examples 'V3 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: user })
+ 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'
@@ -115,7 +115,7 @@ shared_examples 'V3 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: user },
+ 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)
diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/background_migrations_matchers.rb
index 423c0e4cefc..f4127efc6ae 100644
--- a/spec/support/background_migrations_matchers.rb
+++ b/spec/support/background_migrations_matchers.rb
@@ -1,4 +1,4 @@
-RSpec::Matchers.define :be_scheduled_migration do |delay, *expected|
+RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected|
match do |migration|
BackgroundMigrationWorker.jobs.any? do |job|
job['args'] == [migration, expected] &&
@@ -11,3 +11,16 @@ RSpec::Matchers.define :be_scheduled_migration do |delay, *expected|
'not scheduled in expected time!'
end
end
+
+RSpec::Matchers.define :be_scheduled_migration do |*expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ args = job['args'].size == 1 ? [BackgroundMigrationWorker.jobs[0]['args'][0], []] : job['args']
+ args == [migration, expected]
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+end
diff --git a/spec/support/batch_loader.rb b/spec/support/batch_loader.rb
new file mode 100644
index 00000000000..bb790e660a6
--- /dev/null
+++ b/spec/support/batch_loader.rb
@@ -0,0 +1,5 @@
+RSpec.configure do |config|
+ config.after do
+ BatchLoader::Executor.clear_current
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 935b170a0f6..5189c57b7db 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -33,6 +33,9 @@ Capybara.register_driver :chrome do |app|
options.add_argument("disable-gpu")
end
+ # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
+ options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
+
Capybara::Selenium::Driver.new(
app,
browser: :chrome,
diff --git a/spec/support/cluster_application_spec.rb b/spec/support/cluster_application_spec.rb
new file mode 100644
index 00000000000..ab77910a050
--- /dev/null
+++ b/spec/support/cluster_application_spec.rb
@@ -0,0 +1,105 @@
+shared_examples 'cluster application specs' do
+ let(:factory_name) { described_class.to_s.downcase.gsub("::", "_") }
+
+ describe '#name' do
+ it 'is .application_name' do
+ expect(subject.name).to eq(described_class.application_name)
+ end
+
+ it 'is recorded in Clusters::Cluster::APPLICATIONS' do
+ expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
+ end
+ end
+
+ describe '#status' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { described_class.new(cluster: cluster) }
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+
+ context 'when application helm is scheduled' do
+ before do
+ create(factory_name, :scheduled, cluster: cluster)
+ end
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+ end
+
+ context 'when application helm is installed' do
+ before do
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+ end
+
+ it 'defaults to :installable' do
+ expect(subject.status_name).to be(:installable)
+ end
+ end
+ end
+
+ describe '#install_command' do
+ it 'has all the needed information' do
+ expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false)
+ end
+ end
+
+ describe 'status state machine' do
+ describe '#make_installing' do
+ subject { create(factory_name, :scheduled) }
+
+ it 'is installing' do
+ subject.make_installing!
+
+ expect(subject).to be_installing
+ end
+ end
+
+ describe '#make_installed' do
+ subject { create(factory_name, :installing) }
+
+ it 'is installed' do
+ subject.make_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ describe '#make_errored' do
+ subject { create(factory_name, :installing) }
+ let(:reason) { 'some errors' }
+
+ it 'is errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
+
+ describe '#make_scheduled' do
+ subject { create(factory_name, :installable) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+
+ describe 'when was errored' do
+ subject { create(factory_name, :errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb
index 224619c899c..d72925e1838 100644
--- a/spec/support/cookie_helper.rb
+++ b/spec/support/cookie_helper.rb
@@ -8,6 +8,10 @@ module CookieHelper
page.driver.browser.manage.add_cookie(name: name, value: value, **options)
end
+ def get_cookie(name)
+ page.driver.browser.manage.cookie_named(name)
+ end
+
private
def on_a_page?
diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb
index eec437fb3aa..c7890e49c66 100644
--- a/spec/support/factory_girl.rb
+++ b/spec/support/factory_girl.rb
@@ -1,3 +1,3 @@
RSpec.configure do |config|
- config.include FactoryGirl::Syntax::Methods
+ config.include FactoryBot::Syntax::Methods
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 08e21ee2537..2c20821ac3f 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -19,7 +19,7 @@ shared_examples 'issuable record that supports quick actions in its description
let(:new_url_opts) { {} }
before do
- project.team << [master, :master]
+ project.add_master(master)
gitlab_sign_in(master)
end
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
index 8a073e58db8..99752ed396e 100644
--- a/spec/support/google_api/cloud_platform_helpers.rb
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -10,6 +10,12 @@ module GoogleApi
request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.ago.to_i.to_s
end
+ def stub_google_project_billing_status
+ redis_double = double
+ 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
+
def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options)
WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
.to_return(cloud_platform_response(cloud_platform_cluster_body(options)))
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index c90359d7cfa..39e94ad53de 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -8,7 +8,7 @@
# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
# reference to the factory-created objects.
class MarkdownFeature
- include FactoryGirl::Syntax::Methods
+ include FactoryBot::Syntax::Methods
def user
@user ||= create(:user)
@@ -24,7 +24,7 @@ class MarkdownFeature
def project
@project ||= create(:project, :repository, group: group).tap do |project|
- project.team << [user, :master]
+ project.add_master(user)
end
end
@@ -85,7 +85,7 @@ class MarkdownFeature
@xproject ||= begin
group = create(:group, :nested)
create(:project, :repository, namespace: group) do |project|
- project.team << [user, :developer]
+ project.add_developer(user)
end
end
end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index 3ac201f1fb1..1685decbe94 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -53,7 +53,7 @@ shared_context 'mentionable context' do
set_mentionable_text.call(ref_string)
- project.team << [author, :developer]
+ project.add_developer(author)
end
end
diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb
index bd83cb88058..baf8bcc04b8 100644
--- a/spec/support/reference_parser_shared_examples.rb
+++ b/spec/support/reference_parser_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features|
end
it "creates reference for member" do
- project.team << [user, :developer]
+ project.add_developer(user)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
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 9399745f900..7b064162726 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.team << [u, :master] } }
+ let(:user) { create(:user).tap { |u| project.add_master(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.team << [assignee, :master]
+ project.add_master(assignee)
end
context 'with labels in command only' do
diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb
index 7457484a932..3f1fd169b72 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/services_shared_context.rb
@@ -29,5 +29,13 @@ Service.available_services_names.each do |service|
end
end
end
+
+ def initialize_service(service)
+ service_item = project.find_or_initialize_service(service)
+ service_item.properties = service_attrs
+ service_item.active = true if service == "kubernetes"
+ service_item.save
+ service_item
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb
new file mode 100644
index 00000000000..96d59e0c472
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb
@@ -0,0 +1,29 @@
+shared_examples 'issuable participants endpoint' do
+ let(:area) { entity.class.name.underscore.pluralize }
+
+ it 'returns participants' do
+ get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", 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.size).to eq(entity.participants.size)
+
+ last_participant = entity.participants.last
+ expect(json_response.last['id']).to eq(last_participant.id)
+ expect(json_response.last['name']).to eq(last_participant.name)
+ expect(json_response.last['username']).to eq(last_participant.username)
+ end
+
+ it 'returns a 404 when iid does not exist' do
+ get api("/projects/#{project.id}/#{area}/999/participants", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 404 when id is used instead of iid' do
+ get api("/projects/#{project.id}/#{area}/#{entity.id}/participants", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index b36cf3c544c..9f08c139322 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -7,6 +7,9 @@ module StubConfiguration
allow_any_instance_of(ApplicationSetting).to receive_messages(to_settings(messages))
allow(Gitlab::CurrentSettings.current_application_settings)
.to receive_messages(to_settings(messages))
+
+ # Ensure that we don't use the Markdown cache when stubbing these values
+ allow_any_instance_of(ApplicationSetting).to receive(:cached_html_up_to_date?).and_return(false)
end
def stub_not_protect_default_branch
diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb
index 19fbe572930..f621463e621 100644
--- a/spec/support/stub_env.rb
+++ b/spec/support/stub_env.rb
@@ -17,6 +17,7 @@ module StubENV
def add_stubbed_value(key, value)
allow(ENV).to receive(:[]).with(key).and_return(value)
+ allow(ENV).to receive(:key?).with(key).and_return(true)
allow(ENV).to receive(:fetch).with(key).and_return(value)
allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val|
value || default_val
@@ -29,6 +30,7 @@ module StubENV
def init_stub
allow(ENV).to receive(:[]).and_call_original
+ allow(ENV).to receive(:key?).and_call_original
allow(ENV).to receive(:fetch).and_call_original
add_stubbed_value(STUBBED_KEY, true)
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index b300b493f86..664698fcbaf 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -1,4 +1,5 @@
require 'rspec/mocks'
+require 'toml'
module TestEnv
extend self
@@ -82,10 +83,10 @@ module TestEnv
setup_gitaly
- # Create repository for FactoryGirl.create(:project)
+ # Create repository for FactoryBot.create(:project)
setup_factory_repo
- # Create repository for FactoryGirl.create(:forked_project_with_submodules)
+ # Create repository for FactoryBot.create(:forked_project_with_submodules)
setup_forked_repo
end
@@ -147,6 +148,9 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{gitaly_dir}]") do
+ # Always re-create config, in case it's outdated. This is fast anyway.
+ Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true)
+
start_gitaly(gitaly_dir)
end
end
@@ -215,7 +219,7 @@ module TestEnv
end
def copy_repo(project, bare_repo:, refs:)
- target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.disk_path}.git")
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
@@ -347,6 +351,9 @@ module TestEnv
end
def component_needs_update?(component_folder, expected_version)
+ # Allow local overrides of the component for tests during development
+ return false if Rails.env.test? && File.symlink?(component_folder)
+
version = File.read(File.join(component_folder, 'VERSION')).strip
# Notice that this will always yield true when using branch versions
diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb
index 565d3203e4f..5e3f19ba19e 100644
--- a/spec/support/updating_mentions_shared_examples.rb
+++ b/spec/support/updating_mentions_shared_examples.rb
@@ -3,7 +3,7 @@ RSpec.shared_examples 'updating mentions' do |service_class|
let(:service_class) { service_class }
before do
- project.team << [mentioned_user, :developer]
+ project.add_developer(mentioned_user)
end
def update_mentionable(opts)
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
new file mode 100644
index 00000000000..dacc5dc5ae7
--- /dev/null
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -0,0 +1,38 @@
+require 'rake_helper'
+
+describe 'gitlab:git rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/git'
+
+ storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } }
+
+ 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 }
+
+ stub_warn_user_is_not_gitlab
+ end
+
+ after do
+ FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
+ end
+
+ describe 'fsck' do
+ it 'outputs the integrity check for a repo' do
+ expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity at .*@hashed\/1\/2\/test.git/).to_stdout
+ end
+
+ it 'errors out about config.lock issues' do
+ FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@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'))
+
+ expect { run_rake_task('gitlab:git:fsck') }.to output(/Ref lock files exist:/).to_stdout
+ end
+ end
+end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index bb32ee62ccb..7ef7fb7d758 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -8,7 +8,7 @@ describe RecordsUploads do
storage :file
def model
- FactoryGirl.build_stubbed(:user)
+ FactoryBot.build_stubbed(:user)
end
end
diff --git a/spec/views/events/event/_push.html.haml_spec.rb b/spec/views/events/event/_push.html.haml_spec.rb
new file mode 100644
index 00000000000..f5634de4916
--- /dev/null
+++ b/spec/views/events/event/_push.html.haml_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe 'events/event/_push.html.haml' do
+ let(:event) { build_stubbed(:push_event) }
+
+ context 'with a branch' do
+ let(:payload) { build_stubbed(:push_event_payload, event: event) }
+
+ before do
+ allow(event).to receive(:push_event_payload).and_return(payload)
+ end
+
+ it 'links to the branch' do
+ allow(event.project.repository).to receive(:branch_exists?).with(event.ref_name).and_return(true)
+ link = project_commits_path(event.project, event.ref_name)
+
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to have_link(event.ref_name, href: link)
+ end
+
+ context 'that has been deleted' do
+ it 'does not link to the branch' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).not_to have_link(event.ref_name)
+ end
+ end
+ end
+
+ context 'with a tag' do
+ let(:payload) { build_stubbed(:push_event_payload, event: event, ref_type: :tag, ref: 'v0.1.0') }
+
+ before do
+ allow(event).to receive(:push_event_payload).and_return(payload)
+ end
+
+ it 'links to the tag' do
+ allow(event.project.repository).to receive(:tag_exists?).with(event.ref_name).and_return(true)
+ link = project_commits_path(event.project, event.ref_name)
+
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to have_link(event.ref_name, href: link)
+ end
+
+ context 'that has been deleted' do
+ it 'does not link to the tag' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).not_to have_link(event.ref_name)
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
index 9b293065797..ec435ec3b32 100644
--- a/spec/views/projects/imports/new.html.haml_spec.rb
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -8,7 +8,7 @@ describe "projects/imports/new.html.haml" do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
it "escapes HTML in import errors" do
diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb
index 6139529013f..6a67da79ec5 100644
--- a/spec/views/projects/jobs/show.html.haml_spec.rb
+++ b/spec/views/projects/jobs/show.html.haml_spec.rb
@@ -187,7 +187,7 @@ describe 'projects/jobs/show' do
context 'when incomplete trigger_request is used' do
before do
- build.trigger_request = FactoryGirl.build(:ci_trigger_request, trigger: nil)
+ build.trigger_request = FactoryBot.build(:ci_trigger_request, trigger: nil)
end
it 'test should not render token block' do
@@ -199,7 +199,7 @@ describe 'projects/jobs/show' do
context 'when complete trigger_request is used' do
before do
- build.trigger_request = FactoryGirl.build(:ci_trigger_request)
+ build.trigger_request = FactoryBot.build(:ci_trigger_request)
end
it 'should render token' 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 28d54c2fb77..264e0ce0b40 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -54,6 +54,8 @@ describe 'projects/merge_requests/show.html.haml' do
it 'closes the merge request if the source project does not exist' do
closed_merge_request.update_attributes(state: 'open')
forked_project.destroy
+ # Reload merge request so MergeRequest#source_project turns to `nil`
+ closed_merge_request.reload
render
diff --git a/spec/views/projects/tree/_blob_item.html.haml_spec.rb b/spec/views/projects/tree/_blob_item.html.haml_spec.rb
new file mode 100644
index 00000000000..6a477c712ff
--- /dev/null
+++ b/spec/views/projects/tree/_blob_item.html.haml_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe 'projects/tree/_blob_item' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
+
+ before do
+ assign(:project, project)
+ assign(:repository, repository)
+ assign(:id, File.join('master', ''))
+ assign(:lfs_blob_ids, [])
+ end
+
+ it 'renders blob item' do
+ render_partial(blob_item)
+
+ expect(rendered).to have_content(blob_item.name)
+ expect(rendered).not_to have_selector('.label-lfs', text: 'LFS')
+ end
+
+ describe 'LFS blob' do
+ before do
+ assign(:lfs_blob_ids, [blob_item].map(&:id))
+
+ render_partial(blob_item)
+ end
+
+ it 'renders LFS badge' do
+ expect(rendered).to have_selector('.label-lfs', text: 'LFS')
+ end
+ end
+
+ def render_partial(blob_item)
+ render partial: 'projects/tree/blob_item', locals: {
+ blob_item: blob_item,
+ type: 'blob'
+ }
+ end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 3c25e341b39..44b32df0395 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -9,6 +9,7 @@ describe 'projects/tree/show' do
before do
assign(:project, project)
assign(:repository, repository)
+ assign(:lfs_blob_ids, [])
allow(view).to receive(:can?).and_return(true)
allow(view).to receive(:can_collaborate_with_project?).and_return(true)
diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb
index cae6bee2776..50980718e66 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.team << [user, :master]
+ project.add_master(user)
assign(:project, project)
assign(:note, note)
diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb
index 1c54cf55fa0..d67e7698635 100644
--- a/spec/workers/background_migration_worker_spec.rb
+++ b/spec/workers/background_migration_worker_spec.rb
@@ -1,13 +1,32 @@
require 'spec_helper'
-describe BackgroundMigrationWorker, :sidekiq do
+describe BackgroundMigrationWorker, :sidekiq, :clean_gitlab_redis_shared_state do
+ let(:worker) { described_class.new }
+
describe '.perform' do
it 'performs a background migration' do
expect(Gitlab::BackgroundMigration)
.to receive(:perform)
.with('Foo', [10, 20])
- described_class.new.perform('Foo', [10, 20])
+ worker.perform('Foo', [10, 20])
+ end
+
+ it 'reschedules a migration if it was performed recently' do
+ expect(worker)
+ .to receive(:always_perform?)
+ .and_return(false)
+
+ worker.lease_for('Foo').try_obtain
+
+ expect(Gitlab::BackgroundMigration)
+ .not_to receive(:perform)
+
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(a_kind_of(Numeric), 'Foo', [10, 20])
+
+ worker.perform('Foo', [10, 20])
end
end
end
diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb
new file mode 100644
index 00000000000..f52a903327c
--- /dev/null
+++ b/spec/workers/check_gcp_project_billing_worker_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe CheckGcpProjectBillingWorker do
+ describe '.perform' do
+ let(:token) { 'bogustoken' }
+
+ subject { described_class.new.perform('token_key') }
+
+ context 'when there is a token in redis' do
+ before do
+ allow_any_instance_of(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
+ redis_double = double
+
+ expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
+ expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
+ expect(redis_double).to receive(:set).with(described_class.redis_shared_state_key_for(token), anything, anything)
+
+ 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_any_instance_of(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
+end
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 0145563e0ed..901d77178bc 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -17,6 +17,14 @@ describe ApplicationWorker do
end
end
+ describe '.queue_namespace' do
+ it 'sets the queue name based on the class name' do
+ worker.queue_namespace :some_namespace
+
+ expect(worker.queue).to eq('some_namespace:foo_bar_dummy')
+ end
+ end
+
describe '.queue' do
it 'returns the queue name' do
worker.sidekiq_options queue: :some_queue
diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb
index 5049886b55c..4118b9aa194 100644
--- a/spec/workers/concerns/cluster_queue_spec.rb
+++ b/spec/workers/concerns/cluster_queue_spec.rb
@@ -14,6 +14,6 @@ describe ClusterQueue do
it 'sets a default pipelines queue automatically' do
expect(worker.sidekiq_options['queue'])
- .to eq :gcp_cluster
+ .to eq 'gcp_cluster:dummy'
end
end
diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb
index 3ae1c5f54d8..c042a52f41f 100644
--- a/spec/workers/concerns/cronjob_queue_spec.rb
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -13,7 +13,7 @@ describe CronjobQueue do
end
it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+ expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob:dummy')
end
it 'disables retrying of failed jobs' do
diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
index 9c69ee32da1..a96f583aff7 100644
--- a/spec/workers/concerns/gitlab/github_import/queue_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
@@ -11,6 +11,6 @@ describe Gitlab::GithubImport::Queue do
include Gitlab::GithubImport::Queue
end
- expect(worker.sidekiq_options['queue']).to eq('github_importer')
+ expect(worker.sidekiq_options['queue']).to eq('github_importer:dummy')
end
end
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
index dd911760948..a312b307fce 100644
--- a/spec/workers/concerns/pipeline_queue_spec.rb
+++ b/spec/workers/concerns/pipeline_queue_spec.rb
@@ -14,15 +14,6 @@ describe PipelineQueue do
it 'sets a default pipelines queue automatically' do
expect(worker.sidekiq_options['queue'])
- .to eq 'pipeline_default'
- end
-
- describe '.enqueue_in' do
- it 'sets a custom sidekiq queue with prefix and group' do
- worker.enqueue_in(group: :processing)
-
- expect(worker.sidekiq_options['queue'])
- .to eq 'pipeline_processing'
- end
+ .to eq 'pipeline_default:dummy'
end
end
diff --git a/spec/workers/concerns/project_import_options_spec.rb b/spec/workers/concerns/project_import_options_spec.rb
new file mode 100644
index 00000000000..b6c111df8b9
--- /dev/null
+++ b/spec/workers/concerns/project_import_options_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe ProjectImportOptions do
+ let(:project) { create(:project, :import_started) }
+ let(:job) { { 'args' => [project.id, nil, nil], 'jid' => '123' } }
+ let(:worker_class) do
+ Class.new do
+ include Sidekiq::Worker
+ include ProjectImportOptions
+ end
+ end
+
+ it 'sets default retry limit' do
+ expect(worker_class.sidekiq_options['retry']).to eq(ProjectImportOptions::IMPORT_RETRY_COUNT)
+ end
+
+ it 'sets default status expiration' do
+ expect(worker_class.sidekiq_options['status_expiration']).to eq(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'marks fork as failed' do
+ expect { worker_class.sidekiq_retries_exhausted_block.call(job) }.to change { project.reload.import_status }.from("started").to("failed")
+ end
+
+ it 'logs the appropriate error message for forked projects' do
+ allow_any_instance_of(Project).to receive(:forked?).and_return(true)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+
+ expect(project.reload.import_error).to include("fork")
+ end
+
+ it 'logs the appropriate error message for forked projects' do
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+
+ expect(project.reload.import_error).to include("import")
+ end
+ end
+end
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
index fdbbfcc90a5..d2eeecfc9a8 100644
--- a/spec/workers/concerns/repository_check_queue_spec.rb
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -13,7 +13,7 @@ describe RepositoryCheckQueue do
end
it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check')
+ expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check:dummy')
end
it 'disables retrying of failed jobs' do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 7ee0a51a263..9e3b99b3502 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -1,21 +1,36 @@
require 'spec_helper'
describe 'Every Sidekiq worker' do
- it 'includes ApplicationWorker' do
- expect(Gitlab::SidekiqConfig.workers).to all(include(ApplicationWorker))
- end
-
it 'does not use the default queue' do
expect(Gitlab::SidekiqConfig.workers.map(&:queue)).not_to include('default')
end
it 'uses the cronjob queue when the worker runs as a cronjob' do
- expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(eq('cronjob'))
+ expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(start_with('cronjob:'))
+ end
+
+ it 'has its queue in app/workers/all_queues.yml', :aggregate_failures 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 << 'default'
+
+ missing_from_file = worker_queues - file_worker_queues
+ expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in app/workers/all_queues.yml"
+
+ unncessarily_in_file = file_worker_queues - worker_queues
+ expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in app/workers/all_queues.yml"
end
- it 'defines the queue in the Sidekiq configuration file' do
- config_queue_names = Gitlab::SidekiqConfig.config_queues.to_set
+ it 'has its queue or namespace in config/sidekiq_queues.yml', :aggregate_failures do
+ config_queues = Gitlab::SidekiqConfig.config_queues.to_set
+
+ Gitlab::SidekiqConfig.workers.each do |worker|
+ queue = worker.queue
+ queue_namespace = queue.split(':').first
- expect(Gitlab::SidekiqConfig.worker_queues).to all(be_in(config_queue_names))
+ expect(config_queues).to include(queue).or(include(queue_namespace))
+ end
end
end
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 303193bab9b..c861a56497e 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.team << [author, :master]
+ source_project.add_master(author)
source_project.repository.expire_branches_cache
end
diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb
new file mode 100644
index 00000000000..20aff020dbb
--- /dev/null
+++ b/spec/workers/rebase_worker_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe RebaseWorker, '#perform' do
+ context 'when rebasing an MR from a fork where upstream has protected branches' do
+ let(:upstream_project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ source_branch: 'feature_conflict',
+ target_project: upstream_project,
+ target_branch: 'master')
+ end
+
+ before do
+ create(:forked_project_link, forked_to_project: fork_project, forked_from_project: upstream_project)
+ end
+
+ it 'sets the correct project for running hooks' do
+ expect(MergeRequests::RebaseService)
+ .to receive(:new).with(fork_project, merge_request.author).and_call_original
+
+ subject.perform(merge_request, merge_request.author)
+ end
+ end
+end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 74c85848b7e..31598586f59 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -1,17 +1,21 @@
require 'spec_helper'
describe RepositoryForkWorker do
- let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, :import_scheduled, forked_from_project: project) }
- let(:shell) { Gitlab::Shell.new }
-
- subject { described_class.new }
-
- before do
- allow(subject).to receive(:gitlab_shell).and_return(shell)
+ describe 'modules' do
+ it 'includes ProjectImportOptions' do
+ expect(described_class).to include_module(ProjectImportOptions)
+ end
end
describe "#perform" do
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, :import_scheduled, forked_from_project: project) }
+ let(:shell) { Gitlab::Shell.new }
+
+ before do
+ allow(subject).to receive(:gitlab_shell).and_return(shell)
+ end
+
def perform!
subject.perform(fork_project.id, '/test/path', project.disk_path)
end
@@ -60,14 +64,7 @@ describe RepositoryForkWorker do
expect_fork_repository.and_return(false)
- expect { perform! }.to raise_error(RepositoryForkWorker::ForkError, error_message)
- end
-
- it 'handles unexpected error' do
- expect_fork_repository.and_raise(RuntimeError)
-
- expect { perform! }.to raise_error(RepositoryForkWorker::ForkError)
- expect(fork_project.reload.import_status).to eq('failed')
+ expect { perform! }.to raise_error(StandardError, error_message)
end
end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 0af537647ad..7274a9f00f9 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -1,11 +1,15 @@
require 'spec_helper'
describe RepositoryImportWorker do
- let(:project) { create(:project, :import_scheduled) }
-
- subject { described_class.new }
+ describe 'modules' do
+ it 'includes ProjectImportOptions' do
+ expect(described_class).to include_module(ProjectImportOptions)
+ end
+ end
describe '#perform' do
+ let(:project) { create(:project, :import_scheduled) }
+
context 'when worker was reset without cleanup' do
let(:jid) { '12345678' }
let(:started_project) { create(:project, :import_started, import_jid: jid) }
@@ -28,6 +32,7 @@ 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)
@@ -44,22 +49,11 @@ describe RepositoryImportWorker do
expect do
subject.perform(project.id)
- end.to raise_error(RepositoryImportWorker::ImportError, error)
+ end.to raise_error(StandardError, error)
expect(project.reload.import_jid).not_to be_nil
end
end
- context 'with unexpected error' do
- it 'marks import as failed' do
- allow_any_instance_of(Projects::ImportService).to receive(:execute).and_raise(RuntimeError)
-
- expect do
- subject.perform(project.id)
- end.to raise_error(RepositoryImportWorker::ImportError)
- expect(project.reload.import_status).to eq('failed')
- end
- end
-
context 'when using an asynchronous importer' do
it 'does not mark the import process as finished' do
service = double(:service)
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
new file mode 100644
index 00000000000..481a84837f9
--- /dev/null
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe RunPipelineScheduleWorker do
+ describe '#perform' do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ set(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) }
+ let(:worker) { described_class.new }
+
+ context 'when a project not found' do
+ it 'does not call the Service' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+ expect(worker).not_to receive(:run_pipeline_schedule)
+
+ worker.perform(100000, user.id)
+ end
+ end
+
+ context 'when a user not found' do
+ it 'does not call the Service' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+ expect(worker).not_to receive(:run_pipeline_schedule)
+
+ worker.perform(pipeline_schedule.id, 10000)
+ end
+ end
+
+ context 'when everything is ok' do
+ let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
+
+ it 'calls the Service' do
+ expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
+ expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+ end
+ end
+end
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index f8b55e873df..c2c2a5f9121 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -14,7 +14,6 @@ describe StuckMergeJobsWorker do
mr_with_sha.reload
mr_without_sha.reload
-
expect(mr_with_sha).to be_merged
expect(mr_without_sha).to be_opened
expect(mr_with_sha.merge_jid).to be_present
@@ -24,10 +23,13 @@ describe StuckMergeJobsWorker do
it 'updates merge request to opened when locked but has not been merged' do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123))
merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked)
+ pipeline = create(:ci_empty_pipeline, project: merge_request.project, ref: merge_request.source_branch, sha: merge_request.source_branch_sha)
worker.perform
- expect(merge_request.reload).to be_opened
+ merge_request.reload
+ expect(merge_request).to be_opened
+ expect(merge_request.head_pipeline).to eq(pipeline)
end
it 'logs updated stuck merge job ids' do
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
index 522e1566271..9adde5fc21a 100644
--- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -22,7 +22,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do
end
it 'does not update head_pipeline_id' do
- expect { subject.perform(merge_request.id) }.to raise_error(ArgumentError)
+ expect { subject.perform(merge_request.id) }.not_to raise_error
expect(merge_request.reload.head_pipeline_id).to eq(nil)
end
diff --git a/vendor/Dockerfile/CONTRIBUTING.md b/vendor/Dockerfile/CONTRIBUTING.md
index 0878db6dd9e..3e98f2e7b5b 100644
--- a/vendor/Dockerfile/CONTRIBUTING.md
+++ b/vendor/Dockerfile/CONTRIBUTING.md
@@ -1,22 +1,15 @@
-The canonical repository for `Dockerfile` templates is
-https://gitlab.com/gitlab-org/Dockerfile.
+## Developer Certificate of Origin + License
-GitLab only mirrors the templates. Please submit your merge requests to
-https://gitlab.com/gitlab-org/Dockerfile.
+By contributing to GitLab B.V., You accept and agree to the following terms and
+conditions for Your present and future Contributions submitted to GitLab B.V.
+Except for the license granted herein to GitLab B.V. and recipients of software
+distributed by GitLab B.V., You reserve all right, title, and interest in and to
+Your Contributions. All Contributions are subject to the following DCO + License
+terms.
-## Contributing
+[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
-Thank you for your interest in contributing to this GitLab project! We welcome
-all contributions. By participating in this project, you agree to abide by the
-[code of conduct](#code-of-conduct).
-
-## Contributor license agreement
-
-By submitting code as an individual you agree to the [individual contributor
-license agreement][individual-agreement].
-
-By submitting code as an entity you agree to the [corporate contributor license
-agreement][corporate-agreement].
+_This notice should stay as the first item in the CONTRIBUTING.md file._
## Code of conduct
diff --git a/vendor/Dockerfile/LICENSE b/vendor/Dockerfile/LICENSE
index d6c93c6fcf7..27a215686e7 100644
--- a/vendor/Dockerfile/LICENSE
+++ b/vendor/Dockerfile/LICENSE
@@ -1,6 +1,6 @@
-The MIT License (MIT)
+Copyright (c) 2011-2017 GitLab B.V.
-Copyright (c) 2016-2017 GitLab.org
+With regard to the GitLab Software:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,17 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+For all third party components incorporated into the GitLab Software, those
+components are licensed under the original license provided by the owner of the
+applicable component.
diff --git a/vendor/gitignore/Eagle.gitignore b/vendor/gitignore/Eagle.gitignore
index 9ced1260266..9afc324d6ae 100644
--- a/vendor/gitignore/Eagle.gitignore
+++ b/vendor/gitignore/Eagle.gitignore
@@ -4,6 +4,9 @@
*.s#?
*.b#?
*.l#?
+*.b$?
+*.s$?
+*.l$?
# Eagle project file
# It contains a serial number and references to the file structure
@@ -31,14 +34,19 @@ eagle.epf
*.drl
*.gpi
*.pls
+*.ger
+*.gpi
+*.xln
*.drd
*.drd.*
+*.s#*
+*.b#*
+
*.info
*.eps
# file locks introduced since 7.x
*.lck
-
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
index ce1c12cdb7a..0eb8a5e8571 100644
--- a/vendor/gitignore/Global/Eclipse.gitignore
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -23,6 +23,9 @@ local.properties
# CDT-specific (C/C++ Development Tooling)
.cproject
+# CDT- autotools
+.autotools
+
# Java annotation processor (APT)
.factorypath
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 345e61ae3f2..a30eacf1d98 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -21,6 +21,7 @@
# CMake
cmake-build-debug/
+cmake-build-release/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index cca150a88dd..d87a6bdbeeb 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -1,8 +1,3 @@
-##---------------------------------------------------
-## Remove autosaves generated by the Matlab editor
-## We have git for backups!
-##---------------------------------------------------
-
# Windows default autosave extension
*.asv
@@ -12,11 +7,19 @@
# Compiled MEX binaries (all platforms)
*.mex*
-# Simulink Code Generation
-slprj/
+# Packaged app and toolbox files
+*.mlappinstall
+*.mltbx
+
+# Generated helpsearch folders
+helpsearch*/
-# Session info
-octave-workspace
+# Simulink code generation folders
+slprj/
+sccprj/
# Simulink autosave extension
*.autosave
+
+# Octave session info
+octave-workspace
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index a1338d68517..f1c181ec9c5 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -1,5 +1,6 @@
# Binaries for programs and plugins
*.exe
+*.exe~
*.dll
*.so
*.dylib
@@ -9,6 +10,3 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
-
-# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
-.glide/
diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore
index eee88b2f0f7..82f3a88e17b 100644
--- a/vendor/gitignore/Haskell.gitignore
+++ b/vendor/gitignore/Haskell.gitignore
@@ -17,5 +17,6 @@ cabal.sandbox.config
*.eventlog
.stack-work/
cabal.project.local
+cabal.project.local~
.HTF/
.ghc.environment.*
diff --git a/vendor/gitignore/Jekyll.gitignore b/vendor/gitignore/Jekyll.gitignore
index 5c91b60c063..2ca868298ce 100644
--- a/vendor/gitignore/Jekyll.gitignore
+++ b/vendor/gitignore/Jekyll.gitignore
@@ -1,3 +1,4 @@
_site/
.sass-cache/
+.jekyll-cache/
.jekyll-metadata
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index 97e28736892..d1bed128fa8 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -57,3 +57,5 @@ typings/
# dotenv environment variables file
.env
+# next.js build output
+.next
diff --git a/vendor/gitignore/ROS.gitignore b/vendor/gitignore/ROS.gitignore
index f8bcd117371..425641f2c3a 100644
--- a/vendor/gitignore/ROS.gitignore
+++ b/vendor/gitignore/ROS.gitignore
@@ -1,3 +1,5 @@
+devel/
+logs/
build/
bin/
lib/
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index 42aeb55000a..828ab1d556a 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -42,3 +42,7 @@ bower.json
# Ignore Byebug command history file.
.byebug_history
+
+# Ignore node_modules
+node_modules/
+
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
index 85fd714a965..d098259ffb0 100644
--- a/vendor/gitignore/Symfony.gitignore
+++ b/vendor/gitignore/Symfony.gitignore
@@ -25,6 +25,7 @@
/bin/*
!bin/console
!bin/symfony_requirements
+/vendor/
# Assets and user uploads
/web/bundles/
@@ -37,6 +38,9 @@
# Build data
/build/
+# Composer PHAR
+/composer.phar
+
# Backup entities generated with doctrine:generate:entities command
**/Entity/*~
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index b6418e51766..9bb63365618 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -215,7 +215,11 @@ TSWLatexianTemp*
*~[0-9]*
# auto folder when using emacs and auctex
-/auto/*
+./auto/*
+*.el
# expex forward references with \gathertags
*-tags.tex
+
+# standalone packages
+*.sta
diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore
index b6b0743f62a..10fc2b4d825 100644
--- a/vendor/gitignore/Umbraco.gitignore
+++ b/vendor/gitignore/Umbraco.gitignore
@@ -16,8 +16,11 @@
# Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder)
# Make sure to include details from VisualStudio.gitignore BEFORE this
-!**/App_Data/[Pp]ackages/
-!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages
+!**/App_Data/[Pp]ackages/*
+!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages/*
# ImageProcessor DiskCache
**/App_Data/cache/
+
+# Ignore the Models Builder models out of date flag
+**/App_Data/Models/ood.flag
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index eb83a8f122d..75e5b1405da 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -1,9 +1,9 @@
-/[Ll]ibrary/
-/[Tt]emp/
-/[Oo]bj/
-/[Bb]uild/
-/[Bb]uilds/
-/Assets/AssetStoreTools*
+[Ll]ibrary/
+[Tt]emp/
+[Oo]bj/
+[Bb]uild/
+[Bb]uilds/
+Assets/AssetStoreTools*
# Visual Studio 2015 cache directory
/.vs/
@@ -25,6 +25,7 @@ ExportedObj/
# Unity3D generated meta files
*.pidb.meta
+*.pdb.meta
# Unity3D Generated File On Crash Reports
sysinfo.txt
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index 6c6e1c327fd..1daca8b50d9 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -50,6 +50,7 @@ SourceArt/**/*.tga
# Binary Files
Binaries/*
+Plugins/*/Binaries/*
# Builds
Build/*
@@ -70,6 +71,7 @@ Saved/*
# Compiled source files for the engine to use
Intermediate/*
+Plugins/*/Intermediate/*
# Cache files for the editor to use
DerivedDataCache/*
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 509668db67a..d3d5371b415 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -24,11 +24,14 @@ bld/
[Oo]bj/
[Ll]og/
-# Visual Studio 2015 cache/options directory
+# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
@@ -51,6 +54,10 @@ project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
@@ -212,6 +219,10 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
@@ -247,7 +258,7 @@ FakesAssemblies/
.ntvs_analysis.dat
node_modules/
-# Typescript v1 declaration files
+# TypeScript v1 declaration files
typings/
# Visual Studio 6 build log
@@ -303,3 +314,10 @@ __pycache__/
# OpenCover UI analysis results
OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
diff --git a/vendor/gitignore/WordPress.gitignore b/vendor/gitignore/WordPress.gitignore
index 97923503c4c..3b181ec0cf2 100644
--- a/vendor/gitignore/WordPress.gitignore
+++ b/vendor/gitignore/WordPress.gitignore
@@ -7,6 +7,7 @@ wp-content/blogs.dir/
wp-content/cache/
wp-content/upgrade/
wp-content/uploads/
+wp-content/mu-plugins/
wp-content/wp-cache-config.php
wp-content/plugins/hello.php
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 88261502d7f..75de266369d 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -34,6 +34,10 @@ variables:
POSTGRES_ENABLED: "true"
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
+ KUBERNETES_VERSION: 1.8.6
+ HELM_VERSION: 2.6.1
+ CODECLIMATE_VERSION: 0.69.0
+
stages:
- build
- test
@@ -41,6 +45,7 @@ stages:
- staging
- canary
- production
+ - performance
- cleanup
build:
@@ -83,6 +88,44 @@ codequality:
artifacts:
paths: [codeclimate.json]
+performance:
+ stage: performance
+ image:
+ name: sitespeedio/sitespeed.io:6.0.3
+ entrypoint: [""]
+ script:
+ - performance
+ artifacts:
+ paths:
+ - performance.json
+ only:
+ refs:
+ - branches
+ kubernetes: active
+
+sast:
+ image: registry.gitlab.com/gitlab-org/gl-sast:latest
+ variables:
+ POSTGRES_DB: "false"
+ allow_failure: true
+ script:
+ - sast .
+ artifacts:
+ paths: [gl-sast-report.json]
+
+sast:container:
+ image: docker:latest
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:dind
+ script:
+ - setup_docker
+ - sast_container
+ artifacts:
+ paths: [gl-sast-container-report.json]
+
review:
stage: review
script:
@@ -93,10 +136,13 @@ review:
- install_tiller
- create_secret
- deploy
+ - persist_environment_url
environment:
name: review/$CI_COMMIT_REF_NAME
url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$AUTO_DEVOPS_DOMAIN
on_stop: stop_review
+ artifacts:
+ paths: [environment_url.txt]
only:
refs:
- branches
@@ -191,9 +237,12 @@ production:
- create_secret
- deploy
- delete canary
+ - persist_environment_url
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+ artifacts:
+ paths: [environment_url.txt]
# when: manual
only:
refs:
@@ -211,6 +260,18 @@ production:
export CI_APPLICATION_TAG=$CI_COMMIT_SHA
export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID}
export TILLER_NAMESPACE=$KUBE_NAMESPACE
+
+ function sast_container() {
+ docker run -d --name db arminc/clair-db:latest
+ docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1
+ apk add -U wget ca-certificates
+ docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
+ wget https://github.com/arminc/clair-scanner/releases/download/v6/clair-scanner_linux_386
+ mv clair-scanner_linux_386 clair-scanner
+ chmod +x clair-scanner
+ touch clair-whitelist.yml
+ ./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
+ }
function codeclimate() {
cc_opts="--env CODECLIMATE_CODE="$PWD" \
@@ -218,8 +279,19 @@ production:
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume /tmp/cc:/tmp/cc"
- docker run ${cc_opts} codeclimate/codeclimate init
- docker run ${cc_opts} codeclimate/codeclimate analyze -f json > codeclimate.json
+ docker run ${cc_opts} "codeclimate/codeclimate:${CODECLIMATE_VERSION}" init
+ docker run ${cc_opts} "codeclimate/codeclimate:${CODECLIMATE_VERSION}" analyze -f json > codeclimate.json
+ }
+
+ function sast() {
+ case "$CI_SERVER_VERSION" in
+ *-ee)
+ /app/bin/run "$@"
+ ;;
+ *)
+ echo "GitLab EE is required"
+ ;;
+ esac
}
function deploy() {
@@ -280,11 +352,11 @@ production:
apk add glibc-2.23-r3.apk
rm glibc-2.23-r3.apk
- curl https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-linux-amd64.tar.gz | tar zx
+ curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
mv linux-amd64/helm /usr/bin/
helm version --client
- curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
+ curl -L -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl"
chmod +x /usr/bin/kubectl
kubectl version --client
}
@@ -345,6 +417,13 @@ production:
}
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"
+ echo ""
+ fi
+
if [[ -f Dockerfile ]]; then
echo "Building Dockerfile-based application..."
docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .
@@ -362,12 +441,6 @@ production:
echo ""
fi
- if [[ -n "$CI_REGISTRY_USER" ]]; then
- echo "Logging to GitLab Container Registry with CI credentials..."
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- echo ""
- fi
-
echo "Pushing to GitLab Container Registry..."
docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
echo ""
@@ -393,6 +466,29 @@ production:
--docker-email="$GITLAB_USER_EMAIL" \
-o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
}
+
+ function performance() {
+ export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
+
+ mkdir gitlab-exporter
+ wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-3/index.js
+
+ mkdir sitespeed-results
+
+ if [ -f .gitlab-urls.txt ]
+ then
+ sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
+ /start.sh --plugins.add gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
+ else
+ /start.sh --plugins.add gitlab-exporter --outputFolder sitespeed-results $CI_ENVIRONMENT_URL
+ fi
+
+ mv sitespeed-results/data/performance.json performance.json
+ }
+
+ function persist_environment_url() {
+ echo $CI_ENVIRONMENT_URL > environment_url.txt
+ }
function delete() {
track="${1-stable}"
@@ -402,7 +498,9 @@ production:
name="$name-$track"
fi
- helm delete "$name" || true
+ if [[ -n "$(helm ls -q "^$name$")" ]]; then
+ helm delete "$name"
+ fi
}
before_script:
diff --git a/vendor/gitlab-ci-yml/CONTRIBUTING.md b/vendor/gitlab-ci-yml/CONTRIBUTING.md
index d4c057bf9dc..d33a1f06f26 100644
--- a/vendor/gitlab-ci-yml/CONTRIBUTING.md
+++ b/vendor/gitlab-ci-yml/CONTRIBUTING.md
@@ -1,16 +1,15 @@
-## Contributing
+## Developer Certificate of Origin + License
-Thank you for your interest in contributing to this GitLab project! We welcome
-all contributions. By participating in this project, you agree to abide by the
-[code of conduct](#code-of-conduct).
+By contributing to GitLab B.V., You accept and agree to the following terms and
+conditions for Your present and future Contributions submitted to GitLab B.V.
+Except for the license granted herein to GitLab B.V. and recipients of software
+distributed by GitLab B.V., You reserve all right, title, and interest in and to
+Your Contributions. All Contributions are subject to the following DCO + License
+terms.
-## Contributor license agreement
+[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
-By submitting code as an individual you agree to the [individual contributor
-license agreement][individual-agreement].
-
-By submitting code as an entity you agree to the [corporate contributor license
-agreement][corporate-agreement].
+_This notice should stay as the first item in the CONTRIBUTING.md file._
## Code of conduct
diff --git a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
new file mode 100644
index 00000000000..4d5b6484d6e
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
@@ -0,0 +1,51 @@
+# This file uses Test Kitchen with the kitchen-dokken driver to
+# perform functional testing. Doing so requires that your runner be a
+# Docker runner configured for privileged mode. Please see
+# https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode
+# for help configuring your runner properly, or, if you want to switch
+# to a different driver, see http://kitchen.ci/docs/drivers
+
+image: "chef/chefdk"
+services:
+ - docker:dind
+
+variables:
+ DOCKER_HOST: "tcp://docker:2375"
+ KITCHEN_LOCAL_YAML: ".kitchen.dokken.yml"
+
+stages:
+ - lint
+ - unit
+ - functional
+
+foodcritic:
+ stage: lint
+ script:
+ - chef exec foodcritic .
+
+cookstyle:
+ stage: lint
+ script:
+ - chef exec cookstyle .
+
+chefspec:
+ stage: unit
+ script:
+ - chef exec rspec spec
+
+# Set up your test matrix here. Example:
+#verify-centos-6:
+# stage: functional
+# before_script:
+# - apt-get update
+# - apt-get -y install rsync
+# script:
+# - kitchen verify default-centos-6 --destroy=always
+#
+#verify-centos-7:
+# stage: functional
+# before_script:
+# - apt-get update
+# - apt-get -y install rsync
+# script:
+# - kitchen verify default-centos-7 --destroy=always
diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
index 86e4985d8d2..d572d7a1edc 100644
--- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
@@ -11,8 +11,8 @@ variables:
# repository in /go/src/gitlab.com/namespace/project
# Thus, making a symbolic link corrects this.
before_script:
- - mkdir -p $GOPATH/src/$REPO_NAME
- - ln -svf $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
+ - mkdir -p $GOPATH/src/$(dirname $REPO_NAME)
+ - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME
stages:
diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE
index d6c93c6fcf7..27a215686e7 100644
--- a/vendor/gitlab-ci-yml/LICENSE
+++ b/vendor/gitlab-ci-yml/LICENSE
@@ -1,6 +1,6 @@
-The MIT License (MIT)
+Copyright (c) 2011-2017 GitLab B.V.
-Copyright (c) 2016-2017 GitLab.org
+With regard to the GitLab Software:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,17 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+For all third party components incorporated into the GitLab Software, those
+components are licensed under the original license provided by the owner of the
+applicable component.
diff --git a/vendor/gitlab-ci-yml/Mono.gitlab-ci.yml b/vendor/gitlab-ci-yml/Mono.gitlab-ci.yml
new file mode 100644
index 00000000000..3585f99760f
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Mono.gitlab-ci.yml
@@ -0,0 +1,42 @@
+# This is a simple gitlab continuous integration template (compatible with the shared runner provided on gitlab.com)
+# using the official mono docker image to build a visual studio project.
+#
+# MyProject.sln
+# MyProject\
+# MyProject\
+# MyProject.csproj (console application)
+# MyProject.Test\
+# MyProject.Test.csproj (test library using nuget packages "NUnit" and "NUnit.ConsoleRunner")
+#
+# Please find the full example project here:
+# https://gitlab.com/tobiaskoch/gitlab-ci-example-mono
+
+# see https://hub.docker.com/_/mono/
+image: mono:latest
+
+stages:
+ - test
+ - deploy
+
+before_script:
+ - nuget restore -NonInteractive
+
+release:
+ stage: deploy
+ only:
+ - master
+ artifacts:
+ paths:
+ - build/release/MyProject.exe
+ script:
+ # The output path is relative to the position of the csproj-file
+ - msbuild /p:Configuration="Release" /p:Platform="Any CPU"
+ /p:OutputPath="./../../build/release/" "MyProject.sln"
+
+debug:
+ stage: test
+ script:
+ # The output path is relative to the position of the csproj-file
+ - msbuild /p:Configuration="Debug" /p:Platform="Any CPU"
+ /p:OutputPath="./../../build/debug/" "MyProject.sln"
+ - mono packages/NUnit.ConsoleRunner.3.6.0/tools/nunit3-console.exe build/debug/MyProject.Test.dll \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
index 6573eceaa59..cab087c48c7 100644
--- a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
@@ -20,4 +20,4 @@ image: "rust:latest"
test:cargo:
script:
- rustc --version && cargo --version # Print version info for debugging
- - cargo test --verbose --jobs 1 --release # Don't paralize to make errors more readable
+ - cargo test --all --verbose
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 6f6ca5f8b32..e3ccf080f74 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -1,462 +1,81 @@
-"","","MIT,ISC,Apache 2.0,New BSD,Simplified BSD"
RedCloth,4.3.2,MIT
-abbrev,1.0.9,ISC
-abbrev,1.1.0,ISC
-accepts,1.3.3,MIT
ace-rails-ap,4.1.2,MIT
-acorn,3.3.0,MIT
-acorn,4.0.13,MIT
-acorn,5.1.1,MIT
-acorn-dynamic-import,2.0.2,MIT
-acorn-jsx,3.0.1,MIT
-actionmailer,4.2.8,MIT
-actionpack,4.2.8,MIT
-actionview,4.2.8,MIT
-activejob,4.2.8,MIT
-activemodel,4.2.8,MIT
-activerecord,4.2.8,MIT
-activesupport,4.2.8,MIT
+actionmailer,4.2.10,MIT
+actionpack,4.2.10,MIT
+actionview,4.2.10,MIT
+activejob,4.2.10,MIT
+activemodel,4.2.10,MIT
+activerecord,4.2.10,MIT
+activesupport,4.2.10,MIT
acts-as-taggable-on,4.0.0,MIT
addressable,2.5.2,Apache 2.0
-after,0.8.2,MIT
-ajv,4.11.8,MIT
-ajv,5.2.2,MIT
-ajv-keywords,1.5.1,MIT
-ajv-keywords,2.1.0,MIT
akismet,2.0.0,MIT
-align-text,0.1.4,MIT
allocations,1.0.5,MIT
-alphanum-sort,1.0.2,MIT
-amdefine,1.0.1,BSD-3-Clause OR MIT
-ansi-escapes,1.4.0,MIT
-ansi-html,0.0.5,"Apache, Version 2.0"
-ansi-html,0.0.7,Apache 2.0
-ansi-regex,2.1.1,MIT
-ansi-styles,2.2.1,MIT
-ansi-styles,3.2.0,MIT
-anymatch,1.3.2,ISC
-append-transform,0.4.0,MIT
-aproba,1.1.1,ISC
-are-we-there-yet,1.1.4,ISC
arel,6.0.4,MIT
-argparse,1.0.9,MIT
-arr-diff,2.0.0,MIT
-arr-flatten,1.0.1,MIT
-array-find,1.0.0,MIT
-array-find-index,1.0.2,MIT
-array-flatten,1.1.1,MIT
-array-flatten,2.1.1,MIT
-array-slice,0.2.3,MIT
-array-union,1.0.2,MIT
-array-uniq,1.0.3,MIT
-array-unique,0.2.1,MIT
-arraybuffer.slice,0.0.6,MIT
-arrify,1.0.1,MIT
asana,0.6.0,MIT
asciidoctor,1.5.3,MIT
asciidoctor-plantuml,0.0.7,MIT
-asn1,0.2.3,MIT
-asn1.js,4.9.1,MIT
-assert,1.4.1,MIT
-assert-plus,0.2.0,MIT
-assert-plus,1.0.0,MIT
-async,0.9.2,MIT
-async,1.5.2,MIT
-async,2.4.1,MIT
-async-each,1.0.1,MIT
-asynckit,0.4.0,MIT
+asset_sync,2.2.0,MIT
atomic,1.1.99,Apache 2.0
attr_encrypted,3.0.3,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
-aws4,1.6.0,MIT
axiom-types,0.1.1,MIT
-axios,0.16.2,MIT
-babel-code-frame,6.22.0,MIT
-babel-core,6.23.1,MIT
-babel-eslint,7.2.1,MIT
-babel-generator,6.23.0,MIT
-babel-helper-bindify-decorators,6.22.0,MIT
-babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT
-babel-helper-call-delegate,6.22.0,MIT
-babel-helper-define-map,6.23.0,MIT
-babel-helper-explode-assignable-expression,6.22.0,MIT
-babel-helper-explode-class,6.22.0,MIT
-babel-helper-function-name,6.23.0,MIT
-babel-helper-get-function-arity,6.22.0,MIT
-babel-helper-hoist-variables,6.22.0,MIT
-babel-helper-optimise-call-expression,6.23.0,MIT
-babel-helper-regex,6.22.0,MIT
-babel-helper-remap-async-to-generator,6.22.0,MIT
-babel-helper-replace-supers,6.23.0,MIT
-babel-helpers,6.23.0,MIT
-babel-loader,7.1.1,MIT
-babel-messages,6.23.0,MIT
-babel-plugin-check-es2015-constants,6.22.0,MIT
-babel-plugin-istanbul,4.0.0,New BSD
-babel-plugin-syntax-async-functions,6.13.0,MIT
-babel-plugin-syntax-async-generators,6.13.0,MIT
-babel-plugin-syntax-class-properties,6.13.0,MIT
-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-object-rest-spread,6.13.0,MIT
-babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
-babel-plugin-transform-async-generator-functions,6.22.0,MIT
-babel-plugin-transform-async-to-generator,6.22.0,MIT
-babel-plugin-transform-class-properties,6.23.0,MIT
-babel-plugin-transform-decorators,6.22.0,MIT
-babel-plugin-transform-define,1.2.0,MIT
-babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT
-babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT
-babel-plugin-transform-es2015-block-scoping,6.23.0,MIT
-babel-plugin-transform-es2015-classes,6.23.0,MIT
-babel-plugin-transform-es2015-computed-properties,6.22.0,MIT
-babel-plugin-transform-es2015-destructuring,6.23.0,MIT
-babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT
-babel-plugin-transform-es2015-for-of,6.23.0,MIT
-babel-plugin-transform-es2015-function-name,6.22.0,MIT
-babel-plugin-transform-es2015-literals,6.22.0,MIT
-babel-plugin-transform-es2015-modules-amd,6.24.0,MIT
-babel-plugin-transform-es2015-modules-commonjs,6.24.0,MIT
-babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT
-babel-plugin-transform-es2015-modules-umd,6.24.0,MIT
-babel-plugin-transform-es2015-object-super,6.22.0,MIT
-babel-plugin-transform-es2015-parameters,6.23.0,MIT
-babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT
-babel-plugin-transform-es2015-spread,6.22.0,MIT
-babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT
-babel-plugin-transform-es2015-template-literals,6.22.0,MIT
-babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
-babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT
-babel-plugin-transform-exponentiation-operator,6.22.0,MIT
-babel-plugin-transform-object-rest-spread,6.23.0,MIT
-babel-plugin-transform-regenerator,6.22.0,MIT
-babel-plugin-transform-strict-mode,6.22.0,MIT
-babel-preset-es2015,6.24.0,MIT
-babel-preset-es2016,6.22.0,MIT
-babel-preset-es2017,6.22.0,MIT
-babel-preset-latest,6.24.0,MIT
-babel-preset-stage-2,6.22.0,MIT
-babel-preset-stage-3,6.22.0,MIT
-babel-register,6.23.0,MIT
-babel-runtime,6.22.0,MIT
-babel-template,6.23.0,MIT
-babel-traverse,6.23.1,MIT
-babel-types,6.23.0,MIT
babosa,1.0.2,MIT
-babylon,6.16.1,MIT
-backo2,1.0.2,MIT
-balanced-match,0.4.2,MIT
-balanced-match,1.0.0,MIT
base32,0.3.2,MIT
-base64-arraybuffer,0.1.5,MIT
-base64-js,1.2.0,MIT
-base64id,1.0.0,MIT
-batch,0.6.1,MIT
+batch-loader,1.2.1,MIT
bcrypt,3.1.11,MIT
-bcrypt-pbkdf,1.0.1,New BSD
bcrypt_pbkdf,1.0.0,MIT
-better-assert,1.0.2,MIT
-big.js,3.1.3,MIT
-binary-extensions,1.10.0,MIT
bindata,2.4.1,ruby
-blob,0.0.4,unknown
-block-stream,0.0.9,ISC
-bluebird,2.11.0,MIT
-bluebird,3.5.0,MIT
-bn.js,4.11.6,MIT
-body-parser,1.17.2,MIT
-bonjour,3.5.0,MIT
-boom,2.10.1,New BSD
bootstrap-sass,3.3.6,MIT
bootstrap_form,2.7.0,MIT
-brace-expansion,1.1.7,MIT
-brace-expansion,1.1.8,MIT
-braces,0.1.5,MIT
-braces,1.8.5,MIT
-brorand,1.0.7,MIT
browser,2.2.0,MIT
-browserify-aes,1.0.6,MIT
-browserify-cipher,1.0.0,MIT
-browserify-des,1.0.0,MIT
-browserify-rsa,4.0.1,MIT
-browserify-sign,4.0.0,ISC
-browserify-zlib,0.1.4,MIT
-browserslist,1.7.7,MIT
-buffer,4.9.1,MIT
-buffer-indexof,1.1.0,MIT
-buffer-shims,1.0.0,MIT
-buffer-xor,1.0.3,MIT
builder,3.2.3,MIT
-builtin-modules,1.1.1,MIT
-builtin-status-codes,3.0.0,MIT
-bytes,2.4.0,MIT
-bytes,2.5.0,MIT
-caller-path,0.1.0,MIT
-callsite,1.0.0,unknown
-callsites,0.2.0,MIT
-camelcase,1.2.1,MIT
-camelcase,2.1.1,MIT
-camelcase,3.0.0,MIT
-camelcase,4.1.0,MIT
-camelcase-keys,2.1.0,MIT
-caniuse-api,1.6.1,MIT
-caniuse-db,1.0.30000649,CC-BY-4.0
carrierwave,1.2.1,MIT
-caseless,0.12.0,Apache 2.0
cause,0.1,MIT
-center-align,0.1.3,MIT
-chalk,1.1.3,MIT
-chalk,2.3.0,MIT
charlock_holmes,0.7.5,MIT
-chokidar,1.7.0,MIT
chronic,0.10.2,MIT
chronic_duration,0.10.6,MIT
chunky_png,1.3.5,MIT
-cipher-base,1.0.3,MIT
-circular-json,0.3.3,MIT
citrus,3.0.2,MIT
-clap,1.1.3,MIT
-cli-cursor,1.0.2,MIT
-cli-width,2.1.0,ISC
-clipboard,1.6.1,MIT
-cliui,2.1.0,ISC
-cliui,3.2.0,ISC
-clone,1.0.2,MIT
-co,4.6.0,MIT
-coa,1.0.1,MIT
-code-point-at,1.1.0,MIT
coercible,1.0.0,MIT
-color,0.11.4,MIT
-color-convert,1.9.0,MIT
-color-name,1.1.2,MIT
-color-string,0.3.0,MIT
-colormin,1.1.2,MIT
-colors,1.1.2,MIT
-combine-lists,1.0.1,MIT
-combined-stream,1.0.5,MIT
-commander,2.9.0,MIT
-commondir,1.0.1,MIT
-component-bind,1.0.0,unknown
-component-emitter,1.1.2,unknown
-component-emitter,1.2.1,MIT
-component-inherit,0.0.3,unknown
-compressible,2.0.11,MIT
-compression,1.7.0,MIT
-compression-webpack-plugin,1.0.0,MIT
-concat-map,0.0.1,MIT
-concat-stream,1.6.0,MIT
concurrent-ruby-ext,1.0.5,MIT
-config-chain,1.1.11,MIT
-configstore,1.4.0,Simplified BSD
-connect,3.6.3,MIT
-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
-constants-browserify,1.0.0,MIT
-contains-path,0.1.0,MIT
-content-disposition,0.5.2,MIT
-content-type,1.0.2,MIT
-convert-source-map,1.3.0,MIT
-cookie,0.3.1,MIT
-cookie-signature,1.0.6,MIT
-copy-webpack-plugin,4.0.1,MIT
-core-js,2.3.0,MIT
-core-js,2.4.1,MIT
-core-util-is,1.0.2,MIT
-cosmiconfig,2.1.1,MIT
crack,0.4.3,MIT
-create-ecdh,4.0.0,MIT
-create-hash,1.1.2,MIT
-create-hmac,1.1.4,MIT
creole,0.5.0,ruby
-cropper,2.3.0,MIT
-cross-spawn,5.1.0,MIT
-cryptiles,2.0.5,New BSD
-crypto-browserify,3.11.0,MIT
-css-color-names,0.0.4,MIT
-css-loader,0.28.0,MIT
-css-selector-tokenizer,0.6.0,MIT
-css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
-cssesc,0.1.0,MIT
-cssnano,3.10.0,MIT
-csso,2.3.2,MIT
-currently-unhandled,0.4.1,MIT
-custom-event,1.0.1,MIT
-d,0.1.1,MIT
-d,1.0.0,MIT
-d3,3.5.11,New BSD
d3_rails,3.5.11,MIT
-dashdash,1.14.1,MIT
-date-now,0.1.4,MIT
-de-indent,1.0.2,MIT
-debug,2.2.0,MIT
-debug,2.3.3,MIT
-debug,2.6.7,MIT
-debug,2.6.8,MIT
debugger-ruby_core_source,1.3.8,MIT
-decamelize,1.2.0,MIT
deckar01-task_list,2.0.0,MIT
declarative,0.0.10,MIT
declarative-option,0.1.0,MIT
-decompress-response,3.3.0,MIT
-deep-equal,1.0.1,MIT
-deep-extend,0.4.2,MIT
-deep-is,0.1.3,MIT
-default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
-defined,1.0.0,MIT
-del,2.2.2,MIT
-del,3.0.0,MIT
-delayed-stream,1.0.0,MIT
-delegate,3.1.2,MIT
-delegates,1.0.0,MIT
-depd,1.1.0,MIT
-depd,1.1.1,MIT
-des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
-destroy,1.0.4,MIT
-detect-indent,4.0.0,MIT
-detect-node,2.0.3,ISC
devise,4.2.0,MIT
devise-two-factor,3.0.0,MIT
-di,0.0.1,MIT
diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+"
-diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
-dns-equal,1.0.0,MIT
-dns-packet,1.2.2,MIT
-dns-txt,2.0.2,MIT
-doctrine,1.5.0,BSD
-doctrine,2.0.0,Apache 2.0
-document-register-element,1.3.0,MIT
-dom-serialize,2.2.1,MIT
-dom-serializer,0.1.0,MIT
-domain-browser,1.1.7,MIT
domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
-domelementtype,1.1.3,unknown
-domelementtype,1.3.0,unknown
-domhandler,2.3.0,unknown
-domutils,1.5.1,unknown
doorkeeper,4.2.6,MIT
doorkeeper-openid_connect,1.2.0,MIT
-dropzone,4.2.0,MIT
dropzonejs-rails,0.7.2,MIT
-duplexer,0.1.1,MIT
-duplexer3,0.1.4,New BSD
-duplexify,3.5.1,MIT
-ecc-jsbn,0.1.1,MIT
-editorconfig,0.13.2,MIT
-ee-first,1.1.1,MIT
-ejs,2.5.6,Apache 2.0
-electron-to-chromium,1.3.3,ISC
-elliptic,6.3.3,MIT
email_reply_trimmer,0.1.6,MIT
-emoji-unicode-version,0.2.1,MIT
-emojis-list,2.1.0,MIT
-encodeurl,1.0.1,MIT
encryptor,3.0.0,MIT
-end-of-stream,1.4.0,MIT
-engine.io,1.8.3,MIT
-engine.io-client,1.8.3,MIT
-engine.io-parser,1.3.2,MIT
-enhanced-resolve,0.9.1,MIT
-enhanced-resolve,3.4.1,MIT
-ent,2.2.0,MIT
-entities,1.1.1,BSD-like
equalizer,0.0.11,MIT
-errno,0.1.4,MIT
-error-ex,1.3.0,MIT
erubis,2.7.0,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
-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-plugin-jasmine,2.2.0,MIT
-eslint-plugin-promise,3.5.0,ISC
-espree,3.5.0,Simplified BSD
-esprima,2.7.3,Simplified BSD
-esprima,4.0.0,Simplified BSD
-esquery,1.0.0,BSD
-esrecurse,4.1.0,Simplified BSD
-estraverse,1.9.3,BSD
-estraverse,4.1.1,Simplified BSD
-estraverse,4.2.0,Simplified BSD
-esutils,2.0.2,BSD
et-orbi,1.0.3,MIT
-etag,1.8.0,MIT
-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
-eventsource,0.1.6,MIT
-evp_bytestokey,1.0.0,MIT
excon,0.57.1,MIT
-execa,0.7.0,MIT
execjs,2.6.0,MIT
-exit-hook,1.1.1,MIT
-expand-braces,0.1.2,MIT
-expand-brackets,0.1.5,MIT
-expand-range,0.1.1,MIT
-expand-range,1.8.2,MIT
-exports-loader,0.6.4,MIT
-express,4.15.4,MIT
expression_parser,0.9.0,MIT
-extend,3.0.1,MIT
-extglob,0.3.2,MIT
-extsprintf,1.0.2,MIT
faraday,0.12.2,MIT
faraday_middleware,0.11.0.1,MIT
faraday_middleware-multi_json,0.0.6,MIT
-fast-deep-equal,1.0.0,MIT
-fast-levenshtein,2.0.6,MIT
fast_gettext,1.4.0,"MIT,ruby"
-fastparse,1.1.1,MIT
-faye-websocket,0.10.0,MIT
-faye-websocket,0.11.1,MIT
-faye-websocket,0.7.3,MIT
ffi,1.9.18,New BSD
-figures,1.7.0,MIT
-file-entry-cache,2.0.0,MIT
-file-loader,0.11.1,MIT
-filename-regex,2.0.0,MIT
-fileset,2.0.3,MIT
-filesize,3.3.0,New BSD
-filesize,3.5.10,New BSD
-fill-range,2.2.3,MIT
-finalhandler,1.0.4,MIT
-find-cache-dir,1.0.0,MIT
-find-root,0.1.2,MIT
-find-up,1.1.2,MIT
-find-up,2.1.0,MIT
-flat-cache,1.2.2,MIT
-flatten,1.0.2,MIT
-flipper,0.10.2,MIT
-flipper-active_record,0.10.2,MIT
+flipper,0.11.0,MIT
+flipper-active_record,0.11.0,MIT
+flipper-active_support_cache_store,0.11.0,MIT
flowdock,0.7.1,MIT
fog-aliyun,0.2.0,MIT
fog-aws,1.4.0,MIT
@@ -467,392 +86,93 @@ fog-local,0.3.1,MIT
fog-openstack,0.1.21,MIT
fog-rackspace,0.1.1,MIT
fog-xml,0.1.3,MIT
-follow-redirects,1.2.3,MIT
font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License"
-for-in,0.1.6,MIT
-for-own,0.1.4,MIT
-forever-agent,0.6.1,Apache 2.0
-form-data,2.1.4,MIT
formatador,0.2.5,MIT
-forwarded,0.1.0,MIT
-fresh,0.5.0,MIT
-from,0.1.7,MIT
-fs-access,1.0.1,MIT
-fs-extra,0.26.7,MIT
-fs.realpath,1.0.0,ISC
-fsevents,1.1.2,MIT
-fstream,1.0.11,ISC
-fstream-ignore,1.0.5,ISC
-function-bind,1.1.0,MIT
-fuzzaldrin-plus,0.5.0,MIT
-gauge,2.7.4,ISC
gemnasium-gitlab-service,0.2.6,MIT
gemojione,3.3.0,MIT
-generate-function,2.0.0,MIT
-generate-object-property,1.2.0,MIT
-get-caller-file,1.0.2,ISC
-get-stdin,4.0.1,MIT
-get-stream,3.0.0,MIT
get_process_mem,0.2.0,MIT
-getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.2.0,MIT
-gitaly-proto,0.51.0,MIT
+gitaly-proto,0.64.0,MIT
github-linguist,4.7.6,MIT
github-markup,1.6.1,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
gitlab-grit,2.8.2,MIT
gitlab-markup,1.6.3,MIT
-gitlab-svgs,1.0.4,unknown
gitlab_omniauth-ldap,2.0.4,MIT
-glob,5.0.15,ISC
-glob,6.0.4,ISC
-glob,7.1.1,ISC
-glob,7.1.2,ISC
-glob-base,0.3.0,MIT
-glob-parent,2.0.0,ISC
-globalid,0.3.7,MIT
-globals,9.18.0,MIT
-globby,5.0.0,MIT
-globby,6.1.0,MIT
+globalid,0.4.1,MIT
gollum-grit_adapter,1.0.1,MIT
gollum-lib,4.2.7,MIT
gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
-good-listener,1.2.2,MIT
google-api-client,0.13.6,Apache 2.0
google-protobuf,3.4.1.1,New BSD
-googleapis-common-protos-types,1.0.0,Apache 2.0
googleauth,0.5.3,Apache 2.0
-got,3.3.1,MIT
-got,7.1.0,MIT
gpgme,2.0.13,LGPL-2.1+
-graceful-fs,4.1.11,ISC
-graceful-readlink,1.0.1,MIT
grape,1.0.0,MIT
grape-entity,0.6.0,MIT
grape-route-helpers,2.1.0,MIT
grape_logging,1.7.0,MIT
-grpc,1.6.6,Apache 2.0
-gzip-size,3.0.0,MIT
+grpc,1.4.5,New BSD
hamlit,2.6.1,MIT
-handle-thing,1.2.5,MIT
-handlebars,4.0.6,MIT
-har-schema,1.0.5,ISC
-har-validator,4.2.1,ISC
-has,1.0.1,MIT
-has-ansi,2.0.0,MIT
-has-binary,0.1.7,MIT
-has-cors,1.1.0,MIT
-has-flag,1.0.0,MIT
-has-flag,2.0.0,MIT
-has-symbol-support-x,1.3.0,MIT
-has-to-string-tag-x,1.3.0,MIT
-has-unicode,2.0.1,ISC
-hash-sum,1.0.2,MIT
-hash.js,1.0.3,MIT
hashie,3.5.6,MIT
hashie-forbidden_attributes,0.1.1,MIT
-hawk,3.1.3,New BSD
-he,1.1.1,MIT
health_check,2.6.0,MIT
hipchat,1.5.2,MIT
-hoek,2.16.3,New BSD
-home-or-tmp,2.0.0,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,1.11.0,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
-htmlparser2,3.9.2,MIT
http,0.9.8,MIT
http-cookie,1.0.3,MIT
-http-deceiver,1.2.7,MIT
-http-errors,1.6.1,MIT
-http-errors,1.6.2,MIT
http-form_data,1.0.1,MIT
-http-proxy,1.16.2,MIT
-http-proxy-middleware,0.17.4,MIT
-http-signature,1.1.1,MIT
http_parser.rb,0.6.0,MIT
httparty,0.13.7,MIT
httpclient,2.8.2,ruby
-https-browserify,0.0.1,MIT
-i18n,0.8.6,MIT
+i18n,0.9.1,MIT
ice_nine,0.11.2,MIT
-iconv-lite,0.4.15,MIT
-icss-replace-symbols,1.0.2,ISC
-ieee754,1.1.8,New BSD
-ignore,3.3.3,MIT
-ignore-by-default,1.0.1,ISC
-immediate,3.0.6,MIT
-imports-loader,0.7.1,MIT
-imurmurhash,0.1.4,MIT
-indent-string,2.1.0,MIT
-indexes-of,1.0.1,MIT
-indexof,0.0.1,unknown
-infinity-agent,2.0.3,MIT
-inflight,1.0.6,ISC
influxdb,0.2.3,MIT
-inherits,2.0.1,ISC
-inherits,2.0.3,ISC
-ini,1.3.4,ISC
-inquirer,0.12.0,MIT
-internal-ip,1.2.0,MIT
-interpret,1.0.1,MIT
-invariant,2.2.2,New BSD
-invert-kv,1.0.0,MIT
-ip,1.1.5,MIT
-ipaddr.js,1.4.0,MIT
ipaddress,0.8.3,MIT
-is-absolute,0.2.6,MIT
-is-absolute-url,2.1.0,MIT
-is-arrayish,0.2.1,MIT
-is-binary-path,1.0.1,MIT
-is-buffer,1.1.5,MIT
-is-builtin-module,1.0.0,MIT
-is-dotfile,1.0.2,MIT
-is-equal-shallow,0.1.3,MIT
-is-extendable,0.1.1,MIT
-is-extglob,1.0.0,MIT
-is-extglob,2.1.1,MIT
-is-finite,1.0.2,MIT
-is-fullwidth-code-point,1.0.0,MIT
-is-fullwidth-code-point,2.0.0,MIT
-is-glob,2.0.1,MIT
-is-glob,3.1.0,MIT
-is-my-json-valid,2.16.0,MIT
-is-npm,1.0.0,MIT
-is-number,0.1.1,MIT
-is-number,2.1.0,MIT
-is-object,1.0.1,MIT
-is-path-cwd,1.0.0,MIT
-is-path-in-cwd,1.0.0,MIT
-is-path-inside,1.0.0,MIT
-is-plain-obj,1.1.0,MIT
-is-posix-bracket,0.1.1,MIT
-is-primitive,2.0.0,MIT
-is-property,1.0.2,MIT
-is-redirect,1.0.0,MIT
-is-relative,0.2.1,MIT
-is-resolvable,1.0.0,MIT
-is-retry-allowed,1.1.0,MIT
-is-stream,1.1.0,MIT
-is-svg,2.1.0,MIT
-is-typedarray,1.0.0,MIT
-is-unc-path,0.1.2,MIT
-is-utf8,0.2.1,MIT
-is-windows,0.2.0,MIT
-isarray,0.0.1,MIT
-isarray,1.0.0,MIT
-isbinaryfile,3.0.2,MIT
-isexe,1.1.2,ISC
-isobject,2.1.0,MIT
-isstream,0.1.2,MIT
-istanbul,0.4.5,New BSD
-istanbul-api,1.1.1,New BSD
-istanbul-lib-coverage,1.0.1,New BSD
-istanbul-lib-hook,1.0.0,New BSD
-istanbul-lib-instrument,1.4.2,New BSD
-istanbul-lib-report,1.0.0-alpha.3,New BSD
-istanbul-lib-source-maps,1.1.0,New BSD
-istanbul-reports,1.0.1,New BSD
-isurl,1.0.0,MIT
-jasmine-core,2.6.3,MIT
-jasmine-jquery,2.1.1,MIT
-jed,1.1.1,MIT
jira-ruby,1.4.1,MIT
-jodid25519,1.0.2,MIT
-jquery,2.2.1,MIT
jquery-atwho-rails,1.3.2,MIT
-jquery-rails,4.1.1,MIT
-jquery-ujs,1.2.1,MIT
-js-base64,2.1.9,BSD
-js-beautify,1.6.12,MIT
-js-cookie,2.1.3,MIT
-js-tokens,3.0.1,MIT
-js-yaml,3.7.0,MIT
-js-yaml,3.9.1,MIT
-jsbn,0.1.1,MIT
-jsesc,0.5.0,MIT
-jsesc,1.3.0,MIT
+jquery-rails,4.3.1,MIT
json,1.8.6,ruby
json-jwt,1.7.2,MIT
-json-loader,0.5.7,MIT
-json-schema,0.2.3,"AFLv2.1,BSD"
-json-schema-traverse,0.3.1,MIT
-json-stable-stringify,1.0.1,MIT
-json-stringify-safe,5.0.1,ISC
-json3,3.3.2,MIT
-json5,0.5.1,MIT
-jsonfile,2.4.0,MIT
-jsonify,0.0.0,Public Domain
-jsonpointer,4.0.1,MIT
-jsprim,1.4.0,MIT
-jszip,3.1.3,(MIT OR GPL-3.0)
-jszip-utils,0.0.2,MIT or GPLv3
jwt,1.5.6,MIT
kaminari,1.0.1,MIT
kaminari-actionview,1.0.1,MIT
kaminari-activerecord,1.0.1,MIT
kaminari-core,1.0.1,MIT
-karma,1.7.0,MIT
-karma-chrome-launcher,2.1.1,MIT
-karma-coverage-istanbul-reporter,0.2.0,MIT
-karma-jasmine,1.1.0,MIT
-karma-mocha-reporter,2.2.2,MIT
-karma-sourcemap-loader,0.3.7,MIT
-karma-webpack,2.0.4,MIT
kgio,2.10.0,LGPL-2.1+
-kind-of,3.1.0,MIT
-klaw,1.3.1,MIT
kubeclient,2.2.0,MIT
-latest-version,1.0.1,MIT
-lazy-cache,1.0.4,MIT
-lcid,1.0.0,MIT
-levn,0.3.0,MIT
licensee,8.7.0,MIT
-lie,3.1.1,MIT
little-plugger,1.1.4,MIT
-load-json-file,1.1.0,MIT
-load-json-file,2.0.0,MIT
-loader-runner,2.3.0,MIT
-loader-utils,0.2.16,MIT
-loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
-locate-path,2.0.0,MIT
-lodash,3.10.1,MIT
-lodash,4.17.4,MIT
-lodash._baseassign,3.2.0,MIT
-lodash._basecopy,3.0.1,MIT
-lodash._baseget,3.7.2,MIT
-lodash._bindcallback,3.0.1,MIT
-lodash._createassigner,3.1.1,MIT
-lodash._getnative,3.9.1,MIT
-lodash._isiterateecall,3.0.9,MIT
-lodash._topath,3.8.1,MIT
-lodash.assign,3.2.0,MIT
-lodash.camelcase,4.1.1,MIT
-lodash.camelcase,4.3.0,MIT
-lodash.capitalize,4.2.1,MIT
-lodash.cond,4.5.2,MIT
-lodash.deburr,4.1.0,MIT
-lodash.defaults,3.1.2,MIT
-lodash.get,3.7.0,MIT
-lodash.get,4.4.2,MIT
-lodash.isarguments,3.1.0,MIT
-lodash.isarray,3.0.4,MIT
-lodash.kebabcase,4.0.1,MIT
-lodash.keys,3.1.2,MIT
-lodash.memoize,4.1.2,MIT
-lodash.restparam,3.6.1,MIT
-lodash.snakecase,4.0.1,MIT
-lodash.uniq,4.5.0,MIT
-lodash.words,4.2.0,MIT
-log4js,0.6.38,Apache 2.0
logging,2.2.2,MIT
-loglevel,1.4.1,MIT
lograge,0.5.1,MIT
-longest,1.0.1,MIT
loofah,2.0.3,MIT
-loose-envify,1.3.1,MIT
-loud-rejection,1.6.0,MIT
-lowercase-keys,1.0.0,MIT
-lru-cache,2.2.4,MIT
-lru-cache,3.2.0,ISC
-lru-cache,4.0.2,ISC
-macaddress,0.2.8,MIT
-mail,2.6.6,MIT
+mail,2.7.0,MIT
mail_room,0.9.1,MIT
-make-dir,1.0.0,MIT
-map-obj,1.0.1,MIT
-map-stream,0.1.0,unknown
-marked,0.3.6,MIT
-math-expression-evaluator,1.2.16,MIT
-media-typer,0.3.0,MIT
-mem,1.1.0,MIT
memoist,0.16.0,MIT
-memory-fs,0.2.0,MIT
-memory-fs,0.4.1,MIT
-meow,3.7.0,MIT
-merge-descriptors,1.0.1,MIT
method_source,0.8.2,MIT
-methods,1.1.2,MIT
-micromatch,2.3.11,MIT
-miller-rabin,4.0.0,MIT
-mime,1.3.4,MIT
-mime-db,1.27.0,MIT
-mime-db,1.29.0,MIT
-mime-types,2.1.15,MIT
mime-types,3.1,MIT
mime-types-data,3.2016.0521,MIT
mimemagic,0.3.0,MIT
-mimic-fn,1.1.0,MIT
-mimic-response,1.0.0,MIT
+mini_mime,0.1.4,MIT
mini_portile2,2.3.0,MIT
-minimalistic-assert,1.0.0,ISC
-minimatch,3.0.3,ISC
-minimatch,3.0.4,ISC
-minimist,0.0.8,MIT
-minimist,1.2.0,MIT
-mkdirp,0.5.1,MIT
-mmap2,2.2.7,ruby
-moment,2.17.1,MIT
-monaco-editor,0.10.0,MIT
-mousetrap,1.4.6,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
-ms,0.7.1,MIT
-ms,0.7.2,MIT
-ms,2.0.0,MIT
multi_json,1.12.2,MIT
multi_xml,0.6.0,MIT
-multicast-dns,6.1.1,MIT
-multicast-dns-service-types,1.1.0,MIT
multipart-post,2.0.0,MIT
mustermann,1.0.0,MIT
mustermann-grape,1.0.0,MIT
-mute-stream,0.0.5,ISC
-mysql2,0.4.5,MIT
-name-all-modules-plugin,1.0.1,MIT
-nan,2.6.2,MIT
-natural-compare,1.4.0,MIT
-negotiator,0.6.1,MIT
-nested-error-stacks,1.0.2,MIT
+mysql2,0.4.10,MIT
net-ldap,0.16.0,MIT
net-ssh,4.1.0,MIT
netrc,0.11.0,MIT
-node-dir,0.1.17,MIT
-node-forge,0.6.33,BSD
-node-libs-browser,1.1.1,MIT
-node-libs-browser,2.0.0,MIT
-node-pre-gyp,0.6.36,New BSD
-node-pre-gyp,0.6.37,New BSD
-nodemon,1.11.0,MIT
nokogiri,1.8.1,MIT
-nopt,1.0.10,MIT
-nopt,3.0.6,ISC
-nopt,4.0.1,ISC
-normalize-package-data,2.4.0,Simplified BSD
-normalize-path,2.1.1,MIT
-normalize-range,0.1.2,MIT
-normalize-url,1.9.1,MIT
-npm-run-path,2.0.2,MIT
-npmlog,4.1.0,ISC
-null-check,1.0.0,MIT
-num2fraction,1.2.2,MIT
-number-is-nan,1.0.1,MIT
numerizer,0.1.1,MIT
oauth,0.5.1,MIT
-oauth-sign,0.8.2,Apache 2.0
oauth2,1.4.0,MIT
-object-assign,3.0.0,MIT
-object-assign,4.1.0,MIT
-object-assign,4.1.1,MIT
-object-component,0.0.3,unknown
-object.omit,2.0.1,MIT
-obuf,1.1.1,MIT
octokit,4.6.2,MIT
oj,2.17.5,MIT
omniauth,1.4.2,MIT
@@ -873,54 +193,10 @@ omniauth-saml,1.7.0,MIT
omniauth-shibboleth,1.2.1,MIT
omniauth-twitter,1.2.1,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
-opener,1.4.3,(WTFPL OR MIT)
-opn,4.0.2,MIT
-optimist,0.6.1,MIT/X11
-optionator,0.8.2,MIT
-options,0.0.6,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-homedir,1.0.2,MIT
-os-locale,1.4.0,MIT
-os-locale,2.1.0,MIT
-os-tmpdir,1.0.2,MIT
-osenv,0.1.4,ISC
-p-cancelable,0.3.0,MIT
-p-finally,1.0.0,MIT
-p-limit,1.1.0,MIT
-p-locate,2.0.0,MIT
-p-map,1.1.1,MIT
-p-timeout,1.2.0,MIT
-package-json,1.2.0,MIT
-pako,0.2.9,MIT
-pako,1.0.5,(MIT AND Zlib)
paranoia,2.3.1,MIT
-parse-asn1,5.0.0,ISC
-parse-glob,3.0.4,MIT
-parse-json,2.2.0,MIT
-parsejson,0.0.3,MIT
-parseqs,0.0.5,MIT
-parseuri,0.0.5,MIT
-parseurl,1.3.1,MIT
-path-browserify,0.0.0,MIT
-path-exists,2.1.0,MIT
-path-exists,3.0.0,MIT
-path-is-absolute,1.0.1,MIT
-path-is-inside,1.0.2,(WTFPL OR MIT)
-path-key,2.0.1,MIT
-path-parse,1.0.5,MIT
-path-to-regexp,0.1.7,MIT
-path-type,1.1.0,MIT
-path-type,2.0.0,MIT
-pause-stream,0.0.11,"MIT,Apache2"
-pbkdf2,3.0.9,MIT
peek,1.0.1,MIT
peek-gc,0.0.2,MIT
peek-host,1.0.0,MIT
@@ -930,86 +206,14 @@ 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
pg,0.18.4,"BSD,ruby,GPL"
-pify,2.3.0,MIT
-pify,3.0.0,MIT
-pikaday,1.6.1,MIT
-pinkie,2.0.4,MIT
-pinkie-promise,2.0.1,MIT
-pkg-dir,1.0.0,MIT
-pkg-dir,2.0.0,MIT
-pkg-up,1.0.0,MIT
-pluralize,1.2.1,MIT
po_to_json,1.0.1,MIT
-portfinder,1.0.13,MIT
posix-spawn,0.3.13,MIT
-postcss,5.2.16,MIT
-postcss-calc,5.3.1,MIT
-postcss-colormin,2.2.2,MIT
-postcss-convert-values,2.6.1,MIT
-postcss-discard-comments,2.0.4,MIT
-postcss-discard-duplicates,2.1.0,MIT
-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
-postcss-message-helpers,2.0.0,MIT
-postcss-minify-font-values,1.0.5,MIT
-postcss-minify-gradients,1.0.5,MIT
-postcss-minify-params,1.2.2,MIT
-postcss-minify-selectors,2.1.1,MIT
-postcss-modules-extract-imports,1.0.1,ISC
-postcss-modules-local-by-default,1.1.1,MIT
-postcss-modules-scope,1.0.2,ISC
-postcss-modules-values,1.2.2,ISC
-postcss-normalize-charset,1.1.1,MIT
-postcss-normalize-url,3.0.8,MIT
-postcss-ordered-values,2.2.3,MIT
-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-svgo,2.1.6,MIT
-postcss-unique-selectors,2.0.2,MIT
-postcss-value-parser,3.3.0,MIT
-postcss-zindex,2.2.0,MIT
-prelude-ls,1.1.2,MIT
premailer,1.10.4,New BSD
premailer-rails,1.9.7,MIT
-prepend-http,1.0.4,MIT
-preserve,0.2.0,MIT
-prismjs,1.6.0,MIT
-private,0.1.7,MIT
-process,0.11.9,MIT
-process-nextick-args,1.0.7,MIT
-progress,1.1.8,MIT
-prometheus-client-mmap,0.7.0.beta18,Apache 2.0
-proto-list,1.2.4,ISC
-proxy-addr,1.1.5,MIT
-prr,0.0.0,MIT
-ps-tree,1.1.0,MIT
-pseudomap,1.0.2,ISC
-public-encrypt,4.0.0,MIT
+prometheus-client-mmap,0.7.0.beta44,Apache 2.0
public_suffix,3.0.0,MIT
-punycode,1.3.2,MIT
-punycode,1.4.1,MIT
pyu-ruby-sasl,0.0.3.3,MIT
-q,1.5.0,MIT
-qjobs,1.1.5,MIT
-qs,6.4.0,New BSD
-qs,6.5.0,New BSD
-query-string,4.3.2,MIT
-querystring,0.2.0,MIT
-querystring-es3,0.2.1,MIT
-querystringify,0.0.4,MIT
-querystringify,1.0.0,MIT
rack,1.6.8,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
@@ -1018,88 +222,35 @@ rack-oauth2,1.2.3,MIT
rack-protection,1.5.3,MIT
rack-proxy,0.6.0,MIT
rack-test,0.6.3,MIT
-rails,4.2.8,MIT
+rails,4.2.10,MIT
rails-deprecated_sanitizer,1.0.3,MIT
rails-dom-testing,1.0.8,MIT
rails-html-sanitizer,1.0.3,MIT
rails-i18n,4.0.9,MIT
-railties,4.2.8,MIT
+railties,4.2.10,MIT
rainbow,2.2.2,MIT
raindrops,0.18.0,LGPL-2.1+
-rake,12.1.0,MIT
-randomatic,1.1.6,MIT
-randombytes,2.0.3,MIT
-range-parser,1.2.0,MIT
-raphael,2.2.7,MIT
-raven-js,3.14.0,Simplified BSD
-raw-body,2.2.0,MIT
-raw-loader,0.5.1,MIT
+rake,12.3.0,MIT
rbnacl,4.0.2,MIT
rbnacl-libsodium,1.0.11,MIT
-rc,1.2.1,(BSD-2-Clause OR MIT OR Apache-2.0)
rdoc,4.2.2,ruby
re2,1.1.1,New BSD
-react-dev-utils,0.5.2,New BSD
-read-all-stream,3.1.0,MIT
-read-pkg,1.1.0,MIT
-read-pkg,2.0.0,MIT
-read-pkg-up,1.0.1,MIT
-read-pkg-up,2.0.0,MIT
-readable-stream,1.0.34,MIT
-readable-stream,2.0.6,MIT
-readable-stream,2.2.9,MIT
-readable-stream,2.3.3,MIT
-readdirp,2.1.0,MIT
-readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
-rechoir,0.6.2,MIT
recursive-open-struct,1.0.0,MIT
-recursive-readdir,2.1.1,MIT
redcarpet,3.4.0,MIT
-redent,1.0.0,MIT
-redis,3.3.3,MIT
-redis-actionpack,5.0.1,MIT
-redis-activesupport,5.0.1,MIT
+redis,3.3.5,MIT
+redis-actionpack,5.0.2,MIT
+redis-activesupport,5.0.4,MIT
redis-namespace,1.5.2,MIT
-redis-rack,1.6.0,MIT
-redis-rails,5.0.1,MIT
-redis-store,1.2.0,MIT
-reduce-css-calc,1.3.0,MIT
-reduce-function-call,1.0.2,MIT
-regenerate,1.3.2,MIT
-regenerator-runtime,0.10.1,MIT
-regenerator-transform,0.9.8,BSD
-regex-cache,0.4.3,MIT
-regexpu-core,1.0.0,MIT
-regexpu-core,2.0.0,MIT
-registry-url,3.1.0,MIT
-regjsgen,0.2.0,MIT
-regjsparser,0.1.5,BSD
-remove-trailing-separator,1.1.0,ISC
-repeat-element,1.1.2,MIT
-repeat-string,0.2.2,MIT
-repeat-string,1.6.1,MIT
-repeating,1.1.3,MIT
-repeating,2.0.1,MIT
+redis-rack,2.0.4,MIT
+redis-rails,5.0.2,MIT
+redis-store,1.4.1,MIT
representable,3.0.4,MIT
-request,2.81.0,Apache 2.0
request_store,1.3.1,MIT
-require-directory,2.1.1,MIT
-require-from-string,1.2.1,MIT
-require-main-filename,1.0.1,ISC
-require-uncached,1.0.3,MIT
-requires-port,1.0.0,MIT
-resolve,1.1.7,MIT
-resolve,1.2.0,MIT
-resolve-from,1.0.1,MIT
responders,2.3.0,MIT
rest-client,2.0.0,MIT
-restore-cursor,1.0.1,MIT
retriable,3.1.1,MIT
-right-align,0.1.3,MIT
-rimraf,2.6.1,ISC
rinku,2.0.0,ISC
-ripemd160,1.0.1,New BSD
rotp,2.1.2,MIT
rouge,2.2.1,MIT
rqrcode,0.7.0,MIT
@@ -1112,244 +263,51 @@ rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
rugged,0.26.0,MIT
-run-async,0.1.0,MIT
-rx-lite,3.1.2,Apache 2.0
-safe-buffer,5.0.1,MIT
-safe-buffer,5.1.1,MIT
safe_yaml,1.0.4,MIT
sanitize,2.1.0,MIT
sass,3.4.22,MIT
sass-rails,5.0.6,MIT
sawyer,0.8.1,MIT
-sax,1.2.2,ISC
securecompare,1.0.0,MIT
seed-fu,2.3.6,MIT
-select,1.1.2,MIT
-select-hose,2.0.0,MIT
-select2,3.5.2-browserify,unknown
select2-rails,3.5.9.3,MIT
-selfsigned,1.10.1,MIT
-semver,4.3.6,ISC
-semver,5.3.0,ISC
-semver-diff,2.1.0,MIT
-send,0.15.4,MIT
sentry-raven,2.5.3,Apache 2.0
-serve-index,1.9.0,MIT
-serve-static,1.12.4,MIT
-set-blocking,2.0.0,ISC
-set-immediate-shim,1.0.1,MIT
-setimmediate,1.0.5,MIT
-setprototypeof,1.0.3,ISC
settingslogic,2.0.9,MIT
sexp_processor,4.9.0,MIT
-sha.js,2.4.8,MIT
-shebang-command,1.2.0,MIT
-shebang-regex,1.0.0,MIT
-shelljs,0.7.8,New BSD
-sidekiq,5.0.4,LGPL
+sidekiq,5.0.5,LGPL
sidekiq-cron,0.6.0,MIT
sidekiq-limit_fetch,3.4.0,MIT
-sigmund,1.0.1,ISC
-signal-exit,3.0.2,ISC
signet,0.7.3,Apache 2.0
slack-notifier,1.5.1,MIT
-slash,1.0.0,MIT
-slice-ansi,0.0.4,MIT
-slide,1.1.6,ISC
-sntp,1.0.9,BSD
-socket.io,1.7.3,MIT
-socket.io-adapter,0.5.0,MIT
-socket.io-client,1.7.3,MIT
-socket.io-parser,2.3.1,MIT
-sockjs,0.3.18,MIT
-sockjs-client,1.0.1,MIT
-sockjs-client,1.1.4,MIT
-sort-keys,1.1.2,MIT
-source-list-map,0.1.8,MIT
-source-list-map,2.0.0,MIT
-source-map,0.1.43,BSD
-source-map,0.2.0,BSD
-source-map,0.4.4,New BSD
-source-map,0.5.6,New BSD
-source-map-support,0.4.11,MIT
-spdx-correct,1.0.2,Apache 2.0
-spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0)
-spdx-license-ids,1.2.2,Unlicense
-spdy,3.4.7,MIT
-spdy-transport,2.0.20,MIT
-split,0.3.3,MIT
-sprintf-js,1.0.3,New BSD
sprockets,3.7.1,MIT
-sprockets-rails,3.2.0,MIT
-sql.js,0.4.0,MIT
-sshpk,1.13.0,MIT
+sprockets-rails,3.2.1,MIT
state_machines,0.4.0,MIT
state_machines-activemodel,0.4.0,MIT
state_machines-activerecord,0.4.0,MIT
-statuses,1.3.1,MIT
-stream-browserify,2.0.1,MIT
-stream-combiner,0.0.4,MIT
-stream-http,2.6.3,MIT
-stream-shift,1.0.0,MIT
-strict-uri-encode,1.1.0,MIT
-string-length,1.0.1,MIT
-string-width,1.0.2,MIT
-string-width,2.0.0,MIT
-string_decoder,0.10.31,MIT
-string_decoder,1.0.1,MIT
-string_decoder,1.0.3,MIT
stringex,2.7.1,MIT
-stringstream,0.0.5,MIT
-strip-ansi,3.0.1,MIT
-strip-bom,2.0.0,MIT
-strip-bom,3.0.0,MIT
-strip-eof,1.0.0,MIT
-strip-indent,1.0.1,MIT
-strip-json-comments,2.0.1,MIT
-supports-color,2.0.0,MIT
-supports-color,3.2.3,MIT
-supports-color,4.2.1,MIT
-svg4everybody,2.1.9,CC0-1.0
-svgo,0.7.2,MIT
sys-filesystem,1.1.6,Artistic 2.0
-table,3.8.3,New BSD
-tapable,0.1.10,MIT
-tapable,0.2.8,MIT
-tar,2.2.1,ISC
-tar-pack,3.4.0,Simplified BSD
temple,0.7.7,MIT
-test-exclude,4.0.0,ISC
text,1.3.1,MIT
-text-table,0.2.0,MIT
thor,0.19.4,MIT
thread_safe,0.3.6,Apache 2.0
-three,0.84.0,MIT
-three-orbit-controls,82.1.0,MIT
-three-stl-loader,1.0.4,MIT
-through,2.3.8,MIT
-thunky,0.1.0,unknown
tilt,2.0.6,MIT
-timeago.js,2.0.5,MIT
-timed-out,2.0.0,MIT
-timed-out,4.0.1,MIT
-timers-browserify,1.4.2,MIT
-timers-browserify,2.0.4,MIT
timfel-krb5-auth,0.8.3,LGPL
-tiny-emitter,1.1.0,MIT
-tmp,0.0.31,MIT
-to-array,0.1.4,MIT
-to-arraybuffer,1.0.1,MIT
-to-fast-properties,1.0.2,MIT
toml-rb,0.3.15,MIT
-touch,1.0.0,ISC
-tough-cookie,2.3.2,New BSD
-traverse,0.6.6,MIT
-trim-newlines,1.0.0,MIT
-trim-right,1.0.1,MIT
truncato,0.7.10,MIT
-tryit,1.0.3,MIT
-ts-loader,3.1.1,MIT
-tty-browserify,0.0.0,MIT
-tunnel-agent,0.6.0,Apache 2.0
-tweetnacl,0.14.5,Unlicense
-type-check,0.3.2,MIT
-type-is,1.6.15,MIT
-typedarray,0.0.6,MIT
-typescript,2.6.1,Apache 2.0
-tzinfo,1.2.3,MIT
+tzinfo,1.2.4,MIT
u2f,0.2.1,MIT
uber,0.1.0,MIT
uglifier,2.7.2,MIT
-uglify-js,2.8.29,Simplified BSD
-uglify-to-browserify,1.0.2,MIT
-uglifyjs-webpack-plugin,0.4.6,MIT
-uid-number,0.0.6,ISC
-ultron,1.0.2,MIT
-ultron,1.1.0,MIT
-unc-path-regex,0.1.2,MIT
-undefsafe,0.0.3,MIT / http://rem.mit-license.org
-underscore,1.8.3,MIT
unf,0.1.4,BSD
unf_ext,0.0.7.4,MIT
unicorn,5.1.0,ruby
unicorn-worker-killer,0.4.4,ruby
-uniq,1.0.1,MIT
-uniqid,4.1.1,MIT
-uniqs,2.0.0,MIT
-unpipe,1.0.0,MIT
-update-notifier,0.5.0,Simplified BSD
-url,0.11.0,MIT
-url-loader,0.5.8,MIT
-url-parse,1.0.5,MIT
-url-parse,1.1.7,MIT
-url-parse,1.1.9,MIT
-url-parse-lax,1.0.0,MIT
-url-to-options,1.0.1,MIT
url_safe_base64,0.2.2,MIT
-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.0,MIT
-uuid,2.0.3,MIT
-uuid,3.0.1,MIT
-validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
-vary,1.1.1,MIT
-vendors,1.0.1,MIT
-verror,1.3.6,MIT
version_sorter,2.1.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.2,MIT
-vue-hot-reload-api,2.0.11,MIT
-vue-loader,11.3.4,MIT
-vue-resource,1.3.4,MIT
-vue-style-loader,2.0.5,MIT
-vue-template-compiler,2.5.2,MIT
-vue-template-es2015-compiler,1.5.1,MIT
-vuex,3.0.0,MIT
warden,1.2.6,MIT
-watchpack,1.4.0,MIT
-wbuf,1.7.2,MIT
-webpack,3.5.5,MIT
-webpack-bundle-analyzer,2.8.2,MIT
-webpack-dev-middleware,1.11.0,MIT
-webpack-dev-server,2.7.1,MIT
webpack-rails,0.9.10,MIT
-webpack-sources,1.0.1,MIT
-webpack-stats-plugin,0.1.5,MIT
-websocket-driver,0.6.5,MIT
-websocket-extensions,0.1.1,MIT
-whet.extend,0.9.9,MIT
-which,1.2.12,ISC
-which-module,1.0.0,ISC
-which-module,2.0.0,ISC
-wide-align,1.1.2,ISC
wikicloth,0.8.1,MIT
-window-size,0.1.0,MIT
-wordwrap,0.0.2,MIT/X11
-wordwrap,0.0.3,MIT
-wordwrap,1.0.0,MIT
-wrap-ansi,2.1.0,MIT
-wrappy,1.0.2,ISC
-write,0.2.1,MIT
-write-file-atomic,1.3.4,ISC
-ws,1.1.2,MIT
-ws,2.3.1,MIT
-wtf-8,1.0.0,MIT
-xdg-basedir,2.0.0,MIT
xml-simple,1.1.5,ruby
-xmlhttprequest-ssl,1.5.3,MIT
-xtend,4.0.1,MIT
-y18n,3.2.1,ISC
-yallist,2.1.2,ISC
-yargs,3.10.0,MIT
-yargs,6.6.0,MIT
-yargs,8.0.2,MIT
-yargs-parser,4.2.1,ISC
-yargs-parser,7.0.0,ISC
-yeast,0.1.2,MIT
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index 7a811e1986b..dcf5e4a0416 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 7db63ecc65f..d4856090ed9 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 96f51ee804c..6ee7e76f676 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml
new file mode 100644
index 00000000000..dd9496deb4d
--- /dev/null
+++ b/vendor/prometheus/values.yaml
@@ -0,0 +1,134 @@
+alertmanager: |
+ enabled: false
+
+kubeStateMetrics: |
+ enabled: 'false'
+
+nodeExporter: |
+ enabled: 'false'
+
+pushgateway: |
+ enabled: 'false'
+
+serverFiles: |
+ alerts: ''
+ rules: ''
+
+ prometheus.yml: |-
+ rule_files: |
+ - /etc/config/rules
+ - /etc/config/alerts
+ scrape_configs: |
+ - job_name: prometheus
+ static_configs: |
+ - targets:
+ - localhost:9090
+
+ - job_name: 'kubernetes-apiservers'
+ kubernetes_sd_configs: |
+ - role: endpoints
+ scheme: https
+
+ tls_config:
+ ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+ insecure_skip_verify: true
+ bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+ relabel_configs:
+ - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
+ action: keep
+ regex: default;kubernetes;https
+ - job_name: 'kubernetes-nodes'
+ scheme: https
+ tls_config:
+ ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+ insecure_skip_verify: true
+ bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+ kubernetes_sd_configs:
+ - role: node
+ relabel_configs:
+ - action: labelmap
+ regex: __meta_kubernetes_node_label_(.+)
+ - target_label: __address__
+ replacement: kubernetes.default.svc:443
+ - source_labels: [__meta_kubernetes_node_name]
+ regex: (.+)
+ target_label: __metrics_path__
+ replacement: /api/v1/nodes/${1}/proxy/metrics
+
+ - job_name: 'kubernetes-service-endpoints'
+ kubernetes_sd_configs:
+ - role: endpoints
+ relabel_configs: |
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
+ action: keep
+ regex: 'true'
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
+ action: replace
+ target_label: __scheme__
+ regex: (https?)
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
+ action: replace
+ target_label: __metrics_path__
+ regex: (.+)
+ - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
+ action: replace
+ target_label: __address__
+ regex: (.+)(?::\d+);(\d+)
+ replacement: $1:$2
+ - action: labelmap
+ regex: __meta_kubernetes_service_label_(.+)
+ - source_labels: [__meta_kubernetes_namespace]
+ action: replace
+ target_label: kubernetes_namespace
+ - source_labels: [__meta_kubernetes_service_name]
+ action: replace
+ target_label: kubernetes_name
+ - job_name: 'prometheus-pushgateway'
+ honor_labels: true
+ kubernetes_sd_configs: |
+ - role: service
+ relabel_configs: |
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
+ action: keep
+ regex: pushgateway
+ - job_name: 'kubernetes-services'
+ metrics_path: /probe
+ params: |
+ module: [http_2xx]
+ kubernetes_sd_configs: |
+ - role: service
+ relabel_configs: |
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
+ action: keep
+ regex: 'true'
+ - source_labels: [__address__]
+ target_label: __param_target
+ - target_label: __address__
+ replacement: blackbox
+ - source_labels: [__param_target]
+ target_label: instance
+ - action: labelmap
+ regex: __meta_kubernetes_service_label_(.+)
+ - source_labels: [__meta_kubernetes_namespace]
+ target_label: kubernetes_namespace
+ - source_labels: [__meta_kubernetes_service_name]
+ target_label: kubernetes_name
+ - job_name: 'kubernetes-pods'
+ kubernetes_sd_configs:
+ - role: pod
+ relabel_configs:
+ - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
+ action: keep
+ regex: 'true'
+ - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
+ action: replace
+ target_label: __metrics_path__
+ regex: (.+)
+ - action: labelmap
+ regex: __meta_kubernetes_pod_label_(.+)
+ - source_labels: [__meta_kubernetes_namespace]
+ action: replace
+ target_label: kubernetes_namespace
+ - source_labels: [__meta_kubernetes_pod_name]
+ action: replace
+ target_label: kubernetes_pod_name
diff --git a/yarn.lock b/yarn.lock
index 69e8f8a0647..da9e50739cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,9 +54,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.1.1":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.1.3.tgz#2beead1bcdd83e7400de29b01014bf17bf76318e"
+"@gitlab-org/gitlab-svgs@^1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.5.0.tgz#6635df6aad6c71fb293ff113efd4311a744c0e2c"
"@types/jquery@^2.0.40":
version "2.0.48"
@@ -116,16 +116,7 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
-ajv@^5.0.0:
- version "5.4.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474"
- dependencies:
- co "^4.6.0"
- fast-deep-equal "^1.0.0"
- fast-json-stable-stringify "^2.0.0"
- json-schema-traverse "^0.3.0"
-
-ajv@^5.1.5:
+ajv@^5.0.0, ajv@^5.1.5:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
dependencies:
@@ -1713,14 +1704,112 @@ custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+d3-array@^1.2.0, d3-array@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
+
+d3-axis@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
+
+d3-brush@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
+ dependencies:
+ d3-dispatch "1"
+ d3-drag "1"
+ d3-interpolate "1"
+ d3-selection "1"
+ d3-transition "1"
+
+d3-collection@1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
+
+d3-color@1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
+
+d3-dispatch@1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
+
+d3-drag@1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
+ dependencies:
+ d3-dispatch "1"
+ d3-selection "1"
+
+d3-ease@1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
+
+d3-format@1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f"
+
+d3-interpolate@1:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6"
+ dependencies:
+ d3-color "1"
+
+d3-path@1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
+
+d3-scale@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-color "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88"
+
+d3-shape@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2, d3-time-format@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
+ dependencies:
+ d3-time "1"
+
+d3-time@1, d3-time@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
+
+d3-timer@1:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
+
+d3-transition@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
+ dependencies:
+ d3-color "1"
+ d3-dispatch "1"
+ d3-ease "1"
+ d3-interpolate "1"
+ d3-selection "^1.1.0"
+ d3-timer "1"
+
d3@3.5.17:
version "3.5.17"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
-d3@^3.5.11:
- version "3.5.11"
- resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
-
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@@ -2540,10 +2629,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-json-stable-stringify@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
-
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -5061,6 +5146,10 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+prettier@1.9.2:
+ version "1.9.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.9.2.tgz#96bc2132f7a32338e6078aeb29727178c6335827"
+
prettier@^1.7.0:
version "1.8.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8"
@@ -6443,6 +6532,10 @@ vue-resource@^1.3.4:
dependencies:
got "^7.0.0"
+vue-router@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
+
vue-style-loader@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.0.3.tgz#623658f81506aef9d121cdc113a4f5c9cac32df7"