summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml34
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md43
-rw-r--r--.nvmrc2
-rw-r--r--.ruby-version2
-rw-r--r--.scss-lint.yml6
-rw-r--r--CHANGELOG.md261
-rw-r--r--CONTRIBUTING.md18
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile12
-rw-r--r--Gemfile.lock48
-rw-r--r--PROCESS.md38
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/emoji.pngbin1218558 -> 1219696 bytes
-rw-r--r--app/assets/images/emoji/gay_pride_flag.pngbin0 -> 2340 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus.pngbin2206 -> 3338 bytes
-rw-r--r--app/assets/images/emoji/speech_left.pngbin0 -> 390 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin2976505 -> 2977099 bytes
-rw-r--r--app/assets/images/icons.json2
-rw-r--r--app/assets/images/icons.svg2
-rw-r--r--app/assets/images/illustrations/epics.svg1
-rw-r--r--app/assets/images/illustrations/gitlab_logo.svg1
-rw-r--r--app/assets/images/illustrations/pipelines_pending.svg1
-rw-r--r--app/assets/images/illustrations/slack_logo.svg1
-rw-r--r--app/assets/images/illustrations/wiki-fro-logged-out-users.svg1
-rw-r--r--app/assets/javascripts/abuse_reports.js4
-rw-r--r--app/assets/javascripts/api.js1
-rw-r--r--app/assets/javascripts/behaviors/autosize.js6
-rw-r--r--app/assets/javascripts/behaviors/copy_as_gfm.js (renamed from app/assets/javascripts/copy_as_gfm.js)18
-rw-r--r--app/assets/javascripts/behaviors/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js52
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue (renamed from app/assets/javascripts/boards/components/board_card.js)40
-rw-r--r--app/assets/javascripts/boards/components/board_list.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js13
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js3
-rw-r--r--app/assets/javascripts/boards/models/issue.js13
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/clusters.js115
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js221
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue185
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue114
-rw-r--r--app/assets/javascripts/clusters/constants.js12
-rw-r--r--app/assets/javascripts/clusters/event_hub.js (renamed from app/assets/javascripts/repo/event_hub.js)0
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js20
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js68
-rw-r--r--app/assets/javascripts/commits.js4
-rw-r--r--app/assets/javascripts/create_label.js3
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js4
-rw-r--r--app/assets/javascripts/dispatcher.js54
-rw-r--r--app/assets/javascripts/droplab/plugins/filter.js2
-rw-r--r--app/assets/javascripts/droplab/utils.js2
-rw-r--r--app/assets/javascripts/dropzone_input.js4
-rw-r--r--app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js16
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js21
-rw-r--r--app/assets/javascripts/environments/components/environment.vue36
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue26
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js3
-rw-r--r--app/assets/javascripts/gl_dropdown.js3
-rw-r--r--app/assets/javascripts/gl_form.js5
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js3
-rw-r--r--app/assets/javascripts/header.js14
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js3
-rw-r--r--app/assets/javascripts/init_legacy_filters.js11
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js13
-rw-r--r--app/assets/javascripts/issuable_context.js28
-rw-r--r--app/assets/javascripts/issue.js8
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue23
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue16
-rw-r--r--app/assets/javascripts/issue_status_select.js57
-rw-r--r--app/assets/javascripts/job.js50
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js6
-rw-r--r--app/assets/javascripts/jobs/services/job_service.js9
-rw-r--r--app/assets/javascripts/labels_select.js838
-rw-r--r--app/assets/javascripts/lazy_loader.js15
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js37
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js28
-rw-r--r--app/assets/javascripts/lib/utils/poll.js12
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js153
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js228
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js4
-rw-r--r--app/assets/javascripts/logo.js8
-rw-r--r--app/assets/javascripts/main.js29
-rw-r--r--app/assets/javascripts/members.js12
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/milestone.js85
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue25
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue12
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js8
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js49
-rw-r--r--app/assets/javascripts/namespace_select.js134
-rw-r--r--app/assets/javascripts/new_branch_form.js168
-rw-r--r--app/assets/javascripts/new_commit_form.js54
-rw-r--r--app/assets/javascripts/notes.js7
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue7
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue17
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.vue87
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue169
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue10
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue2
-rw-r--r--app/assets/javascripts/project.js248
-rw-r--r--app/assets/javascripts/project_avatar.js31
-rw-r--r--app/assets/javascripts/project_find_file.js3
-rw-r--r--app/assets/javascripts/project_import.js17
-rw-r--r--app/assets/javascripts/project_label_subscription.js79
-rw-r--r--app/assets/javascripts/project_new.js272
-rw-r--r--app/assets/javascripts/project_select.js134
-rw-r--r--app/assets/javascripts/project_show.js11
-rw-r--r--app/assets/javascripts/project_variables.js60
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue7
-rw-r--r--app/assets/javascripts/render_gfm.js3
-rw-r--r--app/assets/javascripts/render_mermaid.js30
-rw-r--r--app/assets/javascripts/repo/components/new_branch_form.vue49
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue30
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/modal.vue18
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/upload.vue68
-rw-r--r--app/assets/javascripts/repo/components/repo.vue93
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue170
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue75
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue169
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue47
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue49
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue53
-rw-r--r--app/assets/javascripts/repo/components/repo_prev_directory.vue30
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue49
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue122
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue26
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue12
-rw-r--r--app/assets/javascripts/repo/helpers/monaco_loader_helper.js25
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js317
-rw-r--r--app/assets/javascripts/repo/index.js108
-rw-r--r--app/assets/javascripts/repo/mixins/repo_mixin.js17
-rw-r--r--app/assets/javascripts/repo/services/index.js40
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js101
-rw-r--r--app/assets/javascripts/repo/stores/actions.js145
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js20
-rw-r--r--app/assets/javascripts/repo/stores/actions/file.js110
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js162
-rw-r--r--app/assets/javascripts/repo/stores/getters.js36
-rw-r--r--app/assets/javascripts/repo/stores/index.js15
-rw-r--r--app/assets/javascripts/repo/stores/mutation_types.js30
-rw-r--r--app/assets/javascripts/repo/stores/mutations.js61
-rw-r--r--app/assets/javascripts/repo/stores/mutations/branch.js9
-rw-r--r--app/assets/javascripts/repo/stores/mutations/file.js54
-rw-r--r--app/assets/javascripts/repo/stores/mutations/tree.js27
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js189
-rw-r--r--app/assets/javascripts/repo/stores/state.js24
-rw-r--r--app/assets/javascripts/repo/stores/utils.js127
-rw-r--r--app/assets/javascripts/search_autocomplete.js16
-rw-r--r--app/assets/javascripts/settings_panels.js45
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js5
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue29
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue31
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue125
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue26
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue46
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue64
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js5
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js34
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js16
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js22
-rw-r--r--app/assets/javascripts/smart_interval.js29
-rw-r--r--app/assets/javascripts/subscription.js45
-rw-r--r--app/assets/javascripts/subscription_select.js49
-rw-r--r--app/assets/javascripts/test_utils/index.js2
-rw-r--r--app/assets/javascripts/test_utils/simulate_input.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js90
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js2
-rw-r--r--app/assets/javascripts/vue_shared/ci_action_icons.js21
-rw-r--r--app/assets/javascripts/vue_shared/ci_status_icons.js43
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue39
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue143
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue37
-rw-r--r--app/assets/javascripts/wikis.js3
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/animations.scss11
-rw-r--r--app/assets/stylesheets/framework/avatar.scss7
-rw-r--r--app/assets/stylesheets/framework/blank.scss83
-rw-r--r--app/assets/stylesheets/framework/blocks.scss31
-rw-r--r--app/assets/stylesheets/framework/buttons.scss3
-rw-r--r--app/assets/stylesheets/framework/callout.scss14
-rw-r--r--app/assets/stylesheets/framework/common.scss111
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss34
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss18
-rw-r--r--app/assets/stylesheets/framework/emoji-sprites.scss2052
-rw-r--r--app/assets/stylesheets/framework/files.scss72
-rw-r--r--app/assets/stylesheets/framework/filters.scss21
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss8
-rw-r--r--app/assets/stylesheets/framework/header.scss48
-rw-r--r--app/assets/stylesheets/framework/highlight.scss4
-rw-r--r--app/assets/stylesheets/framework/icons.scss28
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss38
-rw-r--r--app/assets/stylesheets/framework/lists.scss62
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss83
-rw-r--r--app/assets/stylesheets/framework/mixins.scss38
-rw-r--r--app/assets/stylesheets/framework/modal.scss4
-rw-r--r--app/assets/stylesheets/framework/popup.scss15
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss (renamed from app/assets/stylesheets/framework/responsive-tables.scss)96
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss52
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/tables.scss2
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss13
-rw-r--r--app/assets/stylesheets/framework/wells.scss31
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/highlight/white.scss26
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss26
-rw-r--r--app/assets/stylesheets/pages/boards.scss13
-rw-r--r--app/assets/stylesheets/pages/builds.scss51
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss2
-rw-r--r--app/assets/stylesheets/pages/clusters.scss7
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss18
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss53
-rw-r--r--app/assets/stylesheets/pages/editor.scss6
-rw-r--r--app/assets/stylesheets/pages/environments.scss111
-rw-r--r--app/assets/stylesheets/pages/events.scss6
-rw-r--r--app/assets/stylesheets/pages/help.scss20
-rw-r--r--app/assets/stylesheets/pages/issuable.scss64
-rw-r--r--app/assets/stylesheets/pages/issues.scss19
-rw-r--r--app/assets/stylesheets/pages/login.scss102
-rw-r--r--app/assets/stylesheets/pages/members.scss18
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss50
-rw-r--r--app/assets/stylesheets/pages/milestone.scss36
-rw-r--r--app/assets/stylesheets/pages/note_form.scss60
-rw-r--r--app/assets/stylesheets/pages/notes.scss161
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss195
-rw-r--r--app/assets/stylesheets/pages/profile.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss64
-rw-r--r--app/assets/stylesheets/pages/repo.scss71
-rw-r--r--app/assets/stylesheets/pages/runners.scss7
-rw-r--r--app/assets/stylesheets/pages/search.scss4
-rw-r--r--app/assets/stylesheets/pages/settings.scss40
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss16
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss16
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss9
-rw-r--r--app/assets/stylesheets/pages/wiki.scss6
-rw-r--r--app/assets/stylesheets/test.scss5
-rw-r--r--app/controllers/admin/applications_controller.rb15
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb2
-rw-r--r--app/controllers/application_controller.rb35
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb1
-rw-r--r--app/controllers/concerns/issuable_actions.rb75
-rw-r--r--app/controllers/concerns/issuable_collections.rb91
-rw-r--r--app/controllers/concerns/issues_action.rb8
-rw-r--r--app/controllers/concerns/lfs_request.rb14
-rw-r--r--app/controllers/concerns/merge_requests_action.rb9
-rw-r--r--app/controllers/concerns/notes_actions.rb15
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb4
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb1
-rw-r--r--app/controllers/jwt_controller.rb6
-rw-r--r--app/controllers/metrics_controller.rb16
-rw-r--r--app/controllers/oauth/applications_controller.rb21
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb6
-rw-r--r--app/controllers/profiles/keys_controller.rb10
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb14
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb25
-rw-r--r--app/controllers/projects/clusters_controller.rb41
-rw-r--r--app/controllers/projects/commit_controller.rb21
-rw-r--r--app/controllers/projects/commits_controller.rb4
-rw-r--r--app/controllers/projects/deployments_controller.rb1
-rw-r--r--app/controllers/projects/git_http_client_controller.rb6
-rw-r--r--app/controllers/projects/group_links_controller.rb14
-rw-r--r--app/controllers/projects/issues_controller.rb87
-rw-r--r--app/controllers/projects/jobs_controller.rb7
-rw-r--r--app/controllers/projects/labels_controller.rb1
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb51
-rw-r--r--app/controllers/projects/milestones_controller.rb12
-rw-r--r--app/controllers/projects/notes_controller.rb1
-rw-r--r--app/controllers/projects/refs_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb9
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/controllers/snippets/notes_controller.rb1
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/autocomplete_users_finder.rb2
-rw-r--r--app/finders/groups_finder.rb8
-rw-r--r--app/finders/issuable_finder.rb1
-rw-r--r--app/finders/personal_access_tokens_finder.rb1
-rw-r--r--app/finders/projects_finder.rb3
-rw-r--r--app/helpers/appearances_helper.rb7
-rw-r--r--app/helpers/application_settings_helper.rb9
-rw-r--r--app/helpers/ci_status_helper.rb29
-rw-r--r--app/helpers/commits_helper.rb26
-rw-r--r--app/helpers/diff_helper.rb7
-rw-r--r--app/helpers/emails_helper.rb1
-rw-r--r--app/helpers/events_helper.rb10
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/icons_helper.rb9
-rw-r--r--app/helpers/issuables_helper.rb52
-rw-r--r--app/helpers/markup_helper.rb25
-rw-r--r--app/helpers/namespaces_helper.rb7
-rw-r--r--app/helpers/nav_helper.rb6
-rw-r--r--app/helpers/notifications_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb14
-rw-r--r--app/helpers/tree_helper.rb16
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/blob.rb17
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/pipeline.rb77
-rw-r--r--app/models/clusters/applications/helm.rb35
-rw-r--r--app/models/clusters/applications/ingress.rb44
-rw-r--r--app/models/clusters/cluster.rb102
-rw-r--r--app/models/clusters/concerns/application_status.rb43
-rw-r--r--app/models/clusters/platforms/kubernetes.rb109
-rw-r--r--app/models/clusters/project.rb8
-rw-r--r--app/models/clusters/providers/gcp.rb80
-rw-r--r--app/models/commit.rb13
-rw-r--r--app/models/commit_collection.rb44
-rw-r--r--app/models/commit_status.rb12
-rw-r--r--app/models/concerns/avatarable.rb25
-rw-r--r--app/models/concerns/awardable.rb1
-rw-r--r--app/models/concerns/cache_markdown_field.rb3
-rw-r--r--app/models/concerns/ignorable_column.rb4
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/concerns/repository_mirroring.rb32
-rw-r--r--app/models/concerns/subscribable.rb2
-rw-r--r--app/models/diff_note.rb3
-rw-r--r--app/models/environment.rb7
-rw-r--r--app/models/epic.rb7
-rw-r--r--app/models/external_issue.rb4
-rw-r--r--app/models/fork_network.rb4
-rw-r--r--app/models/fork_network_member.rb10
-rw-r--r--app/models/gcp/cluster.rb116
-rw-r--r--app/models/group.rb18
-rw-r--r--app/models/group_custom_attribute.rb6
-rw-r--r--app/models/identity.rb16
-rw-r--r--app/models/issue.rb16
-rw-r--r--app/models/key.rb8
-rw-r--r--app/models/lfs_object.rb10
-rw-r--r--app/models/merge_request.rb55
-rw-r--r--app/models/merge_request_diff.rb8
-rw-r--r--app/models/merge_request_diff_commit.rb4
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/note.rb23
-rw-r--r--app/models/oauth_access_token.rb10
-rw-r--r--app/models/pages_domain.rb9
-rw-r--r--app/models/project.rb121
-rw-r--r--app/models/project_custom_attribute.rb6
-rw-r--r--app/models/project_services/chat_message/issue_message.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb1
-rw-r--r--app/models/project_services/kubernetes_service.rb5
-rw-r--r--app/models/project_services/packagist_service.rb65
-rw-r--r--app/models/project_services/prometheus_service.rb6
-rw-r--r--app/models/project_wiki.rb8
-rw-r--r--app/models/repository.rb72
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/system_note_metadata.rb10
-rw-r--r--app/models/user.rb68
-rw-r--r--app/models/wiki_page.rb19
-rw-r--r--app/policies/ci/build_policy.rb11
-rw-r--r--app/policies/clusters/cluster_policy.rb (renamed from app/policies/gcp/cluster_policy.rb)4
-rw-r--r--app/presenters/clusters/cluster_presenter.rb (renamed from app/presenters/gcp/cluster_presenter.rb)4
-rw-r--r--app/serializers/blob_entity.rb4
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/cluster_application_entity.rb5
-rw-r--r--app/serializers/cluster_entity.rb1
-rw-r--r--app/serializers/cluster_serializer.rb2
-rw-r--r--app/serializers/issuable_entity.rb8
-rw-r--r--app/serializers/issuable_sidebar_entity.rb16
-rw-r--r--app/serializers/issue_entity.rb5
-rw-r--r--app/serializers/issue_serializer.rb15
-rw-r--r--app/serializers/issue_sidebar_entity.rb3
-rw-r--r--app/serializers/merge_request_basic_entity.rb6
-rw-r--r--app/serializers/merge_request_entity.rb4
-rw-r--r--app/serializers/merge_request_serializer.rb9
-rw-r--r--app/serializers/time_trackable_entity.rb11
-rw-r--r--app/serializers/tree_entity.rb4
-rw-r--r--app/serializers/tree_root_entity.rb4
-rw-r--r--app/services/access_token_validation_service.rb7
-rw-r--r--app/services/applications/create_service.rb13
-rw-r--r--app/services/base_count_service.rb34
-rw-r--r--app/services/base_renderer.rb7
-rw-r--r--app/services/ci/create_cluster_service.rb15
-rw-r--r--app/services/ci/ensure_stage_service.rb39
-rw-r--r--app/services/ci/fetch_gcp_operation_service.rb17
-rw-r--r--app/services/ci/fetch_kubernetes_token_service.rb1
-rw-r--r--app/services/ci/finalize_cluster_creation_service.rb33
-rw-r--r--app/services/ci/integrate_cluster_service.rb26
-rw-r--r--app/services/ci/pipeline_trigger_service.rb8
-rw-r--r--app/services/ci/provision_cluster_service.rb36
-rw-r--r--app/services/ci/update_cluster_service.rb22
-rw-r--r--app/services/clusters/applications/base_helm_service.rb29
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb65
-rw-r--r--app/services/clusters/applications/install_service.rb21
-rw-r--r--app/services/clusters/applications/schedule_installation_service.rb22
-rw-r--r--app/services/clusters/create_service.rb29
-rw-r--r--app/services/clusters/gcp/fetch_operation_service.rb16
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb56
-rw-r--r--app/services/clusters/gcp/provision_service.rb47
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb48
-rw-r--r--app/services/clusters/update_service.rb7
-rw-r--r--app/services/delete_merged_branches_service.rb2
-rw-r--r--app/services/events/render_service.rb21
-rw-r--r--app/services/issuable/common_system_notes_service.rb81
-rw-r--r--app/services/issuable_base_service.rb103
-rw-r--r--app/services/issues/base_service.rb12
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/keys/base_service.rb1
-rw-r--r--app/services/labels/promote_service.rb1
-rw-r--r--app/services/merge_requests/base_service.rb8
-rw-r--r--app/services/merge_requests/build_service.rb1
-rw-r--r--app/services/merge_requests/merge_service.rb39
-rw-r--r--app/services/merge_requests/update_service.rb10
-rw-r--r--app/services/metrics_service.rb3
-rw-r--r--app/services/milestones/promote_service.rb85
-rw-r--r--app/services/notes/render_service.rb21
-rw-r--r--app/services/projects/count_service.rb25
-rw-r--r--app/services/projects/forks_count_service.rb2
-rw-r--r--app/services/projects/group_links/create_service.rb15
-rw-r--r--app/services/projects/group_links/destroy_service.rb11
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb2
-rw-r--r--app/services/projects/import_service.rb34
-rw-r--r--app/services/projects/open_issues_count_service.rb2
-rw-r--r--app/services/projects/open_merge_requests_count_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb38
-rw-r--r--app/services/projects/unlink_fork_service.rb16
-rw-r--r--app/services/system_hooks_service.rb48
-rw-r--r--app/services/system_note_service.rb16
-rw-r--r--app/services/todo_service.rb19
-rw-r--r--app/services/users/keys_count_service.rb27
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb18
-rw-r--r--app/uploaders/file_uploader.rb10
-rw-r--r--app/validators/abstract_path_validator.rb38
-rw-r--r--app/validators/certificate_key_validator.rb1
-rw-r--r--app/validators/certificate_validator.rb1
-rw-r--r--app/validators/cluster_name_validator.rb24
-rw-r--r--app/validators/dynamic_path_validator.rb53
-rw-r--r--app/validators/namespace_path_validator.rb19
-rw-r--r--app/validators/project_path_validator.rb19
-rw-r--r--app/validators/user_path_validator.rb15
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml51
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/hook_logs/_index.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml33
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/ci/status/_badge.html.haml4
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml8
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml70
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml98
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml21
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml22
-rw-r--r--app/views/events/_event_note.atom.haml2
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/notify/_note_email.html.haml11
-rw-r--r--app/views/profiles/accounts/_reset_token.html.haml11
-rw-r--r--app/views/profiles/accounts/show.html.haml16
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml37
-rw-r--r--app/views/projects/_export.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml9
-rw-r--r--app/views/projects/_md_preview.html.haml26
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml2
-rw-r--r--app/views/projects/clusters/_form.html.haml40
-rw-r--r--app/views/projects/clusters/_header.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml17
-rw-r--r--app/views/projects/clusters/new_gcp.html.haml10
-rw-r--r--app/views/projects/clusters/show.html.haml31
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_limit_exceeded_message.html.haml8
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/branches.html.haml26
-rw-r--r--app/views/projects/commits/_commit.html.haml7
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml4
-rw-r--r--app/views/projects/diffs/_stats.html.haml8
-rw-r--r--app/views/projects/edit.html.haml21
-rw-r--r--app/views/projects/environments/show.html.haml17
-rw-r--r--app/views/projects/graphs/show.html.haml14
-rw-r--r--app/views/projects/hook_logs/_index.html.haml2
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml4
-rw-r--r--app/views/projects/issues/_new_branch.html.haml27
-rw-r--r--app/views/projects/issues/show.html.haml6
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml7
-rw-r--r--app/views/projects/jobs/show.html.haml6
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml4
-rw-r--r--app/views/projects/milestones/show.html.haml11
-rw-r--r--app/views/projects/pipelines/index.html.haml6
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml4
-rw-r--r--app/views/projects/protected_tags/_create_protected_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml4
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml27
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml16
-rw-r--r--app/views/projects/tags/_tag.html.haml8
-rw-r--r--app/views/projects/tags/index.html.haml14
-rw-r--r--app/views/projects/tags/new.html.haml21
-rw-r--r--app/views/projects/tags/show.html.haml18
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/projects/tree/_truncated_notice_tree_row.html.haml7
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/history.html.haml3
-rw-r--r--app/views/projects/wikis/show.html.haml4
-rw-r--r--app/views/shared/_import_form.html.haml3
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml2
-rw-r--r--app/views/shared/_ref_switcher.html.haml4
-rw-r--r--app/views/shared/boards/components/sidebar/_notifications.html.haml10
-rw-r--r--app/views/shared/hook_logs/_content.html.haml2
-rw-r--r--app/views/shared/icons/_add_new_project.svg2
-rw-r--r--app/views/shared/icons/_icon_autodevops.svg4
-rw-r--r--app/views/shared/icons/_icon_hourglass.svg1
-rw-r--r--app/views/shared/icons/_lightbulb.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml1
-rw-r--r--app/views/shared/issuable/_participants.html.haml18
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml13
-rw-r--r--app/views/shared/members/_member.html.haml1
-rw-r--r--app/views/shared/milestones/_milestone.html.haml7
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml16
-rw-r--r--app/views/shared/repo/_editable_mode.html.haml2
-rw-r--r--app/views/shared/repo/_repo.html.haml7
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml1
-rw-r--r--app/workers/cluster_install_app_worker.rb11
-rw-r--r--app/workers/cluster_provision_worker.rb6
-rw-r--r--app/workers/cluster_wait_for_app_installation_worker.rb14
-rw-r--r--app/workers/concerns/cluster_applications.rb9
-rw-r--r--app/workers/concerns/gitlab/github_import/notify_upon_death.rb31
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb54
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb16
-rw-r--r--app/workers/concerns/gitlab/github_import/rescheduling_methods.rb40
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb30
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb74
-rw-r--r--app/workers/gitlab/github_import/import_diff_note_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/import_issue_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/import_note_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/import_pull_request_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb38
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb43
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb33
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb31
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb27
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb29
-rw-r--r--app/workers/gitlab/github_import/stage/import_repository_worker.rb38
-rw-r--r--app/workers/irker_worker.rb1
-rw-r--r--app/workers/repository_import_worker.rb11
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb1
-rw-r--r--app/workers/update_merge_requests_worker.rb18
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb21
-rw-r--r--changelogs/unreleased/1312-time-spent-at.yml5
-rw-r--r--changelogs/unreleased/14970-suggest-rename-remote.yml5
-rw-r--r--changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml5
-rw-r--r--changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml5
-rw-r--r--changelogs/unreleased/23000-pages-api.yml5
-rw-r--r--changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml5
-rw-r--r--changelogs/unreleased/27654-retry-button.yml5
-rw-r--r--changelogs/unreleased/28202_decrease_abc_threshold_step5.yml5
-rw-r--r--changelogs/unreleased/30140-restore-readme-only-preference.yml5
-rw-r--r--changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step3.yml5
-rw-r--r--changelogs/unreleased/32098-pipelines-navigation.yml6
-rw-r--r--changelogs/unreleased/32318-filter-icon.yml5
-rw-r--r--changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml5
-rw-r--r--changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml5
-rw-r--r--changelogs/unreleased/34600-performance-wiki-pages.yml5
-rw-r--r--changelogs/unreleased/34841-todos.yml5
-rw-r--r--changelogs/unreleased/34897-delete-branch-after-merge.yml5
-rw-r--r--changelogs/unreleased/35199-case-insensitive-branches-search.yml5
-rw-r--r--changelogs/unreleased/35644-refactor-have-http-status-into-have-gitlab-http-status.yml5
-rw-r--r--changelogs/unreleased/35652-prometheus-service-page-shows-error.yml5
-rw-r--r--changelogs/unreleased/36160-zindex.yml5
-rw-r--r--changelogs/unreleased/37032-get-project-branch-invalid-name-message.yml5
-rw-r--r--changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml5
-rw-r--r--changelogs/unreleased/37660-match-sidebar-colors.yml5
-rw-r--r--changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml6
-rw-r--r--changelogs/unreleased/38075_allow_refernce_integer_labels.yml5
-rw-r--r--changelogs/unreleased/38178-fl-mr-notes-components.yml6
-rw-r--r--changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml5
-rw-r--r--changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml5
-rw-r--r--changelogs/unreleased/38720-sort-admin-runners.yml5
-rw-r--r--changelogs/unreleased/38822-oauth-search-case-insensitive.yml5
-rw-r--r--changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml5
-rw-r--r--changelogs/unreleased/38986-due-date.yml5
-rw-r--r--changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml6
-rw-r--r--changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml5
-rw-r--r--changelogs/unreleased/39054-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml5
-rw-r--r--changelogs/unreleased/39167-async-boards-sidebar.yml5
-rw-r--r--changelogs/unreleased/39188-change-default-disabled-merge-message.yml5
-rw-r--r--changelogs/unreleased/39297-remove-help-text-group-lists.yml5
-rw-r--r--changelogs/unreleased/39335-add-time-spend-to-milestones.yml5
-rw-r--r--changelogs/unreleased/39366-email-confirmation-fails.yml5
-rw-r--r--changelogs/unreleased/39419-remove-overzealous-tooltips.yml5
-rw-r--r--changelogs/unreleased/39436-pages-api-administrative.yml5
-rw-r--r--changelogs/unreleased/39441-bring-edit-form-back.yml5
-rw-r--r--changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml5
-rw-r--r--changelogs/unreleased/39495-fix-bitbucket-login.yml5
-rw-r--r--changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml5
-rw-r--r--changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml5
-rw-r--r--changelogs/unreleased/39582-nestingdepth-6.yml5
-rw-r--r--changelogs/unreleased/39583-reopen-issue-count-cache.yml5
-rw-r--r--changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml5
-rw-r--r--changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml6
-rw-r--r--changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml5
-rw-r--r--changelogs/unreleased/39977-gitlab-shell-default-timeout.yml5
-rw-r--r--changelogs/unreleased/40016-log-header.yml5
-rw-r--r--changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml5
-rw-r--r--changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml5
-rw-r--r--changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml5
-rw-r--r--changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml5
-rw-r--r--changelogs/unreleased/40377-blank-states.yml (renamed from changelogs/unreleased/fix_global_board_routes_39073.yml)2
-rw-r--r--changelogs/unreleased/4080-align-retry-btn.yml5
-rw-r--r--changelogs/unreleased/add-lazy-option-to-user-avatar-image-component.yml5
-rw-r--r--changelogs/unreleased/add-shared-vue-loading-button.yml5
-rw-r--r--changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml5
-rw-r--r--changelogs/unreleased/animate-auto-devops.yml5
-rw-r--r--changelogs/unreleased/api-configure-jira.yml5
-rw-r--r--changelogs/unreleased/api-doc-group-statistics.yml5
-rw-r--r--changelogs/unreleased/brand_header_change.yml5
-rw-r--r--changelogs/unreleased/bvl-circuitbreaker-backoff.yml6
-rw-r--r--changelogs/unreleased/bvl-circuitbreaker-improvements.yml5
-rw-r--r--changelogs/unreleased/bvl-delete-empty-fork-networks.yml5
-rw-r--r--changelogs/unreleased/bvl-do-not-use-redis-keys.yml5
-rw-r--r--changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml5
-rw-r--r--changelogs/unreleased/bvl-dont-rename-free-names.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-group-atom-feed.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml5
-rw-r--r--changelogs/unreleased/bvl-group-trees.yml5
-rw-r--r--changelogs/unreleased/bvl-subgroup-in-dropdowns.yml5
-rw-r--r--changelogs/unreleased/cleanup-issues-schema.yml5
-rw-r--r--changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml5
-rw-r--r--changelogs/unreleased/es-module-broadcast_message.yml5
-rw-r--r--changelogs/unreleased/feature-reliable-rspec-with-eval-script.yml5
-rw-r--r--changelogs/unreleased/feature-ssh_host_fingerprint.yml5
-rw-r--r--changelogs/unreleased/feature_add_mermaid.yml5
-rw-r--r--changelogs/unreleased/fix-500-on-old-merge-requests.yml5
-rw-r--r--changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml5
-rw-r--r--changelogs/unreleased/fix-ci-pipelines-index.yml5
-rw-r--r--changelogs/unreleased/fix-filter-by-my-reaction.yml5
-rw-r--r--changelogs/unreleased/fix-project-select-js-without-button.yml5
-rw-r--r--changelogs/unreleased/fix-protected-branches-descriptions.yml5
-rw-r--r--changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml5
-rw-r--r--changelogs/unreleased/fix-system-hook-docs.yml5
-rw-r--r--changelogs/unreleased/fix-todos-last-page.yml5
-rw-r--r--changelogs/unreleased/fix_diff_parsing.yml5
-rw-r--r--changelogs/unreleased/hide-pipeline-zero-duration.yml5
-rw-r--r--changelogs/unreleased/improved-changes-dropdown.yml5
-rw-r--r--changelogs/unreleased/issue-36484.yml5
-rw-r--r--changelogs/unreleased/issue_39238.yml5
-rw-r--r--changelogs/unreleased/issue_40337.yml5
-rw-r--r--changelogs/unreleased/make-merge-jid-handling-less-stateful.yml5
-rw-r--r--changelogs/unreleased/merge-requests-schema-cleanup.yml5
-rw-r--r--changelogs/unreleased/mk-add-user-rate-limits.yml6
-rw-r--r--changelogs/unreleased/move_markdown_preview_to_concern.yml5
-rw-r--r--changelogs/unreleased/mr-14642.yml6
-rw-r--r--changelogs/unreleased/multi-file-editor-submodules.yml5
-rw-r--r--changelogs/unreleased/new-mr-repo-editor.yml5
-rw-r--r--changelogs/unreleased/not-found-in-commits.yml5
-rw-r--r--changelogs/unreleased/osw-merge-process-logs.yml5
-rw-r--r--changelogs/unreleased/reduce-queries-for-artifacts-button.yml5
-rw-r--r--changelogs/unreleased/replace_explore_projects-feature.yml5
-rw-r--r--changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml5
-rw-r--r--changelogs/unreleased/sh-fix-container-registry-destroy.yml5
-rw-r--r--changelogs/unreleased/sh-fix-environment-write-ref.yml5
-rw-r--r--changelogs/unreleased/sh-memoize-logger.yml5
-rw-r--r--changelogs/unreleased/sha-handling.yml5
-rw-r--r--changelogs/unreleased/skip_confirmation_user_API.yml7
-rw-r--r--changelogs/unreleased/tc-saml-fix-false-empty.yml5
-rw-r--r--changelogs/unreleased/text-utils.yml5
-rw-r--r--changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml6
-rw-r--r--changelogs/unreleased/update-fe-i18n-guide.yml5
-rw-r--r--changelogs/unreleased/update-merge-worker-metrics.yml5
-rw-r--r--changelogs/unreleased/use-git-branch-merged.yml5
-rw-r--r--changelogs/unreleased/use-title.yml5
-rw-r--r--changelogs/unreleased/winh-subgroups-api.yml5
-rw-r--r--changelogs/unreleased/zj-add-performance-changelog-cat.yml5
-rw-r--r--changelogs/unreleased/zj-commit-cache.yml5
-rw-r--r--changelogs/unreleased/zj-commit-show-n-1.yml5
-rw-r--r--changelogs/unreleased/zj-peek-gitaly.yml5
-rw-r--r--config/application.rb2
-rw-r--r--config/dependency_decisions.yml36
-rw-r--r--config/environments/test.rb1
-rw-r--r--config/gitlab.yml.example6
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/7_prometheus_metrics.rb18
-rw-r--r--config/initializers/8_metrics.rb9
-rw-r--r--config/initializers/ar5_batching.rb1
-rw-r--r--config/initializers/batch_loader.rb1
-rw-r--r--config/initializers/devise.rb3
-rw-r--r--config/initializers/gollum.rb28
-rw-r--r--config/initializers/math_lexer.rb2
-rw-r--r--config/initializers/omniauth.rb1
-rw-r--r--config/initializers/plantuml_lexer.rb2
-rw-r--r--config/initializers/postgresql_cte.rb2
-rw-r--r--config/initializers/rack_attack_global.rb61
-rw-r--r--config/karma.config.js9
-rw-r--r--config/locales/doorkeeper.en.yml15
-rw-r--r--config/prometheus/additional_metrics.yml10
-rw-r--r--config/routes/group.rb52
-rw-r--r--config/routes/profile.rb1
-rw-r--r--config/routes/project.rb6
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb85
-rw-r--r--db/migrate/20150827121444_add_fast_forward_option_to_project.rb6
-rw-r--r--db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb2
-rw-r--r--db/migrate/20160608195742_add_repository_storage_to_projects.rb2
-rw-r--r--db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb1
-rw-r--r--db/migrate/20160715154212_add_request_access_enabled_to_projects.rb2
-rw-r--r--db/migrate/20160715204316_add_request_access_enabled_to_groups.rb2
-rw-r--r--db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb1
-rw-r--r--db/migrate/20160831223750_remove_features_enabled_from_projects.rb2
-rw-r--r--db/migrate/20160913162434_remove_projects_pushes_since_gc.rb2
-rw-r--r--db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb2
-rw-r--r--db/migrate/20170124193205_add_two_factor_columns_to_users.rb2
-rw-r--r--db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb2
-rw-r--r--db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb2
-rw-r--r--db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb2
-rw-r--r--db/migrate/20170320173259_migrate_assignees.rb1
-rw-r--r--db/migrate/20170918111708_create_project_custom_attributes.rb15
-rw-r--r--db/migrate/20170918140927_create_group_custom_attributes.rb19
-rw-r--r--db/migrate/20170919211300_remove_temporary_ci_builds_index.rb1
-rw-r--r--db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb38
-rw-r--r--db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb78
-rw-r--r--db/migrate/20171013094327_create_new_clusters_architectures.rb68
-rw-r--r--db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb26
-rw-r--r--db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb18
-rw-r--r--db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb21
-rw-r--r--db/migrate/20171106132212_issues_confidential_not_null.rb23
-rw-r--r--db/migrate/20171106135924_issues_milestone_id_foreign_key.rb38
-rw-r--r--db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb45
-rw-r--r--db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb44
-rw-r--r--db/migrate/20171106154015_remove_issues_branch_name.rb13
-rw-r--r--db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb37
-rw-r--r--db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb19
-rw-r--r--db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb43
-rw-r--r--db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb39
-rw-r--r--db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb46
-rw-r--r--db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb46
-rw-r--r--db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb45
-rw-r--r--db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb39
-rw-r--r--db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb35
-rw-r--r--db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb1
-rw-r--r--db/post_migrate/20170309171644_reset_relative_position_for_issue.rb1
-rw-r--r--db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb1
-rw-r--r--db/post_migrate/20170406111121_clean_upload_symlinks.rb1
-rw-r--r--db/post_migrate/20170406142253_migrate_user_project_view.rb1
-rw-r--r--db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb1
-rw-r--r--db/post_migrate/20170503004427_update_retried_for_ci_build.rb1
-rw-r--r--db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb1
-rw-r--r--db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb1
-rw-r--r--db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb1
-rw-r--r--db/post_migrate/20170612071012_move_personal_snippets_files.rb1
-rw-r--r--db/post_migrate/20170613111224_clean_appearance_symlinks.rb1
-rw-r--r--db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb1
-rw-r--r--db/post_migrate/20170927112319_update_notes_type_for_import.rb1
-rw-r--r--db/post_migrate/20171012150314_remove_user_authentication_token.rb20
-rw-r--r--db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb99
-rw-r--r--db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb29
-rw-r--r--db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb14
-rw-r--r--db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb19
-rw-r--r--db/post_migrate/20171114104051_remove_empty_fork_networks.rb36
-rw-r--r--db/schema.rb148
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/auth/README.md4
-rw-r--r--doc/administration/high_availability/README.md4
-rw-r--r--doc/administration/integration/plantuml.md37
-rw-r--r--doc/administration/logs.md9
-rw-r--r--doc/administration/monitoring/github_imports.md101
-rw-r--r--doc/administration/operations/sidekiq_memory_killer.md4
-rw-r--r--doc/administration/repository_storage_types.md31
-rw-r--r--doc/administration/troubleshooting/debug.md30
-rw-r--r--doc/api/README.md126
-rw-r--r--doc/api/custom_attributes.md27
-rw-r--r--doc/api/environments.md2
-rw-r--r--doc/api/groups.md59
-rw-r--r--doc/api/merge_requests.md28
-rw-r--r--doc/api/pages_domains.md25
-rw-r--r--doc/api/pipelines.md2
-rw-r--r--doc/api/projects.md6
-rw-r--r--doc/api/services.md71
-rw-r--r--doc/api/session.md55
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/ci/README.md4
-rw-r--r--doc/ci/docker/README.md4
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--doc/ci/docker/using_docker_images.md4
-rw-r--r--doc/ci/enable_or_disable_ci.md6
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/examples/deployment/composer-npm-deploy.md14
-rw-r--r--doc/ci/examples/php.md4
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md23
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md24
-rw-r--r--doc/ci/examples/test-clojure-application.md12
-rw-r--r--doc/ci/examples/test-phoenix-application.md10
-rw-r--r--doc/ci/git_submodules.md6
-rw-r--r--doc/ci/permissions/README.md2
-rw-r--r--doc/ci/quick_start/README.md2
-rw-r--r--doc/ci/runners/README.md2
-rw-r--r--doc/ci/services/README.md6
-rw-r--r--doc/ci/services/docker-services.md12
-rw-r--r--doc/ci/variables/README.md7
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/README.md7
-rw-r--r--doc/development/database_debugging.md55
-rw-r--r--doc/development/doc_styleguide.md4
-rw-r--r--doc/development/fe_guide/axios.md68
-rw-r--r--doc/development/fe_guide/dropdowns.md38
-rw-r--r--doc/development/fe_guide/emojis.md27
-rw-r--r--doc/development/fe_guide/icons.md30
-rw-r--r--doc/development/fe_guide/index.md6
-rw-r--r--doc/development/fe_guide/vue.md70
-rw-r--r--doc/development/fe_guide/vue_resource.md72
-rw-r--r--doc/development/file_storage.md49
-rw-r--r--doc/development/github_importer.md209
-rw-r--r--doc/development/i18n/externalization.md27
-rw-r--r--doc/development/licensing.md6
-rw-r--r--doc/development/limit_ee_conflicts.md6
-rw-r--r--doc/development/migration_style_guide.md38
-rw-r--r--doc/development/query_recorder.md46
-rw-r--r--doc/development/rake_tasks.md10
-rw-r--r--doc/development/testing_guide/best_practices.md29
-rw-r--r--doc/development/testing_guide/testing_levels.md2
-rw-r--r--doc/development/testing_guide/testing_rake_tasks.md2
-rw-r--r--doc/development/ux_guide/components.md33
-rw-r--r--doc/development/ux_guide/img/modals-general-confimation-dialog.pngbin0 -> 51205 bytes
-rw-r--r--doc/development/ux_guide/img/modals-layout-for-modals.pngbin0 -> 68203 bytes
-rw-r--r--doc/development/ux_guide/img/modals-special-confimation-dialog.pngbin0 -> 89978 bytes
-rw-r--r--doc/development/ux_guide/img/modals-three-buttons.pngbin0 -> 54927 bytes
-rw-r--r--doc/development/ux_guide/resources.md6
-rw-r--r--doc/development/ux_guide/users.md75
-rw-r--r--doc/gitlab-basics/README.md4
-rw-r--r--doc/install/README.md4
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/relative_url.md16
-rw-r--r--doc/install/requirements.md10
-rw-r--r--doc/integration/README.md4
-rw-r--r--doc/integration/external-issue-tracker.md1
-rw-r--r--doc/intro/README.md4
-rw-r--r--doc/legal/README.md4
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md31
-rw-r--r--doc/legal/individual_contributor_license_agreement.md27
-rw-r--r--doc/migrate_ci_to_ce/README.md9
-rw-r--r--doc/policy/maintenance.md31
-rw-r--r--doc/raketasks/README.md4
-rw-r--r--doc/raketasks/import.md47
-rw-r--r--doc/raketasks/user_management.md15
-rw-r--r--doc/security/README.md4
-rw-r--r--doc/ssh/README.md8
-rw-r--r--doc/system_hooks/system_hooks.md70
-rw-r--r--doc/topics/autodevops/index.md10
-rw-r--r--doc/university/README.md16
-rw-r--r--doc/university/bookclub/booklist.md4
-rw-r--r--doc/university/bookclub/index.md4
-rw-r--r--doc/university/glossary/README.md157
-rw-r--r--doc/university/high-availability/aws/README.md4
-rw-r--r--doc/university/process/README.md6
-rw-r--r--doc/university/support/README.md6
-rw-r--r--doc/university/training/end-user/README.md4
-rw-r--r--doc/university/training/gitlab_flow.md4
-rw-r--r--doc/university/training/index.md4
-rw-r--r--doc/university/training/topics/additional_resources.md6
-rw-r--r--doc/university/training/topics/agile_git.md4
-rw-r--r--doc/university/training/topics/bisect.md4
-rw-r--r--doc/university/training/topics/cherry_picking.md4
-rw-r--r--doc/university/training/topics/env_setup.md4
-rw-r--r--doc/university/training/topics/explore_gitlab.md4
-rw-r--r--doc/university/training/topics/feature_branching.md4
-rw-r--r--doc/university/training/topics/getting_started.md4
-rw-r--r--doc/university/training/topics/git_add.md4
-rw-r--r--doc/university/training/topics/git_intro.md4
-rw-r--r--doc/university/training/topics/git_log.md8
-rw-r--r--doc/university/training/topics/gitlab_flow.md4
-rw-r--r--doc/university/training/topics/merge_conflicts.md4
-rw-r--r--doc/university/training/topics/merge_requests.md4
-rw-r--r--doc/university/training/topics/rollback_commits.md4
-rw-r--r--doc/university/training/topics/stash.md4
-rw-r--r--doc/university/training/topics/subtree.md8
-rw-r--r--doc/university/training/topics/tags.md4
-rw-r--r--doc/university/training/topics/unstage.md4
-rw-r--r--doc/university/training/user_training.md4
-rw-r--r--doc/update/10.0-to-10.1.md4
-rw-r--r--doc/update/10.1-to-10.2.md360
-rw-r--r--doc/update/2.6-to-3.0.md4
-rw-r--r--doc/update/2.9-to-3.0.md4
-rw-r--r--doc/update/3.0-to-3.1.md4
-rw-r--r--doc/update/3.1-to-4.0.md4
-rw-r--r--doc/update/4.0-to-4.1.md4
-rw-r--r--doc/update/4.1-to-4.2.md4
-rw-r--r--doc/update/4.2-to-5.0.md4
-rw-r--r--doc/update/5.0-to-5.1.md4
-rw-r--r--doc/update/5.1-to-5.2.md4
-rw-r--r--doc/update/5.1-to-5.4.md4
-rw-r--r--doc/update/5.1-to-6.0.md4
-rw-r--r--doc/update/5.2-to-5.3.md4
-rw-r--r--doc/update/5.3-to-5.4.md4
-rw-r--r--doc/update/5.4-to-6.0.md4
-rw-r--r--doc/update/6.0-to-6.1.md4
-rw-r--r--doc/update/6.1-to-6.2.md4
-rw-r--r--doc/update/6.2-to-6.3.md4
-rw-r--r--doc/update/6.3-to-6.4.md4
-rw-r--r--doc/update/6.4-to-6.5.md4
-rw-r--r--doc/update/6.5-to-6.6.md4
-rw-r--r--doc/update/6.6-to-6.7.md4
-rw-r--r--doc/update/6.7-to-6.8.md4
-rw-r--r--doc/update/6.8-to-6.9.md4
-rw-r--r--doc/update/6.9-to-7.0.md4
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md4
-rw-r--r--doc/update/7.0-to-7.1.md4
-rw-r--r--doc/update/7.1-to-7.2.md4
-rw-r--r--doc/update/7.10-to-7.11.md4
-rw-r--r--doc/update/7.11-to-7.12.md4
-rw-r--r--doc/update/7.12-to-7.13.md4
-rw-r--r--doc/update/7.13-to-7.14.md4
-rw-r--r--doc/update/7.14-to-8.0.md4
-rw-r--r--doc/update/7.2-to-7.3.md4
-rw-r--r--doc/update/7.3-to-7.4.md4
-rw-r--r--doc/update/7.4-to-7.5.md4
-rw-r--r--doc/update/7.5-to-7.6.md4
-rw-r--r--doc/update/7.6-to-7.7.md4
-rw-r--r--doc/update/7.7-to-7.8.md4
-rw-r--r--doc/update/7.8-to-7.9.md4
-rw-r--r--doc/update/7.9-to-7.10.md4
-rw-r--r--doc/update/8.0-to-8.1.md4
-rw-r--r--doc/update/8.1-to-8.2.md4
-rw-r--r--doc/update/8.10-to-8.11.md4
-rw-r--r--doc/update/8.11-to-8.12.md4
-rw-r--r--doc/update/8.12-to-8.13.md4
-rw-r--r--doc/update/8.13-to-8.14.md4
-rw-r--r--doc/update/8.14-to-8.15.md4
-rw-r--r--doc/update/8.15-to-8.16.md4
-rw-r--r--doc/update/8.16-to-8.17.md4
-rw-r--r--doc/update/8.17-to-9.0.md4
-rw-r--r--doc/update/8.2-to-8.3.md4
-rw-r--r--doc/update/8.3-to-8.4.md4
-rw-r--r--doc/update/8.4-to-8.5.md4
-rw-r--r--doc/update/8.5-to-8.6.md4
-rw-r--r--doc/update/8.6-to-8.7.md4
-rw-r--r--doc/update/8.7-to-8.8.md4
-rw-r--r--doc/update/8.8-to-8.9.md4
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--doc/update/9.0-to-9.1.md4
-rw-r--r--doc/update/9.1-to-9.2.md4
-rw-r--r--doc/update/9.2-to-9.3.md4
-rw-r--r--doc/update/9.3-to-9.4.md4
-rw-r--r--doc/update/9.4-to-9.5.md4
-rw-r--r--doc/update/9.5-to-10.0.md4
-rw-r--r--doc/update/patch_versions.md4
-rw-r--r--doc/update/upgrader.md4
-rw-r--r--[-rwxr-xr-x]doc/user/discussions/img/image_resolved_discussion.pngbin48234 -> 48234 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/discussions/img/onion_skin_view.pngbin45053 -> 45053 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/discussions/img/swipe_view.pngbin16483 -> 16483 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/discussions/img/two_up_view.pngbin61759 -> 61759 bytes
-rw-r--r--doc/user/markdown.md32
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/profile/index.md2
-rw-r--r--doc/user/profile/personal_access_tokens.md12
-rw-r--r--doc/user/profile/preferences.md3
-rw-r--r--doc/user/project/clusters/index.md71
-rw-r--r--doc/user/project/container_registry.md3
-rw-r--r--doc/user/project/img/label_priority_sort_order.pngbin0 -> 101667 bytes
-rw-r--r--doc/user/project/img/labels_filter_by_priority.pngbin38717 -> 0 bytes
-rw-r--r--doc/user/project/img/priority_sort_order.pngbin0 -> 102242 bytes
-rw-r--r--doc/user/project/import/github.md29
-rw-r--r--doc/user/project/integrations/custom_issue_tracker.md20
-rw-r--r--doc/user/project/integrations/img/webhook_logs.pngbin24066 -> 132319 bytes
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md4
-rw-r--r--doc/user/project/integrations/webhooks.md22
-rw-r--r--doc/user/project/issue_board.md26
-rw-r--r--doc/user/project/labels.md29
-rw-r--r--doc/user/project/members/index.md2
-rw-r--r--doc/user/project/milestones/index.md3
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md2
-rw-r--r--doc/user/project/pages/getting_started_part_one.md2
-rw-r--r--doc/user/project/pipelines/job_artifacts.md6
-rw-r--r--doc/user/project/pipelines/schedules.md2
-rw-r--r--doc/user/project/pipelines/settings.md4
-rw-r--r--doc/workflow/README.md4
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--features/steps/profile/notifications.rb2
-rw-r--r--features/steps/project/commits/branches.rb8
-rw-r--r--features/steps/project/commits/commits.rb2
-rw-r--r--features/steps/project/issues/issues.rb6
-rw-r--r--features/steps/project/issues/labels.rb2
-rw-r--r--features/steps/project/issues/milestones.rb3
-rw-r--r--features/steps/shared/diff_note.rb9
-rw-r--r--features/steps/shared/note.rb2
-rw-r--r--features/support/capybara.rb29
-rw-r--r--features/support/capybara_helpers.rb10
-rw-r--r--fixtures/emojis/aliases.json4
-rw-r--r--fixtures/emojis/digests.json18
-rwxr-xr-xfixtures/emojis/generate_aliases.rb18
-rw-r--r--fixtures/emojis/index.json33
-rw-r--r--lib/additional_email_headers_interceptor.rb6
-rw-r--r--lib/api/api.rb6
-rw-r--r--lib/api/api_guard.rb151
-rw-r--r--lib/api/branches.rb9
-rw-r--r--lib/api/commits.rb4
-rw-r--r--lib/api/entities.rb34
-rw-r--r--lib/api/groups.rb67
-rw-r--r--lib/api/helpers.rb33
-rw-r--r--lib/api/helpers/custom_validators.rb1
-rw-r--r--lib/api/helpers/internal_helpers.rb12
-rw-r--r--lib/api/helpers/runner.rb1
-rw-r--r--lib/api/internal.rb4
-rw-r--r--lib/api/issues.rb12
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/notes.rb4
-rw-r--r--lib/api/pages_domains.rb22
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/runners.rb4
-rw-r--r--lib/api/services.rb27
-rw-r--r--lib/api/session.rb20
-rw-r--r--lib/api/snippets.rb1
-rw-r--r--lib/api/users.rb7
-rw-r--r--lib/api/v3/branches.rb6
-rw-r--r--lib/api/v3/builds.rb2
-rw-r--r--lib/api/v3/commits.rb4
-rw-r--r--lib/api/v3/runners.rb1
-rw-r--r--lib/api/v3/services.rb20
-rw-r--r--lib/api/v3/snippets.rb2
-rw-r--r--lib/backup/repository.rb24
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/absolute_link_filter.rb34
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb30
-rw-r--r--lib/banzai/filter/mermaid_filter.rb20
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb6
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb35
-rw-r--r--lib/banzai/filter/user_reference_filter.rb57
-rw-r--r--lib/banzai/note_renderer.rb21
-rw-r--r--lib/banzai/object_renderer.rb8
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb3
-rw-r--r--lib/banzai/querying.rb2
-rw-r--r--lib/banzai/reference_parser/user_parser.rb1
-rw-r--r--lib/banzai/renderer.rb13
-rw-r--r--lib/banzai/request_store_reference_cache.rb27
-rw-r--r--lib/constraints/group_url_constrainer.rb2
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/constraints/user_url_constrainer.rb2
-rw-r--r--lib/declarative_policy.rb2
-rw-r--r--lib/declarative_policy/base.rb2
-rw-r--r--lib/declarative_policy/cache.rb2
-rw-r--r--lib/declarative_policy/rule.rb5
-rw-r--r--lib/declarative_policy/runner.rb1
-rw-r--r--lib/feature.rb14
-rw-r--r--lib/file_size_validator.rb1
-rw-r--r--lib/github/client.rb54
-rw-r--r--lib/github/collection.rb29
-rw-r--r--lib/github/error.rb3
-rw-r--r--lib/github/import.rb376
-rw-r--r--lib/github/import/issue.rb13
-rw-r--r--lib/github/import/legacy_diff_note.rb12
-rw-r--r--lib/github/import/merge_request.rb13
-rw-r--r--lib/github/import/note.rb13
-rw-r--r--lib/github/rate_limit.rb27
-rw-r--r--lib/github/repositories.rb19
-rw-r--r--lib/github/representation/base.rb30
-rw-r--r--lib/github/representation/branch.rb55
-rw-r--r--lib/github/representation/comment.rb42
-rw-r--r--lib/github/representation/issuable.rb37
-rw-r--r--lib/github/representation/issue.rb27
-rw-r--r--lib/github/representation/label.rb13
-rw-r--r--lib/github/representation/milestone.rb25
-rw-r--r--lib/github/representation/pull_request.rb71
-rw-r--r--lib/github/representation/release.rb17
-rw-r--r--lib/github/representation/repo.rb6
-rw-r--r--lib/github/representation/user.rb15
-rw-r--r--lib/github/response.rb25
-rw-r--r--lib/github/user.rb24
-rw-r--r--lib/gitlab/access.rb6
-rw-r--r--lib/gitlab/auth.rb31
-rw-r--r--lib/gitlab/auth/request_authenticator.rb25
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb109
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb12
-rw-r--r--lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb30
-rw-r--r--lib/gitlab/bare_repository_import/importer.rb101
-rw-r--r--lib/gitlab/bare_repository_import/repository.rb42
-rw-r--r--lib/gitlab/bare_repository_importer.rb97
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb4
-rw-r--r--lib/gitlab/changes_list.rb1
-rw-r--r--lib/gitlab/checks/change_access.rb12
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb27
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb1
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb3
-rw-r--r--lib/gitlab/ci/build/image.rb1
-rw-r--r--lib/gitlab/ci/config/entry/image.rb1
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb1
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb2
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/created.rb2
-rw-r--r--lib/gitlab/ci/status/failed.rb2
-rw-r--r--lib/gitlab/ci/status/manual.rb2
-rw-r--r--lib/gitlab/ci/status/pending.rb2
-rw-r--r--lib/gitlab/ci/status/running.rb2
-rw-r--r--lib/gitlab/ci/status/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/success.rb2
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/daemon.rb3
-rw-r--r--lib/gitlab/database.rb33
-rw-r--r--lib/gitlab/database/grant.rb30
-rw-r--r--lib/gitlab/database/migration_helpers.rb9
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb12
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb8
-rw-r--r--lib/gitlab/diff/file.rb18
-rw-r--r--lib/gitlab/diff/file_collection/base.rb5
-rw-r--r--lib/gitlab/diff/inline_diff.rb1
-rw-r--r--lib/gitlab/diff/parser.rb1
-rw-r--r--lib/gitlab/diff/position.rb1
-rw-r--r--lib/gitlab/ee_compat_check.rb30
-rw-r--r--lib/gitlab/email/handler/unsubscribe_handler.rb1
-rw-r--r--lib/gitlab/fogbugz_import/client.rb1
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb2
-rw-r--r--lib/gitlab/gcp/model.rb13
-rw-r--r--lib/gitlab/git/blob.rb60
-rw-r--r--lib/gitlab/git/commit.rb2
-rw-r--r--lib/gitlab/git/lfs_changes.rb33
-rw-r--r--lib/gitlab/git/operation_service.rb10
-rw-r--r--lib/gitlab/git/popen.rb9
-rw-r--r--lib/gitlab/git/remote_repository.rb82
-rw-r--r--lib/gitlab/git/repository.rb147
-rw-r--r--lib/gitlab/git/repository_mirroring.rb95
-rw-r--r--lib/gitlab/git/rev_list.rb58
-rw-r--r--lib/gitlab/git/wiki.rb174
-rw-r--r--lib/gitlab/gitaly_client.rb18
-rw-r--r--lib/gitlab/gitaly_client/attributes_bag.rb31
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/diff.rb16
-rw-r--r--lib/gitlab/gitaly_client/diff_stitcher.rb2
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb17
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb10
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb19
-rw-r--r--lib/gitlab/gitaly_client/wiki_file.rb9
-rw-r--r--lib/gitlab/gitaly_client/wiki_page.rb25
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb135
-rw-r--r--lib/gitlab/github_import.rb34
-rw-r--r--lib/gitlab/github_import/bulk_importing.rb25
-rw-r--r--lib/gitlab/github_import/caching.rb151
-rw-r--r--lib/gitlab/github_import/client.rb263
-rw-r--r--lib/gitlab/github_import/importer/diff_note_importer.rb63
-rw-r--r--lib/gitlab/github_import/importer/diff_notes_importer.rb31
-rw-r--r--lib/gitlab/github_import/importer/issue_and_label_links_importer.rb25
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb81
-rw-r--r--lib/gitlab/github_import/importer/issues_importer.rb35
-rw-r--r--lib/gitlab/github_import/importer/label_links_importer.rb52
-rw-r--r--lib/gitlab/github_import/importer/labels_importer.rb55
-rw-r--r--lib/gitlab/github_import/importer/milestones_importer.rb58
-rw-r--r--lib/gitlab/github_import/importer/note_importer.rb54
-rw-r--r--lib/gitlab/github_import/importer/notes_importer.rb31
-rw-r--r--lib/gitlab/github_import/importer/pull_request_importer.rb91
-rw-r--r--lib/gitlab/github_import/importer/pull_requests_importer.rb83
-rw-r--r--lib/gitlab/github_import/importer/releases_importer.rb55
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb96
-rw-r--r--lib/gitlab/github_import/issuable_finder.rb81
-rw-r--r--lib/gitlab/github_import/label_finder.rb37
-rw-r--r--lib/gitlab/github_import/markdown_text.rb30
-rw-r--r--lib/gitlab/github_import/milestone_finder.rb40
-rw-r--r--lib/gitlab/github_import/page_counter.rb31
-rw-r--r--lib/gitlab/github_import/parallel_importer.rb48
-rw-r--r--lib/gitlab/github_import/parallel_scheduling.rb162
-rw-r--r--lib/gitlab/github_import/rate_limit_error.rb9
-rw-r--r--lib/gitlab/github_import/representation.rb25
-rw-r--r--lib/gitlab/github_import/representation/diff_note.rb87
-rw-r--r--lib/gitlab/github_import/representation/expose_attribute.rb26
-rw-r--r--lib/gitlab/github_import/representation/issue.rb80
-rw-r--r--lib/gitlab/github_import/representation/note.rb70
-rw-r--r--lib/gitlab/github_import/representation/pull_request.rb114
-rw-r--r--lib/gitlab/github_import/representation/to_hash.rb31
-rw-r--r--lib/gitlab/github_import/representation/user.rb34
-rw-r--r--lib/gitlab/github_import/sequential_importer.rb50
-rw-r--r--lib/gitlab/github_import/user_finder.rb164
-rw-r--r--lib/gitlab/gitlab_import/client.rb1
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/hook_data/issue_builder.rb2
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml4
-rw-r--r--lib/gitlab/import_export/importer.rb4
-rw-r--r--lib/gitlab/import_export/merge_request_parser.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb5
-rw-r--r--lib/gitlab/import_sources.rb4
-rw-r--r--lib/gitlab/issuable_metadata.rb8
-rw-r--r--lib/gitlab/job_waiter.rb8
-rw-r--r--lib/gitlab/kubernetes/helm.rb96
-rw-r--r--lib/gitlab/kubernetes/namespace.rb30
-rw-r--r--lib/gitlab/kubernetes/pod.rb12
-rw-r--r--lib/gitlab/ldap/auth_hash.rb2
-rw-r--r--lib/gitlab/ldap/authentication.rb1
-rw-r--r--lib/gitlab/ldap/user.rb6
-rw-r--r--lib/gitlab/legacy_github_import/base_formatter.rb (renamed from lib/gitlab/github_import/base_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/branch_formatter.rb (renamed from lib/gitlab/github_import/branch_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/client.rb148
-rw-r--r--lib/gitlab/legacy_github_import/comment_formatter.rb (renamed from lib/gitlab/github_import/comment_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb (renamed from lib/gitlab/github_import/importer.rb)3
-rw-r--r--lib/gitlab/legacy_github_import/issuable_formatter.rb (renamed from lib/gitlab/github_import/issuable_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/issue_formatter.rb (renamed from lib/gitlab/github_import/issue_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/label_formatter.rb (renamed from lib/gitlab/github_import/label_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/milestone_formatter.rb (renamed from lib/gitlab/github_import/milestone_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/project_creator.rb (renamed from lib/gitlab/github_import/project_creator.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/pull_request_formatter.rb (renamed from lib/gitlab/github_import/pull_request_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/release_formatter.rb (renamed from lib/gitlab/github_import/release_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/user_formatter.rb (renamed from lib/gitlab/github_import/user_formatter.rb)2
-rw-r--r--lib/gitlab/legacy_github_import/wiki_formatter.rb (renamed from lib/gitlab/github_import/wiki_formatter.rb)2
-rw-r--r--lib/gitlab/lfs_token.rb4
-rw-r--r--lib/gitlab/metrics/background_transaction.rb14
-rw-r--r--lib/gitlab/metrics/base_sampler.rb63
-rw-r--r--lib/gitlab/metrics/influx_db.rb31
-rw-r--r--lib/gitlab/metrics/influx_sampler.rb101
-rw-r--r--lib/gitlab/metrics/instrumentation.rb11
-rw-r--r--lib/gitlab/metrics/method_call.rb54
-rw-r--r--lib/gitlab/metrics/prometheus.rb30
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb67
-rw-r--r--lib/gitlab/metrics/samplers/base_sampler.rb64
-rw-r--r--lib/gitlab/metrics/samplers/influx_sampler.rb103
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb111
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb50
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb14
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb15
-rw-r--r--lib/gitlab/metrics/subscribers/rails_cache.rb42
-rw-r--r--lib/gitlab/metrics/system.rb4
-rw-r--r--lib/gitlab/metrics/transaction.rb117
-rw-r--r--lib/gitlab/metrics/unicorn_sampler.rb48
-rw-r--r--lib/gitlab/metrics/web_transaction.rb82
-rw-r--r--lib/gitlab/middleware/go.rb15
-rw-r--r--lib/gitlab/middleware/rails_queue_duration.rb13
-rw-r--r--lib/gitlab/middleware/read_only.rb19
-rw-r--r--lib/gitlab/multi_collection_paginator.rb4
-rw-r--r--lib/gitlab/o_auth/user.rb4
-rw-r--r--lib/gitlab/optimistic_locking.rb1
-rw-r--r--lib/gitlab/path_regex.rb16
-rw-r--r--lib/gitlab/performance_bar/peek_query_tracker.rb2
-rw-r--r--lib/gitlab/regex.rb2
-rw-r--r--lib/gitlab/routing.rb19
-rw-r--r--lib/gitlab/saml/user.rb1
-rw-r--r--lib/gitlab/shell.rb4
-rw-r--r--lib/gitlab/shell_adapter.rb2
-rw-r--r--lib/gitlab/sherlock/transaction.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb41
-rw-r--r--lib/gitlab/string_range_marker.rb1
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb1
-rw-r--r--lib/gitlab/testing/request_blocker_middleware.rb12
-rw-r--r--lib/gitlab/testing/request_inspector_middleware.rb71
-rw-r--r--lib/gitlab/url_blocker.rb4
-rw-r--r--lib/gitlab/url_sanitizer.rb1
-rw-r--r--lib/gitlab/usage_data.rb6
-rw-r--r--lib/gitlab/utils/strong_memoize.rb31
-rw-r--r--lib/gitlab/visibility_level.rb1
-rw-r--r--lib/gitlab/workhorse.rb6
-rw-r--r--lib/google_api/cloud_platform/client.rb1
-rw-r--r--lib/haml_lint/inline_javascript.rb1
-rw-r--r--lib/rouge/lexers/math.rb9
-rw-r--r--lib/rouge/lexers/plantuml.rb9
-rw-r--r--lib/system_check/app/git_user_default_ssh_config_check.rb4
-rw-r--r--lib/system_check/app/ruby_version_check.rb2
-rw-r--r--lib/system_check/simple_executor.rb1
-rw-r--r--lib/tasks/gemojione.rake31
-rw-r--r--lib/tasks/gitlab/backup.rake39
-rw-r--r--lib/tasks/gitlab/cleanup.rake7
-rw-r--r--lib/tasks/gitlab/dev.rake7
-rw-r--r--lib/tasks/gitlab/gitaly.rake17
-rw-r--r--lib/tasks/gitlab/import.rake14
-rw-r--r--lib/tasks/gitlab/sidekiq.rake47
-rw-r--r--lib/tasks/gitlab/users.rake11
-rw-r--r--lib/tasks/import.rake38
-rw-r--r--lib/tasks/tokens.rake12
-rw-r--r--locale/bg/gitlab.po333
-rw-r--r--locale/de/gitlab.po327
-rw-r--r--locale/eo/gitlab.po329
-rw-r--r--locale/es/gitlab.po327
-rw-r--r--locale/fr/gitlab.po791
-rw-r--r--locale/gitlab.pot4
-rw-r--r--locale/it/gitlab.po327
-rw-r--r--locale/ja/gitlab.po325
-rw-r--r--locale/ko/gitlab.po325
-rw-r--r--locale/nl_NL/gitlab.po327
-rw-r--r--locale/pt_BR/gitlab.po1089
-rw-r--r--locale/ru/gitlab.po705
-rw-r--r--locale/uk/gitlab.po619
-rw-r--r--locale/zh_CN/gitlab.po451
-rw-r--r--locale/zh_HK/gitlab.po325
-rw-r--r--locale/zh_TW/gitlab.po331
-rw-r--r--package.json10
-rw-r--r--qa/.gitignore1
-rw-r--r--qa/Dockerfile7
-rwxr-xr-xqa/bin/qa2
-rw-r--r--qa/qa.rb8
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/base.rb2
-rw-r--r--qa/qa/page/main/entry.rb22
-rw-r--r--qa/qa/page/main/login.rb19
-rw-r--r--qa/qa/page/mattermost/login.rb19
-rw-r--r--qa/qa/page/mattermost/main.rb11
-rw-r--r--qa/qa/runtime/scenario.rb28
-rw-r--r--qa/qa/scenario/bootable.rb45
-rw-r--r--qa/qa/scenario/entrypoint.rb29
-rw-r--r--qa/qa/scenario/test/integration/mattermost.rb6
-rw-r--r--qa/qa/specs/config.rb11
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb3
-rw-r--r--qa/qa/specs/features/mattermost/group_create_spec.rb3
-rw-r--r--qa/qa/specs/features/mattermost/login_spec.rb24
-rw-r--r--qa/qa/specs/features/project/create_spec.rb3
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb3
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb3
-rw-r--r--qa/qa/specs/runner.rb22
-rw-r--r--qa/spec/runtime/scenario_spec.rb27
-rw-r--r--qa/spec/scenario/bootable_spec.rb24
-rw-r--r--qa/spec/scenario/entrypoint_spec.rb44
-rw-r--r--rubocop/cop/line_break_after_guard_clauses.rb100
-rw-r--r--rubocop/cop/migration/update_large_table.rb (renamed from rubocop/cop/migration/add_column_with_default_to_large_table.rb)22
-rw-r--r--rubocop/rubocop.rb3
-rw-r--r--scripts/create_mysql_user.sh8
-rw-r--r--scripts/create_postgres_user.sh8
-rw-r--r--scripts/prepare_build.sh14
-rwxr-xr-xscripts/static-analysis23
-rwxr-xr-xscripts/trigger-build-docs37
-rw-r--r--spec/controllers/application_controller_spec.rb90
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb107
-rw-r--r--spec/controllers/concerns/lfs_request_spec.rb50
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb4
-rw-r--r--spec/controllers/groups/children_controller_spec.rb11
-rw-r--r--spec/controllers/import/github_controller_spec.rb4
-rw-r--r--spec/controllers/metrics_controller_spec.rb14
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb85
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb488
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb32
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb41
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb25
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb64
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb28
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb41
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb14
-rw-r--r--spec/controllers/projects_controller_spec.rb11
-rw-r--r--spec/factories/clusters/applications/helm.rb35
-rw-r--r--spec/factories/clusters/applications/ingress.rb35
-rw-r--r--spec/factories/clusters/cluster.rb39
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb20
-rw-r--r--spec/factories/clusters/providers/gcp.rb32
-rw-r--r--spec/factories/commit_statuses.rb1
-rw-r--r--spec/factories/fork_network_members.rb8
-rw-r--r--spec/factories/gcp/cluster.rb38
-rw-r--r--spec/factories/group_custom_attributes.rb7
-rw-r--r--spec/factories/merge_requests.rb4
-rw-r--r--spec/factories/notes.rb1
-rw-r--r--spec/factories/project_custom_attributes.rb7
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb2
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_hooks_spec.rb4
-rw-r--r--spec/features/admin/admin_labels_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb2
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb4
-rw-r--r--spec/features/admin/admin_users_spec.rb27
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb6
-rw-r--r--spec/features/atom/dashboard_spec.rb6
-rw-r--r--spec/features/atom/issues_spec.rb6
-rw-r--r--spec/features/atom/users_spec.rb6
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb18
-rw-r--r--spec/features/boards/sidebar_spec.rb26
-rw-r--r--spec/features/calendar_spec.rb6
-rw-r--r--spec/features/commits_spec.rb29
-rw-r--r--spec/features/container_registry_spec.rb2
-rw-r--r--spec/features/copy_as_gfm_spec.rb4
-rw-r--r--spec/features/dashboard/group_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb2
-rw-r--r--spec/features/dashboard/issues_spec.rb8
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb16
-rw-r--r--spec/features/discussion_comments/commit_spec.rb2
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb2
-rw-r--r--spec/features/explore/new_menu_spec.rb10
-rw-r--r--spec/features/groups/members/manage_members.rb6
-rw-r--r--spec/features/groups_spec.rb2
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb2
-rw-r--r--spec/features/issues/create_branch_merge_request_spec.rb13
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb24
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb3
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb49
-rw-r--r--spec/features/issues/issue_detail_spec.rb7
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb10
-rw-r--r--spec/features/issues/move_spec.rb6
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb4
-rw-r--r--spec/features/issues_spec.rb14
-rw-r--r--spec/features/markdown_spec.rb6
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb6
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb22
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb20
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb12
-rw-r--r--spec/features/merge_requests/diffs_spec.rb10
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb16
-rw-r--r--spec/features/merge_requests/form_spec.rb2
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb9
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb2
-rw-r--r--spec/features/merge_requests/versions_spec.rb8
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb2
-rw-r--r--spec/features/milestone_spec.rb29
-rw-r--r--spec/features/profile_spec.rb37
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb4
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb6
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb2
-rw-r--r--spec/features/projects/artifacts/download_spec.rb2
-rw-r--r--spec/features/projects/artifacts/file_spec.rb1
-rw-r--r--spec/features/projects/branches_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb111
-rw-r--r--spec/features/projects/commit/diff_notes_spec.rb4
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb7
-rw-r--r--spec/features/projects/deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb8
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb26
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin679559 -> 688161 bytes
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb38
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb3
-rw-r--r--spec/features/projects/members/list_spec.rb16
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb4
-rw-r--r--spec/features/projects/members/share_with_group_spec.rb4
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb11
-rw-r--r--spec/features/projects/merge_requests/user_edits_merge_request_spec.rb5
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb4
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb26
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb52
-rw-r--r--spec/features/projects/project_settings_spec.rb6
-rw-r--r--spec/features/projects/ref_switcher_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_jira_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb8
-rw-r--r--spec/features/projects/services/user_activates_packagist_spec.rb24
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb1
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb40
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb3
-rw-r--r--spec/features/projects/snippets/create_snippet_spec.rb4
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb10
-rw-r--r--spec/features/projects/tree/create_file_spec.rb7
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb46
-rw-r--r--spec/features/projects/user_browses_files_spec.rb5
-rw-r--r--spec/features/projects/user_creates_project_spec.rb31
-rw-r--r--spec/features/projects/user_transfers_a_project_spec.rb49
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb6
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb13
-rw-r--r--spec/features/projects_spec.rb4
-rw-r--r--spec/features/protected_branches_spec.rb8
-rw-r--r--spec/features/raven_js_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb4
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb8
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb4
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb6
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb7
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb16
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb2
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb2
-rw-r--r--spec/features/triggers_spec.rb22
-rw-r--r--spec/features/u2f_spec.rb24
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb26
-rw-r--r--spec/features/users_spec.rb1
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/finders/autocomplete_users_finder_spec.rb15
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json33
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json44
-rw-r--r--spec/fixtures/api/schemas/entities/issue_sidebar.json21
-rw-r--r--spec/fixtures/api/schemas/entities/label.json26
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json4
-rw-r--r--spec/fixtures/api/schemas/issue.json29
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json18
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domains.json21
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json6
-rw-r--r--spec/fixtures/clusters/sample_cert.pem33
-rw-r--r--spec/fixtures/markdown.md.erb34
-rw-r--r--spec/helpers/application_helper_spec.rb73
-rw-r--r--spec/helpers/ci_status_helper_spec.rb12
-rw-r--r--spec/helpers/events_helper_spec.rb90
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb26
-rw-r--r--spec/helpers/groups_helper_spec.rb32
-rw-r--r--spec/helpers/icons_helper_spec.rb28
-rw-r--r--spec/helpers/issuables_helper_spec.rb32
-rw-r--r--spec/helpers/labels_helper_spec.rb2
-rw-r--r--spec/helpers/markup_helper_spec.rb151
-rw-r--r--spec/helpers/namespaces_helper_spec.rb25
-rw-r--r--spec/helpers/tree_helper_spec.rb32
-rw-r--r--spec/initializers/8_metrics_spec.rb5
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js47
-rw-r--r--spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js19
-rw-r--r--spec/javascripts/boards/board_card_spec.js29
-rw-r--r--spec/javascripts/boards/issue_spec.js13
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js257
-rw-r--r--spec/javascripts/clusters/components/application_row_spec.js237
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js42
-rw-r--r--spec/javascripts/clusters/services/mock_data.js50
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js89
-rw-r--r--spec/javascripts/clusters_spec.js79
-rw-r--r--spec/javascripts/copy_as_gfm_spec.js49
-rw-r--r--spec/javascripts/emoji_spec.js19
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js19
-rw-r--r--spec/javascripts/fixtures/clusters.rb2
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml8
-rw-r--r--spec/javascripts/fixtures/search_autocomplete.html.haml1
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js22
-rw-r--r--spec/javascripts/gl_form_spec.js4
-rw-r--r--spec/javascripts/groups/components/app_spec.js4
-rw-r--r--spec/javascripts/header_spec.js3
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js5
-rw-r--r--spec/javascripts/issuable_context_spec.js33
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js51
-rw-r--r--spec/javascripts/issue_show/components/edit_actions_spec.js9
-rw-r--r--spec/javascripts/issue_spec.js34
-rw-r--r--spec/javascripts/jobs/job_details_mediator_spec.js20
-rw-r--r--spec/javascripts/jobs/mock_data.js2
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js3
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js30
-rw-r--r--spec/javascripts/lib/utils/datefix_spec.js6
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js27
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js6
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js62
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js116
-rw-r--r--spec/javascripts/merge_request_notes_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph/legend_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js19
-rw-r--r--spec/javascripts/monitoring/utils/multiple_time_series_spec.js2
-rw-r--r--spec/javascripts/namespace_select_spec.js65
-rw-r--r--spec/javascripts/new_branch_spec.js3
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js25
-rw-r--r--spec/javascripts/notes_spec.js26
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/mock_data.js12
-rw-r--r--spec/javascripts/pipelines/graph/stage_column_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/navigation_tabs_spec.js128
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js133
-rw-r--r--spec/javascripts/repo/components/new_branch_form_spec.js76
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js138
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js178
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js103
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js207
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js82
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js62
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js83
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js74
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js42
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js56
-rw-r--r--spec/javascripts/repo/components/repo_preview_spec.js32
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js168
-rw-r--r--spec/javascripts/repo/components/repo_spec.js87
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js104
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js39
-rw-r--r--spec/javascripts/repo/helpers.js15
-rw-r--r--spec/javascripts/repo/mock_data.js14
-rw-r--r--spec/javascripts/repo/services/repo_service_spec.js171
-rw-r--r--spec/javascripts/repo/stores/actions/branch_spec.js38
-rw-r--r--spec/javascripts/repo/stores/actions/file_spec.js417
-rw-r--r--spec/javascripts/repo/stores/actions/tree_spec.js469
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js419
-rw-r--r--spec/javascripts/repo/stores/getters_spec.js119
-rw-r--r--spec/javascripts/repo/stores/mutations/branch_spec.js18
-rw-r--r--spec/javascripts/repo/stores/mutations/file_spec.js131
-rw-r--r--spec/javascripts/repo/stores/mutations/tree_spec.js71
-rw-r--r--spec/javascripts/repo/stores/mutations_spec.js117
-rw-r--r--spec/javascripts/repo/stores/utils_spec.js102
-rw-r--r--spec/javascripts/search_autocomplete_spec.js31
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js4
-rw-r--r--spec/javascripts/sidebar/mock_data.js2
-rw-r--r--spec/javascripts/sidebar/participants_spec.js174
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js17
-rw-r--r--spec/javascripts/sidebar/sidebar_service_spec.js55
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js93
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js36
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js42
-rw-r--r--spec/javascripts/smart_interval_spec.js243
-rw-r--r--spec/javascripts/test_bundle.js40
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js179
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js60
-rw-r--r--spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js47
-rw-r--r--spec/javascripts/vue_shared/ci_action_icons_spec.js27
-rw-r--r--spec/javascripts/vue_shared/ci_status_icon_spec.js27
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js18
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js48
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_warning_spec.js6
-rw-r--r--spec/javascripts/vue_shared/components/loading_button_spec.js18
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js41
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js10
-rw-r--r--spec/javascripts/vue_shared/components/markdown/toolbar_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js49
-rw-r--r--spec/lib/additional_email_headers_interceptor_spec.rb21
-rw-r--r--spec/lib/banzai/commit_renderer_spec.rb2
-rw-r--r--spec/lib/banzai/filter/absolute_link_filter_spec.rb58
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb62
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/mermaid_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb45
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb33
-rw-r--r--spec/lib/banzai/note_renderer_spec.rb24
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb4
-rw-r--r--spec/lib/banzai/renderer_spec.rb2
-rw-r--r--spec/lib/container_registry/path_spec.rb18
-rw-r--r--spec/lib/feature_spec.rb41
-rw-r--r--spec/lib/github/client_spec.rb34
-rw-r--r--spec/lib/github/import/legacy_diff_note_spec.rb9
-rw-r--r--spec/lib/github/import/note_spec.rb9
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb67
-rw-r--r--spec/lib/gitlab/auth/user_auth_finders_spec.rb194
-rw-r--r--spec/lib/gitlab/auth_spec.rb42
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb22
-rw-r--r--spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb62
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb168
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb51
-rw-r--r--spec/lib/gitlab/bare_repository_importer_spec.rb100
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb33
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb46
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/manual_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb2
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb7
-rw-r--r--spec/lib/gitlab/database/grant_spec.rb22
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb16
-rw-r--r--spec/lib/gitlab/database_spec.rb42
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb8
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb55
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb1
-rw-r--r--spec/lib/gitlab/git/lfs_changes_spec.rb48
-rw-r--r--spec/lib/gitlab/git/popen_spec.rb17
-rw-r--r--spec/lib/gitlab/git/remote_repository_spec.rb99
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb295
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb123
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb34
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb13
-rw-r--r--spec/lib/gitlab/gitaly_client/wiki_service_spec.rb88
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb62
-rw-r--r--spec/lib/gitlab/github_import/caching_spec.rb117
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb389
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb152
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb119
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb201
-rw-r--r--spec/lib/gitlab/github_import/importer/issues_importer_spec.rb111
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb82
-rw-r--r--spec/lib/gitlab/github_import/importer/labels_importer_spec.rb107
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb120
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb151
-rw-r--r--spec/lib/gitlab/github_import/importer/notes_importer_spec.rb116
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb221
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb272
-rw-r--r--spec/lib/gitlab/github_import/importer/releases_importer_spec.rb125
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb264
-rw-r--r--spec/lib/gitlab/github_import/issuable_finder_spec.rb38
-rw-r--r--spec/lib/gitlab/github_import/label_finder_spec.rb61
-rw-r--r--spec/lib/gitlab/github_import/markdown_text_spec.rb28
-rw-r--r--spec/lib/gitlab/github_import/milestone_finder_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/page_counter_spec.rb32
-rw-r--r--spec/lib/gitlab/github_import/parallel_importer_spec.rb40
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb296
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_note_spec.rb164
-rw-r--r--spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb19
-rw-r--r--spec/lib/gitlab/github_import/representation/issue_spec.rb182
-rw-r--r--spec/lib/gitlab/github_import/representation/note_spec.rb107
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_spec.rb288
-rw-r--r--spec/lib/gitlab/github_import/representation/to_hash_spec.rb37
-rw-r--r--spec/lib/gitlab/github_import/representation/user_spec.rb33
-rw-r--r--spec/lib/gitlab/github_import/representation_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/sequential_importer_spec.rb37
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb333
-rw-r--r--spec/lib/gitlab/github_import_spec.rb79
-rw-r--r--spec/lib/gitlab/hook_data/issuable_builder_spec.rb7
-rw-r--r--spec/lib/gitlab/hook_data/issue_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml11
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/project.json28
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb11
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml55
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb4
-rw-r--r--spec/lib/gitlab/issuable_metadata_spec.rb12
-rw-r--r--spec/lib/gitlab/kubernetes/helm_spec.rb100
-rw-r--r--spec/lib/gitlab/kubernetes/namespace_spec.rb66
-rw-r--r--spec/lib/gitlab/ldap/authentication_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb8
-rw-r--r--spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/branch_formatter_spec.rb)2
-rw-r--r--spec/lib/gitlab/legacy_github_import/client_spec.rb97
-rw-r--r--spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/comment_formatter_spec.rb)2
-rw-r--r--spec/lib/gitlab/legacy_github_import/importer_spec.rb (renamed from spec/lib/gitlab/github_import/importer_spec.rb)28
-rw-r--r--spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/issuable_formatter_spec.rb)2
-rw-r--r--spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/issue_formatter_spec.rb)14
-rw-r--r--spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/label_formatter_spec.rb)2
-rw-r--r--spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/milestone_formatter_spec.rb)8
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb (renamed from spec/lib/gitlab/github_import/project_creator_spec.rb)2
-rw-r--r--spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/pull_request_formatter_spec.rb)26
-rw-r--r--spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/release_formatter_spec.rb)2
-rw-r--r--spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/user_formatter_spec.rb)2
-rw-r--r--spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb (renamed from spec/lib/gitlab/github_import/wiki_formatter_spec.rb)2
-rw-r--r--spec/lib/gitlab/metrics/background_transaction_spec.rb19
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb3
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb17
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb84
-rw-r--r--spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb (renamed from spec/lib/gitlab/metrics/influx_sampler_spec.rb)2
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb90
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb (renamed from spec/lib/gitlab/metrics/unicorn_sampler_spec.rb)2
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb11
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb17
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb89
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb (renamed from spec/lib/gitlab/metrics/transaction_spec.rb)75
-rw-r--r--spec/lib/gitlab/metrics_spec.rb43
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb144
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb12
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb42
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb11
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb65
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb63
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb16
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb52
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb18
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb2
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb8
-rw-r--r--spec/mailers/notify_spec.rb20
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb166
-rw-r--r--spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb25
-rw-r--r--spec/migrations/remove_empty_fork_networks_spec.rb24
-rw-r--r--spec/migrations/schedule_merge_request_diff_migrations_spec.rb19
-rw-r--r--spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb19
-rw-r--r--spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb64
-rw-r--r--spec/models/blob_spec.rb17
-rw-r--r--spec/models/ci/build_spec.rb18
-rw-r--r--spec/models/ci/pipeline_spec.rb126
-rw-r--r--spec/models/clusters/applications/helm_spec.rb102
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb108
-rw-r--r--spec/models/clusters/cluster_spec.rb202
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb188
-rw-r--r--spec/models/clusters/project_spec.rb6
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb183
-rw-r--r--spec/models/commit_collection_spec.rb59
-rw-r--r--spec/models/commit_spec.rb11
-rw-r--r--spec/models/commit_status_spec.rb73
-rw-r--r--spec/models/concerns/avatarable_spec.rb44
-rw-r--r--spec/models/concerns/ignorable_column_spec.rb12
-rw-r--r--spec/models/concerns/issuable_spec.rb37
-rw-r--r--spec/models/concerns/milestoneish_spec.rb17
-rw-r--r--spec/models/concerns/routable_spec.rb1
-rw-r--r--spec/models/concerns/subscribable_spec.rb6
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/diff_note_spec.rb6
-rw-r--r--spec/models/diff_viewer/base_spec.rb22
-rw-r--r--spec/models/diff_viewer/server_side_spec.rb9
-rw-r--r--spec/models/environment_spec.rb16
-rw-r--r--spec/models/fork_network_member_spec.rb18
-rw-r--r--spec/models/fork_network_spec.rb10
-rw-r--r--spec/models/gcp/cluster_spec.rb264
-rw-r--r--spec/models/group_custom_attribute_spec.rb16
-rw-r--r--spec/models/group_spec.rb58
-rw-r--r--spec/models/identity_spec.rb24
-rw-r--r--spec/models/issue_spec.rb18
-rw-r--r--spec/models/key_spec.rb23
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb83
-rw-r--r--spec/models/merge_request_spec.rb49
-rw-r--r--spec/models/milestone_spec.rb2
-rw-r--r--spec/models/note_spec.rb31
-rw-r--r--spec/models/project_custom_attribute_spec.rb16
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb13
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb1
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb6
-rw-r--r--spec/models/project_services/packagist_service_spec.rb46
-rw-r--r--spec/models/project_spec.rb192
-rw-r--r--spec/models/project_wiki_spec.rb135
-rw-r--r--spec/models/repository_spec.rb45
-rw-r--r--spec/models/user_spec.rb121
-rw-r--r--spec/models/wiki_page_spec.rb4
-rw-r--r--spec/policies/ci/build_policy_spec.rb77
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb (renamed from spec/policies/gcp/cluster_policy_spec.rb)6
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb (renamed from spec/presenters/gcp/cluster_presenter_spec.rb)11
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb10
-rw-r--r--spec/requests/api/groups_spec.rb146
-rw-r--r--spec/requests/api/helpers_spec.rb462
-rw-r--r--spec/requests/api/internal_spec.rb46
-rw-r--r--spec/requests/api/jobs_spec.rb33
-rw-r--r--spec/requests/api/merge_requests_spec.rb62
-rw-r--r--spec/requests/api/pages_domains_spec.rb47
-rw-r--r--spec/requests/api/projects_spec.rb13
-rw-r--r--spec/requests/api/services_spec.rb21
-rw-r--r--spec/requests/api/session_spec.rb107
-rw-r--r--spec/requests/api/users_spec.rb41
-rw-r--r--spec/requests/api/v3/builds_spec.rb2
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb4
-rw-r--r--spec/requests/api/v3/projects_spec.rb2
-rw-r--r--spec/requests/lfs_http_spec.rb47
-rw-r--r--spec/requests/openid_connect_spec.rb13
-rw-r--r--spec/requests/rack_attack_global_spec.rb362
-rw-r--r--spec/routing/group_routing_spec.rb133
-rw-r--r--spec/routing/project_routing_spec.rb19
-rw-r--r--spec/routing/routing_spec.rb41
-rw-r--r--spec/rubocop/cop/line_break_after_guard_clauses_spec.rb160
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb44
-rw-r--r--spec/rubocop/cop/migration/update_large_table_spec.rb69
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb30
-rw-r--r--spec/serializers/cluster_entity_spec.rb51
-rw-r--r--spec/serializers/cluster_serializer_spec.rb21
-rw-r--r--spec/serializers/issue_entity_spec.rb20
-rw-r--r--spec/serializers/issue_serializer_spec.rb27
-rw-r--r--spec/serializers/merge_request_basic_serializer_spec.rb10
-rw-r--r--spec/serializers/merge_request_entity_spec.rb11
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb12
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb2
-rw-r--r--spec/services/applications/create_service_spec.rb13
-rw-r--r--spec/services/base_count_service_spec.rb80
-rw-r--r--spec/services/ci/create_cluster_service_spec.rb47
-rw-r--r--spec/services/ci/fetch_gcp_operation_service_spec.rb36
-rw-r--r--spec/services/ci/finalize_cluster_creation_service_spec.rb61
-rw-r--r--spec/services/ci/integrate_cluster_service_spec.rb42
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb24
-rw-r--r--spec/services/ci/provision_cluster_service_spec.rb85
-rw-r--r--spec/services/ci/update_cluster_service_spec.rb37
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb91
-rw-r--r--spec/services/clusters/applications/install_service_spec.rb60
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb55
-rw-r--r--spec/services/clusters/create_service_spec.rb64
-rw-r--r--spec/services/clusters/gcp/fetch_operation_service_spec.rb43
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb111
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb69
-rw-r--r--spec/services/clusters/gcp/verify_provision_status_service_spec.rb107
-rw-r--r--spec/services/clusters/update_service_spec.rb59
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb8
-rw-r--r--spec/services/events/render_service_spec.rb37
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb49
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb22
-rw-r--r--spec/services/merge_requests/update_service_spec.rb2
-rw-r--r--spec/services/milestones/destroy_service_spec.rb4
-rw-r--r--spec/services/milestones/promote_service_spec.rb113
-rw-r--r--spec/services/notes/render_service_spec.rb31
-rw-r--r--spec/services/notification_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb22
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb16
-rw-r--r--spec/services/projects/hashed_storage_migration_service_spec.rb2
-rw-r--r--spec/services/projects/import_service_spec.rb83
-rw-r--r--spec/services/projects/transfer_service_spec.rb32
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb18
-rw-r--r--spec/services/system_hooks_service_spec.rb38
-rw-r--r--spec/services/todo_service_spec.rb12
-rw-r--r--spec/services/users/keys_count_service_spec.rb66
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--spec/support/api_helpers.rb28
-rw-r--r--spec/support/bare_repo_operations.rb60
-rw-r--r--spec/support/capybara.rb44
-rw-r--r--spec/support/capybara_helpers.rb2
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb36
-rw-r--r--spec/support/cookie_helper.rb17
-rw-r--r--spec/support/cycle_analytics_helpers.rb1
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb27
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb2
-rw-r--r--spec/support/fixture_helpers.rb1
-rwxr-xr-xspec/support/generate-seed-repo-rb1
-rw-r--r--spec/support/gitaly.rb9
-rw-r--r--spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535bin0 -> 304 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cfbin0 -> 597 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb162
-rw-r--r--spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31ebin0 -> 185 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3fbin0 -> 642 bytes
-rw-r--r--spec/support/gitlab_stubs/session.json4
-rw-r--r--spec/support/gitlab_stubs/user.json6
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb119
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb2
-rw-r--r--spec/support/helpers/note_interaction_helpers.rb2
-rw-r--r--spec/support/input_helper.rb7
-rw-r--r--spec/support/inspect_requests.rb17
-rw-r--r--spec/support/kubernetes_helpers.rb37
-rw-r--r--spec/support/legacy_path_redirect_shared_examples.rb13
-rw-r--r--spec/support/live_debugger.rb17
-rw-r--r--spec/support/login_helpers.rb18
-rw-r--r--spec/support/matchers/access_matchers_for_controller.rb2
-rw-r--r--spec/support/matchers/security_header_matcher.rb5
-rw-r--r--spec/support/mobile_helpers.rb2
-rw-r--r--spec/support/protected_tags/access_control_ce_shared_examples.rb2
-rw-r--r--spec/support/query_recorder.rb7
-rw-r--r--spec/support/quick_actions_helpers.rb2
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce.rb6
-rw-r--r--spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb5
-rw-r--r--spec/support/stub_configuration.rb4
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/support/time_tracking_shared_examples.rb2
-rw-r--r--spec/support/wait_for_requests.rb36
-rw-r--r--spec/tasks/gitlab/cleanup_rake_spec.rb41
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb17
-rw-r--r--spec/tasks/gitlab/users_rake_spec.rb38
-rw-r--r--spec/tasks/tokens_spec.rb6
-rw-r--r--spec/unicorn/unicorn_spec.rb1
-rw-r--r--spec/uploaders/file_uploader_spec.rb78
-rw-r--r--spec/validators/dynamic_path_validator_spec.rb97
-rw-r--r--spec/validators/namespace_path_validator_spec.rb38
-rw-r--r--spec/validators/project_path_validator_spec.rb38
-rw-r--r--spec/validators/user_path_validator_spec.rb38
-rw-r--r--spec/views/projects/commit/branches.html.haml_spec.rb109
-rw-r--r--spec/views/shared/issuable/_participants.html.haml.rb26
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb19
-rw-r--r--spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb49
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb70
-rw-r--r--spec/workers/concerns/gitlab/github_import/queue_spec.rb12
-rw-r--r--spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb110
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb77
-rw-r--r--spec/workers/gitlab/github_import/advance_stage_worker_spec.rb115
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb42
-rw-r--r--spec/workers/gitlab/github_import/import_issue_worker_spec.rb45
-rw-r--r--spec/workers/gitlab/github_import/import_note_worker_spec.rb40
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb51
-rw-r--r--spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb95
-rw-r--r--spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb32
-rw-r--r--spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb30
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb32
-rw-r--r--spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb29
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb32
-rw-r--r--spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb49
-rw-r--r--spec/workers/repository_import_worker_spec.rb23
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb12
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb61
-rw-r--r--vendor/assets/javascripts/autosize.js243
-rw-r--r--vendor/assets/javascripts/fuzzaldrin-plus.js1161
-rw-r--r--vendor/assets/javascripts/latinise.js11
-rw-r--r--vendor/assets/javascripts/peek.js28
-rw-r--r--vendor/gitignore/Android.gitignore2
-rw-r--r--vendor/gitignore/Composer.gitignore2
-rw-r--r--vendor/gitignore/Global/Windows.gitignore2
-rw-r--r--vendor/gitignore/Perl.gitignore2
-rw-r--r--vendor/gitignore/Terraform.gitignore15
-rw-r--r--vendor/gitignore/VisualStudio.gitignore6
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/Rust.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml86
-rw-r--r--vendor/licenses.csv228
-rw-r--r--yarn.lock79
2020 files changed, 48957 insertions, 19609 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 87d73fc0c52..d4b375696c2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-62.0-node-8.x-yarn-1.2-postgresql-9.6"
.default-cache: &default-cache
- key: "ruby-233-with-yarn"
+ key: "ruby-235-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
@@ -23,7 +23,6 @@ variables:
SIMPLECOV: "true"
GIT_DEPTH: "20"
GIT_SUBMODULE_STRATEGY: "none"
- PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
@@ -194,7 +193,7 @@ review-docs-deploy:
name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
- url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
+ url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- ./trigger-build-docs deploy
@@ -257,7 +256,7 @@ flaky-examples-check:
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
- allow_failure: yes
+ allow_failure: true
retry: 0
only:
- branches
@@ -417,12 +416,7 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
- allow_failure: yes
retry: 0
- cache:
- key: "ee_compat_check_repo"
- paths:
- - ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
@@ -454,8 +448,9 @@ db:migrate:reset-mysql:
stage: test
variables:
SETUP_DB: "false"
+ CREATE_DB_USER: "true"
script:
- - git fetch origin v8.14.10
+ - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
@@ -479,7 +474,7 @@ migration:path-mysql:
<<: *pull-cache
stage: test
script:
- - bundle exec rake db:rollback STEP=120
+ - bundle exec rake db:rollback STEP=119
- bundle exec rake db:migrate
db:rollback-pg:
@@ -498,6 +493,7 @@ db:rollback-mysql:
variables:
SIZE: "1"
SETUP_DB: "false"
+ CREATE_DB_USER: "true"
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
@@ -533,7 +529,6 @@ gitlab:assets:compile:
NODE_ENV: "production"
RAILS_ENV: "production"
SETUP_DB: "false"
- USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
@@ -551,7 +546,6 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
@@ -583,12 +577,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 codeclimate/codeclimate:0.69.0 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]
+qa:internal:
+ stage: test
+ variables:
+ SETUP_DB: "false"
+ services: []
+ script:
+ - cd qa/
+ - bundle install
+ - bundle exec rspec
+
coverage:
<<: *dedicated-runner
<<: *except-docs
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
index 1278061a410..5b55eb1374b 100644
--- a/.gitlab/issue_templates/Feature Proposal.md
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -1,22 +1,3 @@
-Please read this!
-
-Before opening a new issue, make sure to search for keywords in the issues
-filtered by the "feature proposal" label:
-
-For the Community Edition issue tracker:
-
-- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
-
-For the Enterprise Edition issue tracker:
-
-- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal
-
-and verify the issue you're about to submit isn't a duplicate.
-
-Please remove this notice if you're confident your issue isn't a duplicate.
-
-------
-
### Description
(Include problem, use cases, benefits, and/or goals)
@@ -25,26 +6,4 @@ Please remove this notice if you're confident your issue isn't a duplicate.
### Links / references
-### Documentation blurb
-
-#### Overview
-
-What is it?
-Why should someone use this feature?
-What is the underlying (business) problem?
-How do you use this feature?
-
-#### Use cases
-
-Who is this for? Provide one or more use cases.
-
-### Feature checklist
-
-Make sure these are completed before closing the issue,
-with a link to the relevant commit.
-
-- [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance)
-- [ ] Documentation
-- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
-
-/label ~"feature proposal" \ No newline at end of file
+/label ~"feature proposal"
diff --git a/.nvmrc b/.nvmrc
index 72906051c5c..f7ee06693c1 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-7.5 \ No newline at end of file
+9.0.0
diff --git a/.ruby-version b/.ruby-version
index 0bee604df76..cc6c9a491e0 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.3.3
+2.3.5
diff --git a/.scss-lint.yml b/.scss-lint.yml
index d2c972fa9c4..dcd4cac780a 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -14,7 +14,7 @@ linters:
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
- enabled: false
+ enabled: true
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
@@ -112,7 +112,7 @@ linters:
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
- enabled: false
+ enabled: true
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
@@ -241,7 +241,7 @@ linters:
# Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
- enabled: false
+ enabled: true
# Do not use parent selector references (&) when they would otherwise
# be unnecessary.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bca9944bb1..48282f67ed4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,249 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.2.0 (2017-11-22)
+
+### Security (4 changes)
+
+- Upgrade Ruby to 2.3.5 to include security patches. !15099
+- Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
+- Convert private tokens to Personal Access Tokens with sudo scope.
+- Remove private tokens from web interface and API.
+
+### Removed (5 changes)
+
+- Remove help text from group issues page and group merge requests page. !14963
+- Remove overzealous tooltips in projects page tabs. !15017
+- Stop merge requests from fetching their refs when the data is already available. !15129
+- Remove update merge request worker tagging.
+- Remove Session API now that private tokens are removed from user API endpoints.
+
+### Fixed (75 changes, 18 of them are from the community)
+
+- Fix 404 errors in API caused when the branch name had a dot. !14462 (gvieira37)
+- Remove unnecessary alt-texts from pipeline emails. !14602 (gernberg)
+- Renders 404 in commits controller if no commits are found for a given path. !14610 (Guilherme Vieira)
+- Cleanup data-page attribute after each Karma test. !14742
+- Removed extra border radius from .file-editor and .file-holder when editing a file. !14803 (Rachel Pipkin)
+- Add support for markdown preview to group milestones. !14806 (Vitaliy @blackst0ne Klachkov)
+- Fixed 'Removed source branch' checkbox in merge widget being ignored. !14832
+- Fix unnecessary ajax requests in admin broadcast message form. !14853
+- Make NamespaceSelect change URL when filtering. !14888
+- Get true failure from evalulate_script by checking for element beforehand. !14898
+- Fix SAML error 500 when no groups are defined for user. !14913
+- Fix 500 errors caused by empty diffs in some discussions. !14945 (Alexander Popov)
+- Fix the atom feed for group events. !14974
+- Hides pipeline duration in commit box when it is zero (nil). !14979 (gvieira37)
+- Add new diff discussions on MR diffs tab in "realtime". !14981
+- Returns a ssh url for go-get=1. !14990 (gvieira37)
+- Case insensitive search for branches. !14995 (George Andrinopoulos)
+- Fixes 404 error to 'Issues assigned to me' and 'Issues I've created' when issues are disabled. !15021 (Jacopo Beschi @jacopo-beschi)
+- Update the groups API documentation. !15024 (Robert Schilling)
+- Validate username/pw for Jiraservice, require them in the API. !15025 (Robert Schilling)
+- Update Merge Request polling so there is only one request at a time. !15032
+- Use project select dropdown not only as a combobutton. !15043
+- Remove create MR button from issues when MRs are disabled. !15071 (George Andrinopoulos)
+- Tighten up whitelisting of certain Geo routes. !15082
+- Allow to disable the Performance Bar. !15084
+- Refresh open Issue and Merge Request project counter caches when re-opening. !15085 (Rob Ede @robjtede)
+- Fix markdown form tabs toggling preview mode from double clicking write mode button. !15119
+- Fix cancel button not working while uploading on the new issue page. !15137
+- Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
+- Fix issues with forked projects of which the source was deleted. !15150
+- Fix GPG signature popup info in Safari and Firefox. !15228
+- Fix GFM reference links for closed milestones. !15234 (Vitaliy @blackst0ne Klachkov)
+- When deleting merged branches, ignore protected tags. !15252
+- Revert a regression on runners sorting (!15134). !15341 (Takuya Noguchi)
+- Don't use JS to delete memberships from projects and groups. !15344
+- Don't try to create fork network memberships for forks with a missing source. !15366
+- Fix gitlab:backup rake for hashed storage based repositories. !15400
+- Fix issue where clicking a GPG verification badge would scroll to the top of the page. !15407
+- Update container repository path reference and allow using double underscore. !15417
+- Fix crash when navigating to second page of the group dashbaord when there are projects and groups on the first page. !15456
+- Fix flash errors showing up on a non configured prometheus integration. !35652
+- Fix timezone bug in Pikaday and upgrade Pikaday version.
+- Fix arguments Import/Export error importing project merge requests.
+- Moves mini graph of pipeline to the end of sentence in MR widget. Cleans HTML and tests.
+- Fix user autocomplete in subgroups.
+- Fixed user profile activity tab being off-screen on mobile.
+- Fix diff parser so it tolerates to diff special markers in the content.
+- Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
+- Don't create build failed todos when the job is automatically retried.
+- Render 404 when polling commit notes without having permissions.
+- Show error message when fast-forward merge is not possible.
+- Prevents position update for image diff notes.
+- Mobile-friendly table on Admin Runners. (Takuya Noguchi)
+- Decreases z-index of select2 to a lower number of our navigation bar.
+- Fix broken Members link when relative URL root paths are used.
+- Avoid regenerating the ref path for the environment.
+- Memoize GitLab logger to reduce open file descriptors.
+- Fix hashed storage with project transfers to another namespace.
+- Fix bad type checking to prevent 0 count badge to be shown.
+- Fix problem with issuable header wrapping when content is too long.
+- Move retry button in job page to sidebar.
+- Formats bytes to human reabale number in registry table.
+- Fix commit pipeline showing wrong status.
+- Include link to issue in reopen message for Slack and Mattermost notifications.
+- Fix double border UI bug on pipelines/environments table and pagination.
+- Remove native title tooltip in pipeline jobs dropdown in Safari.
+- Fix namespacing for MergeWhenPipelineSucceedsService in MR API.
+- Prevent error when authorizing an admin-created OAauth application without a set owner.
+- Always return full avatar URL for private/internal groups/projects when asset host is set.
+- Make sure group and project creation is blocked for new users that are external by default.
+- Make sure NotesActions#noteable returns a Noteable in the update action.
+- Reallow project paths ending in periods.
+- Only set Auto-Submitted header once for emails on push.
+- Fix overlap of right-sidebar and main content when creating a Wiki page.
+- Enables scroll to bottom once user has scrolled back to bottom in job log.
+
+### Changed (21 changes, 7 of them are from the community)
+
+- Added possibility to enter past date in /spend command to log time in the past. !3044 (g3dinua, LockiStrike)
+- Add Prometheus equivalent of all InfluxDB metrics. !13891
+- Show collapsible project lists. !14055
+- Make Prometheus metrics endpoint return empty response when metrics are disabled. !14490
+- Support custom attributes on groups and projects. !14593 (Markus Koller)
+- Avoid fetching all branches for branch existence checks. !14778
+- Update participants and subscriptions button in issuable sidebar to be async. !14836
+- Replace WikiPage::CreateService calls with wiki_page factory in specs. !14850 (Jacopo Beschi @jacopo-beschi)
+- Add lazy option to UserAvatarImage. !14895
+- Add readme only option as project view. !14900
+- Todos spelled correctly on Todos list page. !15015
+- Support uml:: and captions in reStructuredText. !15120 (Markus Koller)
+- Add system hooks user_rename and group_rename. !15123
+- Change tags order in refs dropdown. !15235 (Vitaliy @blackst0ne Klachkov)
+- Change default cluster size to n1-default-2. !39649 (Fabio Busatto)
+- Change 'Sign Out' route from a DELETE to a GET. !39708 (Joe Marty)
+- Change background color of nav sidebar to match other gl sidebars.
+- Update i18n section in FE docs for marking and interpolation.
+- Add a count of changes to the merge requests API.
+- Improve GitLab Import rake task to work with Hashed Storage and Subgroups.
+- 14830 Move GitLab export option to top of import list when creating a new project.
+
+### Performance (14 changes)
+
+- Improve branch listing page performance. !14729
+- Improve DashboardController#activity.json performance. !14985
+- Add a latest_merge_request_diff_id column to merge_requests. !15035
+- Improve performance of the /projects/:id/repository/branches API endpoint. !15215
+- Ensure merge requests with lots of version don't time out when searching for pipelines.
+- Speed up issues list APIs.
+- Remove Filesystem check metrics that use too much CPU to handle requests.
+- Disable Unicorn sampling in Sidekiq since there are no Unicorn sockets to monitor.
+- Truncate tree to max 1,000 items and display notice to users.
+- Add Performance improvement as category on the changelog.
+- Cache commits fetched from the repository.
+- Cache the number of user SSH keys.
+- Optimise getting the pipeline status of commits.
+- Improve performance of commits list by fully using DB index when getting commit note counts.
+
+### Added (26 changes, 10 of them are from the community)
+
+- Expose duration in Job entity. !13644 (Mehdi Lahmam (@mehlah))
+- Prevent git push when LFS objects are missing. !13837
+- Automatic configuration settings page. !13850 (Francisco Lopez)
+- Add API endpoints for Pages Domains. !13917 (Travis Miller)
+- Include the changes in issuable webhook payloads. !14308
+- Add Packagist project service. !14493 (Matt Coleman)
+- Add sort runners on admin runners. !14661 (Takuya Noguchi)
+- Repo Editor: Add option to start a new MR directly from comit section. !14665
+- Issue JWT token with registry:catalog:* scope when requested by GitLab admin. !14751 (Vratislav Kalenda)
+- Support show-all-refs for git over HTTP. !14834
+- Add loading button for new UX paradigm. !14883
+- Get Project Branch API shows an helpful error message on invalid refname. !14884 (Jacopo Beschi @jacopo-beschi)
+- Refactor have_http_status into have_gitlab_http_status. !14958 (Jacopo Beschi @jacopo-beschi)
+- Suggest to rename the remote for existing repository instructions. !14970 (helmo42)
+- Adds project_id to pipeline hook data. !15044 (Jacopo Beschi @jacopo-beschi)
+- Hashed Storage support for Attachments. !15068
+- Add metric tagging for sidekiq workers. !15111
+- Expose project visibility as CI variable - CI_PROJECT_VISIBILITY. !15193
+- Allow multiple queries in a single Prometheus graph to support additional environments (Canary, Staging, et al.). !15201
+- Allow promoting project milestones to group milestones.
+- Added submodule support in multi-file editor.
+- Add applications section to GKE clusters page to easily install Helm Tiller, Ingress.
+- Allow files to uploaded in the multi-file editor.
+- Add Ingress to available Cluster applications.
+- Adds typescript support.
+- Add sudo scope for OAuth and Personal Access Tokens to be used by admins to impersonate other users on the API.
+
+### Other (18 changes, 8 of them are from the community)
+
+- Decrease Perceived Complexity threshold to 14. !14231 (Maxim Rydkin)
+- Replace the 'features/explore/projects.feature' spinach test with an rspec analog. !14755 (Vitaliy @blackst0ne Klachkov)
+- While displaying a commit, do not show list of related branches if there are thousands of branches. !14812
+- Removed d3.js from the graph and users bundles and used the common_d3 bundle instead. !14826
+- Make contributors page translatable. !14915
+- Decrease ABC threshold to 54.28. !14920 (Maxim Rydkin)
+- Clarify system_hook triggers in documentation. !14957 (Joe Marty)
+- Free up some reserved group names. !15052
+- Bump carrierwave to 1.2.1. !15072 (Takuya Noguchi)
+- Enable NestingDepth (level 6) on scss-lint. !15073 (Takuya Noguchi)
+- Enable BorderZero rule in scss-lint. !15168 (Takuya Noguchi)
+- Internationalized tags page. !38589
+- Moves placeholders components into shared folder with documentation. Makes them easier to reuse in MR and Snippets comments.
+- Reorganize welcome page for new users.
+- Refactor GroupLinksController. (15121)
+- Remove filter icon from search bar.
+- Use title as placeholder instead of issue title for reusability.
+- Add Gitaly metrics to the performance bar.
+
+
+## 10.1.4 (2017-11-14)
+
+### Fixed (4 changes)
+
+- Don't try to create fork network memberships for forks with a missing source. !15366
+- Formats bytes to human reabale number in registry table.
+- Prevent error when authorizing an admin-created OAauth application without a set owner.
+- Prevents position update for image diff notes.
+
+
+## 10.1.3 (2017-11-10)
+
+- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
+- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137
+- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
+- [FIXED] Fix issues with forked projects of which the source was deleted. !15150
+- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228
+- [FIXED] Make sure group and project creation is blocked for new users that are external by default.
+- [FIXED] Fix arguments Import/Export error importing project merge requests.
+- [FIXED] Fix diff parser so it tolerates to diff special markers in the content.
+- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
+- [FIXED] Render 404 when polling commit notes without having permissions.
+- [FIXED] Show error message when fast-forward merge is not possible.
+- [FIXED] Avoid regenerating the ref path for the environment.
+- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests.
+
+## 10.1.2 (2017-11-08)
+
+- [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities.
+- [SECURITY] Properly translate IP addresses written in decimal, octal, or other formats in SSRF protections in project imports.
+- [FIXED] Fix TRIGGER checks for MySQL.
+
+## 10.1.1 (2017-10-31)
+
+- [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu)
+- [FIXED] Forbid the usage of `Redis#keys`. !14889
+- [FIXED] Make the circuitbreaker more robust by adding higher thresholds, and multiple access attempts. !14933
+- [FIXED] Only cache last push event for existing projects when pushing to a fork. !14989
+- [FIXED] Fix bug preventing secondary emails from being confirmed. !15010
+- [FIXED] Fix broken wiki pages that link to a wiki file. !15019
+- [FIXED] Don't rename paths that were freed up when upgrading. !15029
+- [FIXED] Fix bitbucket login. !15051
+- [FIXED] Update gitaly in Gitlab 10.1 to 0.43.1 for temp file cleanup. !15055
+- [FIXED] Use the correct visibility attribute for projects in system hooks. !15065
+- [FIXED] Normalize LDAP DN when looking up identity.
+- [FIXED] Adds callback functions for initial request in clusters page.
+- [FIXED] Fix missing Import/Export issue assignees.
+- [FIXED] Allow boards as top level route.
+- [FIXED] Fix widget of locked merge requests not being presented.
+- [FIXED] Fix editing issue description in mobile view.
+- [FIXED] Fix deletion of container registry or images returning an error.
+- [FIXED] Fix the writing of invalid environment refs.
+- [CHANGED] Store circuitbreaker settings in the database instead of config. !14842
+- [CHANGED] Update default disabled merge request widget message to reflect a general failure. !14960
+- [PERFORMANCE] Stop merge requests with thousands of commits from timing out. !15063
+
## 10.1.0 (2017-10-22)
- [SECURITY] Use a timeout on certain git operations. !14872
@@ -194,6 +437,24 @@ entry.
- creation of keys moved to services. !13331 (haseebeqx)
- Add username as GL_USERNAME in hooks.
+## 10.0.5 (2017-11-03)
+
+- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
+- [FIXED] Fix `rake gitlab:incoming_email:check` and make it report the actual error. !14423
+- [FIXED] Does not check if an invariant hashed storage path exists on disk when renaming projects. !14428
+- [FIXED] Fix bottom spacing for dropdowns that open upwards. !14535
+- [FIXED] Fix the project import with issues and milestones. !14657
+- [FIXED] Fix broken Y-axis scaling in some Prometheus graphs. !14693
+- [FIXED] Fixed duplicate notifications when added multiple labels on an issue. !14798
+- [FIXED] Don't rename paths that were freed up when upgrading. !15029
+- [FIXED] Fixed issue/merge request breadcrumb titles not having links.
+- [FIXED] Fix application setting to cache nil object.
+- [FIXED] Fix missing Import/Export issue assignees.
+- [FIXED] Allow boards as top level route.
+- [FIXED] Fixed milestone breadcrumb links.
+- [FIXED] Fixed merge request widget merged & closed date tooltip text.
+- [FIXED] fix merge request widget status icon for failed CI.
+
## 10.0.4 (2017-10-16)
- [SECURITY] Move project repositories between namespaces when renaming users.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9b2ee157193..4930b541ba2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,9 +1,13 @@
-## Contributor license agreement
+## Developer Certificate of Origin + License
-By submitting code as an individual you agree to the
-[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
-By submitting code as an entity you agree to the
-[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+By contributing to GitLab B.V., You accept and agree to the following terms and
+conditions for Your present and future Contributions submitted to GitLab B.V.
+Except for the license granted herein to GitLab B.V. and recipients of software
+distributed by GitLab B.V., You reserve all right, title, and interest in and to
+Your Contributions. All Contributions are subject to the following DCO + License
+terms.
+
+[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
_This notice should stay as the first item in the CONTRIBUTING.md file._
@@ -100,8 +104,7 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
-If you want to contribute to GitLab, but are not sure where to start,
-look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight].
+If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners.
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
@@ -540,6 +543,7 @@ When having your code reviewed and when reviewing merge requests please take the
etc.), they should conform to our [Licensing guidelines][license-finder-doc].
See the instructions in that document for help if your MR fails the
"license-finder" test with a "Dependencies that need approval" error.
+1. The merge request meets the [definition of done](#definition-of-done).
## Definition of done
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 564edf82ddf..316ba4bd9e6 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.50.0
+0.55.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 944880fa15e..bea438e9ade 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.2.0
+3.3.1
diff --git a/Gemfile b/Gemfile
index 8c9edf5c733..6034323956c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
-gem 'carrierwave', '~> 1.1'
+gem 'carrierwave', '~> 1.2'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@@ -263,6 +263,8 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
+gem 'batch-loader'
+
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
@@ -324,9 +326,9 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
- gem 'capybara', '~> 2.15.0'
+ gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'poltergeist', '~> 1.9.0'
+ gem 'selenium-webdriver', '~> 3.5'
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
@@ -343,7 +345,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false
- gem 'license_finder', '~> 2.1.0', require: false
+ gem 'license_finder', '~> 3.1', require: false
gem 'knapsack', '~> 1.11.0'
gem 'activerecord_sane_schema_dumper', '0.2'
@@ -398,7 +400,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.48.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.54.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 34f4e6af7e7..4787be92365 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -73,6 +73,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
+ batch-loader (1.1.1)
bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -83,6 +84,7 @@ GEM
bindata (2.4.1)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
+ blankslate (2.1.2.4)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
@@ -107,18 +109,19 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (1.1.0)
+ carrierwave (1.2.1)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.5)
+ childprocess (0.7.0)
+ ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
- cliver (0.3.2)
coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
@@ -273,7 +276,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.48.0)
+ gitaly-proto (0.54.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -291,7 +294,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
- gitlab-markup (1.6.2)
+ gitlab-markup (1.6.3)
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
@@ -353,10 +356,10 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.6.6)
+ grpc (1.7.2)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
- googleauth (~> 0.5.1)
+ googleauth (>= 0.5.1, < 0.7)
haml (4.0.7)
tilt
haml_lint (0.26.0)
@@ -450,11 +453,13 @@ GEM
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
- license_finder (2.1.0)
+ license_finder (3.1.1)
bundler
httparty
rubyzip
thor
+ toml (= 0.1.2)
+ with_env (> 1.0)
xml-simple
licensee (8.7.0)
rugged (~> 0.24)
@@ -570,6 +575,8 @@ GEM
activerecord (>= 4.0, < 5.2)
parser (2.4.0.0)
ast (~> 2.2)
+ parslet (1.5.0)
+ blankslate (~> 2.0)
path_expander (1.0.1)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
@@ -604,11 +611,6 @@ GEM
pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
- poltergeist (1.9.0)
- capybara (~> 2.1)
- cliver (~> 0.3.1)
- multi_json (~> 1.0)
- websocket-driver (>= 0.2.0)
posix-spawn (0.3.13)
powerpack (0.1.1)
premailer (1.10.4)
@@ -818,6 +820,9 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
+ selenium-webdriver (3.5.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.0)
sentry-raven (2.5.3)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
@@ -899,6 +904,8 @@ GEM
tilt (2.0.6)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
+ toml (0.1.2)
+ parslet (~> 1.5.0)
toml-rb (0.3.15)
citrus (~> 3.0, > 3.0)
truncato (0.7.10)
@@ -949,13 +956,11 @@ GEM
hashdiff
webpack-rails (0.9.10)
railties (>= 3.2.0)
- websocket-driver (0.6.3)
- websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.2)
wikicloth (0.8.1)
builder
expression_parser
rinku
+ with_env (1.1.0)
xml-simple (1.1.5)
xpath (2.1.0)
nokogiri (~> 1.3)
@@ -978,6 +983,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
+ batch-loader
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
@@ -988,9 +994,9 @@ DEPENDENCIES
browser (~> 2.2)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
- capybara (~> 2.15.0)
+ capybara (~> 2.15)
capybara-screenshot (~> 1.0.0)
- carrierwave (~> 1.1)
+ carrierwave (~> 1.2)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
@@ -1030,7 +1036,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.48.0)
+ gitaly-proto (~> 0.54.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1062,7 +1068,7 @@ DEPENDENCIES
knapsack (~> 1.11.0)
kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
- license_finder (~> 2.1.0)
+ license_finder (~> 3.1)
licensee (~> 8.7.0)
lograge (~> 0.5)
loofah (~> 2.0.3)
@@ -1104,7 +1110,6 @@ DEPENDENCIES
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
- poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta18)
pry-byebug (~> 3.4.1)
@@ -1150,6 +1155,7 @@ DEPENDENCIES
scss_lint (~> 0.54.0)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
+ selenium-webdriver (~> 3.5)
sentry-raven (~> 2.5.3)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
@@ -1189,4 +1195,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.15.4
+ 1.16.0
diff --git a/PROCESS.md b/PROCESS.md
index 06963243b25..7c8db689256 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -141,21 +141,29 @@ the stable branch are:
* Fixes for security issues
* New or updated translations (as long as they do not touch application code)
-During the feature freeze all merge requests that are meant to go into the upcoming
-release should have the correct milestone assigned _and_ have the label
-~"Pick into Stable" set, so that release managers can find and pick them.
-Merge requests without a milestone and this label will
-not be merged into any stable branches.
-
-Fixes marked like this will be shipped in the next RC for that release. Once
-the final RC has been prepared ready for release on the 22nd, further fixes
-marked ~"Pick into Stable" will go into a patch for that release.
-
-If a merge request is to be picked into more than one release it will also need
-the ~"Pick into Backports" label set to remind the release manager to change
-the milestone after cherry-picking. As before, it should still have the
-~"Pick into Stable" label and the milestone of the highest release it will be
-picked into.
+During the feature freeze all merge requests that are meant to go into the
+upcoming release should have the correct milestone assigned _and_ the
+`Pick into X.Y` label where `X.Y` is equal to the milestone, so that release
+managers can find and pick them.
+Merge requests without this label will not be picked into the stable release.
+
+For example, if the upcoming release is `10.2.0` you will need to set the
+`Pick into 10.2` label.
+
+Fixes marked like this will be shipped in the next RC (before the 22nd), or the
+next patch release.
+
+If a merge request is to be picked into more than one release it will need one
+`Pick into X.Y` label per release where the merge request should be back-ported
+to.
+
+For example, if the current patch release is `10.1.1` and a regression fix needs
+to be backported down to the `9.5` release, you will need to assign it the
+`10.1` milestone and the following labels:
+
+- `Pick into 10.1`
+- `Pick into 10.0`
+- `Pick into 9.5`
### Asking for an exception
diff --git a/VERSION b/VERSION
index 19eac09041d..73cdb768a24 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-10.2.0-pre
+10.3.0-pre
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 5dcd9c09b70..723c2c3f4c8 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/emoji/gay_pride_flag.png b/app/assets/images/emoji/gay_pride_flag.png
new file mode 100644
index 00000000000..1bec5f2ffd7
--- /dev/null
+++ b/app/assets/images/emoji/gay_pride_flag.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png
index 078f0657f95..9cf2458df1a 100644
--- a/app/assets/images/emoji/mrs_claus.png
+++ b/app/assets/images/emoji/mrs_claus.png
Binary files differ
diff --git a/app/assets/images/emoji/speech_left.png b/app/assets/images/emoji/speech_left.png
new file mode 100644
index 00000000000..00c05959bcd
--- /dev/null
+++ b/app/assets/images/emoji/speech_left.png
Binary files differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
index b0fa9e1139e..987279c13cc 100644
--- a/app/assets/images/emoji@2x.png
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index c0ed2ffdcb2..d8d173612d5 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":164,"spriteSize":72823,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","dashboard","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","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","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":173,"spriteSize":75815,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","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","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index b9829d0d450..c8f10628713 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-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 15V1a1 1 0 0 1 1-1h4.604c.93 0 1.762.088 2.495.264.733.176 1.353.445 1.863.807.509.363.897.82 1.164 1.369.268.549.401 1.197.401 1.945 0 .366-.045.718-.137 1.055-.091.337-.23.652-.417.945a3.453 3.453 0 0 1-.71.796 3.645 3.645 0 0 1-1.021.588c.469.117.87.295 1.203.533.333.238.608.515.824.83.216.315.374.657.473 1.027.099.37.148.75.148 1.138 0 1.553-.5 2.725-1.5 3.516-1 .791-2.423 1.187-4.27 1.187H3a1 1 0 0 1-1-1zm3.297-5.967v4.319H8.12c.425 0 .791-.053 1.099-.16.307-.106.564-.252.769-.44.205-.186.357-.406.456-.659.099-.252.148-.529.148-.83a3.04 3.04 0 0 0-.131-.928 1.78 1.78 0 0 0-.413-.703 1.8 1.8 0 0 0-.73-.445c-.3-.103-.66-.154-1.077-.154H5.297zm0-2.33h2.44c.842-.014 1.468-.192 1.878-.533.41-.34.616-.826.616-1.456 0-.725-.21-1.247-.632-1.566-.421-.318-1.086-.478-1.995-.478H5.297v4.033z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 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="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 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-additions" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 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="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="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.009 6.958a4 4 0 0 0 5.283 4.775 1 1 0 0 1 .712 1.87A6 6 0 0 1 2.077 6.44l-.741-.2a.5.5 0 0 1-.12-.915L3.41 4.058a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.711-1.87 6 6 0 0 1 7.927 7.162l.74.2a.5.5 0 0 1 .121.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="ehfirst-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="ehsecond-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="ehthird-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 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="talic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 0h7a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm2 2h3L8 14H5L8 2zM3 14h7a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 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="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="eofirst-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="eosecond-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="eothird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/epics.svg b/app/assets/images/illustrations/epics.svg
new file mode 100644
index 00000000000..1a37e6bba5f
--- /dev/null
+++ b/app/assets/images/illustrations/epics.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300" viewBox="0 0 430 300"><g fill="none" fill-rule="evenodd"><g transform="translate(75 53)"><rect width="284" height="208" y="5" fill="#F9F9F9" rx="10"/><rect width="284" height="208" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v188a6 6 0 0 0 6 6h264a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h264c5.523 0 10 4.477 10 10v188c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#EEE" fill-rule="nonzero" d="M25.168 153.995c3.837-.215 7.173.028 10.119.691a3 3 0 1 0 1.318-5.853c-3.509-.79-7.4-1.074-11.773-.828a3 3 0 1 0 .336 5.99zm19.043 4.66c2.401 1.704 4.388 3.61 7.569 7.083a3 3 0 0 0 4.424-4.054c-3.448-3.763-5.686-5.911-8.522-7.923a3 3 0 1 0-3.471 4.894zm15.575 15.173c3.181 2.675 6.52 4.665 10.397 6.039a3 3 0 0 0 2.004-5.655c-3.162-1.121-5.884-2.743-8.54-4.976a3 3 0 1 0-3.861 4.592zm22.133 8.148c1.02.037 2.067.045 3.143.023a72.664 72.664 0 0 0 8.346-.638 3 3 0 1 0-.812-5.945c-2.442.334-4.996.53-7.658.585a48.55 48.55 0 0 1-2.796-.021 3 3 0 0 0-.223 5.996zm22.778-3.286c3.9-1.37 7.427-3.15 10.54-5.305a3 3 0 0 0-3.415-4.933c-2.665 1.845-5.712 3.382-9.114 4.578a3 3 0 0 0 1.989 5.66zm19.156-13.62a33.752 33.752 0 0 0 5.276-10.817 3 3 0 1 0-5.773-1.633 27.753 27.753 0 0 1-4.341 8.9 3 3 0 1 0 4.838 3.55zm6.577-22.657c-.187-3.817-.926-7.71-2.204-11.596a3 3 0 0 0-5.7 1.874c1.113 3.384 1.75 6.745 1.91 10.016a3 3 0 1 0 5.994-.294zm-7.097-22.26c-1.897-3.2-4.152-6.325-6.748-9.344a3 3 0 0 0-4.55 3.913c2.372 2.756 4.421 5.597 6.136 8.49a3 3 0 0 0 5.162-3.06zm-11.546-17.793c-.938-3.025-1.402-6.42-1.365-9.976a3 3 0 0 0-6-.063c-.043 4.163.506 8.177 1.634 11.816a3 3 0 1 0 5.731-1.777zm.053-20.107c.905-3.341 2.22-6.538 3.904-9.448a3 3 0 0 0-5.194-3.004c-1.948 3.368-3.463 7.048-4.501 10.884a3 3 0 1 0 5.791 1.568zm10.134-17.305c2.475-2.28 5.265-4.09 8.335-5.374a3 3 0 1 0-2.314-5.536c-3.725 1.558-7.105 3.75-10.086 6.497a3 3 0 1 0 4.065 4.413zm18.177-7.586c3.202-.18 6.599.092 10.18.843a3 3 0 0 0 1.23-5.872c-4.086-.857-8.009-1.172-11.747-.962a3 3 0 1 0 .337 5.99zm20.047 3.95c3.068 1.268 6.232 2.842 9.487 4.728a3 3 0 0 0 3.009-5.191c-3.48-2.017-6.883-3.71-10.204-5.083a3 3 0 1 0-2.292 5.545zm19.578 9.955c3.711 1.586 7.376 2.77 10.997 3.565a3 3 0 0 0 1.286-5.86c-3.248-.713-6.555-1.782-9.925-3.222a3 3 0 1 0-2.358 5.517zm22.591 4.789c3.94-.04 7.808-.553 11.61-1.513a3 3 0 1 0-1.468-5.817 43.358 43.358 0 0 1-10.203 1.33 3 3 0 0 0 .061 6zm22.52-5.558c3.335-1.637 6.607-3.613 9.845-5.916a3 3 0 1 0-3.477-4.89c-2.984 2.122-5.98 3.931-9.011 5.42a3 3 0 1 0 2.643 5.386zm18.678-13.054a3 3 0 0 1-4.02-4.454 130.547 130.547 0 0 0 5.31-5.088 3 3 0 1 1 4.265 4.22 136.507 136.507 0 0 1-5.555 5.322zm-48.722 25.641a3 3 0 1 1 4.314-4.17c3.056 3.16 5.075 6.744 6.172 10.754a3 3 0 0 1-5.787 1.584c-.834-3.047-2.35-5.739-4.699-8.168zm5.347 18.049a3 3 0 1 1 5.978.52c-.282 3.232-.805 6.273-1.832 11.206a3 3 0 0 1-5.874-1.222c.981-4.717 1.473-7.572 1.728-10.504zm-3.777 21.555a3 3 0 0 1 5.953.747c-.5 3.988-.397 7.09.399 9.67a3 3 0 1 1-5.733 1.769c-1.087-3.52-1.217-7.426-.62-12.186zm7.393 22.444a3 3 0 0 1 4.461-4.013c2.703 3.005 5.224 5.296 7.594 6.947a3 3 0 0 1-3.429 4.924c-2.775-1.932-5.632-4.53-8.626-7.858zm20.352 12.28a3 3 0 1 1 .334-5.99c2.77.154 5.453-.554 9.224-2.254a3 3 0 0 1 2.466 5.47c-4.57 2.06-8.103 2.993-12.024 2.775zm21.784-7.058a3 3 0 0 1-1.815-5.719c4.227-1.342 8.24-1.61 12.496-.572a3 3 0 0 1-1.421 5.83c-3.116-.76-6.025-.566-9.26.46zM106.53 56.038a3 3 0 1 1-3.45 4.909c-1.074-.755-6.723-6.044-8.083-7.204a68.019 68.019 0 0 0-.332-.281 3 3 0 1 1 3.865-4.59l.362.306c1.643 1.402 6.971 6.391 7.638 6.86zM88.536 42.422a3 3 0 0 1-2.285 5.548c-3.14-1.293-5.78-1.34-8.105-.05a3 3 0 0 1-2.91-5.247c4.087-2.266 8.597-2.187 13.3-.25zM66.698 48.73a3 3 0 0 1 2.029 5.647c-4.432 1.592-8.786.835-13.166-1.88a3 3 0 1 1 3.16-5.1c2.93 1.816 5.425 2.25 7.977 1.333zm-15.636-8.038a3 3 0 0 1-4.352 4.13c-.911-.96-1.85-1.98-3.061-3.32-.295-.325-2.437-2.703-3.07-3.4-.47-.518-.9-.988-1.313-1.436a3 3 0 0 1 4.41-4.068c.425.46.866.942 1.346 1.47.642.709 2.79 3.092 3.076 3.41a180.865 180.865 0 0 0 2.964 3.214z"/><path fill="#E1DBF1" d="M254.66 72.196l2-3.464a2 2 0 1 0-3.464-2l-2 3.464-3.464-2a2 2 0 0 0-2 3.464l3.464 2-2 3.464a2 2 0 0 0 3.464 2l2-3.464 3.464 2a2 2 0 1 0 2-3.464l-3.464-2zm-151.904 78.732l2.829-2.828a2 2 0 0 0-2.829-2.829l-2.828 2.829-2.828-2.829a2 2 0 0 0-2.829 2.829l2.829 2.828-2.829 2.829a2 2 0 1 0 2.829 2.828l2.828-2.828 2.828 2.828a2 2 0 1 0 2.829-2.828l-2.829-2.829z"/><path fill="#6B4FBB" d="M210.66 173.66l3.464-2a2 2 0 1 0-2-3.464l-3.464 2-2-3.464a2 2 0 0 0-3.464 2l2 3.464-3.464 2a2 2 0 1 0 2 3.464l3.464-2 2 3.464a2 2 0 1 0 3.464-2l-2-3.464z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M27 181a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M138 85a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M200 57a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#FC6D26" fill-rule="nonzero" d="M222.647 121.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M103.647 28.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M85 103.488L81.841 108h6.318L85 103.488zm6.436 2.218A4 4 0 0 1 88.159 112H81.84a4 4 0 0 1-3.277-6.294l3.16-4.512a4 4 0 0 1 6.553 0l3.159 4.512z"/></g><path fill="#F9F9F9" d="M334.376 99.43A48.805 48.805 0 0 0 366 111c27.062 0 49-21.938 49-49s-21.938-49-49-49-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#FFF" d="M339.376 94.43A48.805 48.805 0 0 0 371 106c27.062 0 49-21.938 49-49S398.062 8 371 8s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M329.85 99.072a4.5 4.5 0 0 1-5.516-5.517l2.827-10.48C322.501 75.258 320 66.31 320 57c0-28.167 22.833-51 51-51s51 22.833 51 51-22.833 51-51 51c-11.859 0-23.096-4.064-32.102-11.37l-9.048 2.442zm10.817-6.169C349.091 100.027 359.737 104 371 104c25.957 0 47-21.043 47-47s-21.043-47-47-47-47 21.043-47 47c0 8.859 2.453 17.351 7.016 24.716l.456.737-3.277 12.144c.072.527.347.685.613.613l11.059-2.984.8.677z"/><g transform="translate(354 34)"><path fill="#E1DBF1" fill-rule="nonzero" d="M13 4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-8zm0-4h8a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5h-8a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M5 11a1 1 0 0 0 0 2h24a1 1 0 0 0 0-2H5zm0-4h24a5 5 0 0 1 0 10H5A5 5 0 0 1 5 7z"/><rect width="12" height="4" x="11" y="31" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="19" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="37" fill="#E1DBF1" rx="2"/><rect width="12" height="4" x="11" y="43" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="25" fill="#E1DBF1" rx="2"/></g><path fill="#F9F9F9" d="M344.238 225.072A38.83 38.83 0 0 1 368 217c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#FFF" d="M348.238 221.072A38.83 38.83 0 0 1 372 213c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#EEE" fill-rule="nonzero" d="M336.85 215.928a4.5 4.5 0 0 0-5.516 5.517l3.543 13.13A40.848 40.848 0 0 0 331 252c0 22.644 18.356 41 41 41s41-18.356 41-41-18.356-41-41-41a40.82 40.82 0 0 0-24.182 7.887l-10.968-2.96zm12.608 6.73A36.824 36.824 0 0 1 372 215c20.435 0 37 16.565 37 37s-16.565 37-37 37-37-16.565-37-37c0-5.747 1.31-11.304 3.795-16.343l.334-.677-3.934-14.577a.5.5 0 0 1 .613-.613l12.865 3.471.785-.604z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M356.097 255.962a7 7 0 0 0 8.81 10.88l1.093-.885v1.454a7 7 0 1 0 14 0v-1.454l1.092.885a7 7 0 1 0 8.81-10.88l-1.185-.96 1.455-.337a7 7 0 1 0-3.15-13.64l-1.4.323.623-1.278a7 7 0 0 0-12.583-6.137l-.662 1.356-.662-1.356a7 7 0 0 0-12.583 6.137l.623 1.278-1.4-.324a7 7 0 1 0-3.15 13.641l1.455.336-1.186.96zm5.464-.913a11.914 11.914 0 0 1-.444-1.95l-.19-1.362-4.2-.97a3 3 0 0 1 1.35-5.845l4.178.964.768-1.145c.373-.557.793-1.082 1.254-1.57l.95-1.006-1.877-3.849a3 3 0 0 1 5.393-2.63l1.892 3.879 1.363-.113a12.188 12.188 0 0 1 2.004 0l1.363.113 1.892-3.879a3 3 0 0 1 5.393 2.63l-1.877 3.849.95 1.006c.461.488.88 1.013 1.254 1.57l.768 1.145 4.178-.964a3 3 0 1 1 1.35 5.846l-4.2.97-.19 1.36a11.914 11.914 0 0 1-.444 1.95l-.413 1.302 3.36 2.72a3 3 0 1 1-3.776 4.663l-3.32-2.688-1.196.706a11.94 11.94 0 0 1-1.808.873l-1.286.492v4.295a3 3 0 1 1-6 0v-4.295l-1.286-.492a11.94 11.94 0 0 1-1.808-.873l-1.196-.706-3.32 2.688a3 3 0 1 1-3.776-4.663l3.36-2.72-.413-1.301z"/><path fill="#FC6D26" fill-rule="nonzero" d="M373 245.411a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/><g><path fill="#F9F9F9" d="M94.624 162.43A48.805 48.805 0 0 1 63 174c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#FFF" stroke="#EEE" stroke-width="4" d="M89.624 157.43A48.805 48.805 0 0 1 58 169c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M99.15 162.072a4.5 4.5 0 0 0 5.516-5.517l-2.827-10.48C106.499 138.258 109 129.31 109 120c0-28.167-22.833-51-51-51S7 91.833 7 120s22.833 51 51 51c11.859 0 23.096-4.064 32.102-11.37l9.048 2.442zm-10.817-6.169C79.909 163.027 69.263 167 58 167c-25.957 0-47-21.043-47-47s21.043-47 47-47 47 21.043 47 47c0 8.859-2.453 17.351-7.016 24.716l-.456.737 3.277 12.144c-.072.527-.347.685-.613.613l-11.059-2.984-.8.677z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M55.47 94.47l-16.148 6.688a4 4 0 0 0-2.164 2.164l-6.689 16.147a4 4 0 0 0 0 3.062l6.689 16.147a4 4 0 0 0 2.164 2.164l16.147 6.689a4 4 0 0 0 3.062 0l16.147-6.689a4 4 0 0 0 2.164-2.164l6.689-16.147a4 4 0 0 0 0-3.062l-6.689-16.147a4 4 0 0 0-2.164-2.164L58.53 94.469a4 4 0 0 0-3.062 0zM57 98.164l16.147 6.688L79.835 121l-6.688 16.147L57 143.835l-16.147-6.688L34.165 121l6.688-16.147L57 98.165zM57 107a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 20c6.075 0 11-4.925 11-11s-4.925-11-11-11-11 4.925-11 11 4.925 11 11 11zm0-4a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/><path fill="#FC6D26" d="M57 126.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-3a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></g></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/gitlab_logo.svg b/app/assets/images/illustrations/gitlab_logo.svg
new file mode 100644
index 00000000000..8dbd75a340e
--- /dev/null
+++ b/app/assets/images/illustrations/gitlab_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="492.509" height="453.68" viewBox="0 0 492.50943 453.67966"><g fill="none" fill-rule="evenodd"><path d="M491.589 259.398l-27.559-84.814L409.413 6.486c-2.81-8.648-15.045-8.648-17.856 0l-54.619 168.098H155.572L100.952 6.486c-2.81-8.648-15.046-8.648-17.856 0L28.478 174.584.921 259.398a18.775 18.775 0 0 0 6.82 20.992l238.513 173.29L484.77 280.39a18.777 18.777 0 0 0 6.82-20.992" fill="#fc6d26"/><path d="M246.255 453.68l90.684-279.096H155.57z" fill="#e24329"/><path d="M246.255 453.68L155.57 174.583H28.479z" fill="#fc6d26"/><path d="M28.479 174.584L.92 259.4a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29z" fill="#fca326"/><path d="M28.479 174.584H155.57L100.952 6.487c-2.81-8.65-15.047-8.65-17.856 0z" fill="#e24329"/><path d="M246.255 453.68l90.684-279.096H464.03z" fill="#fc6d26"/><path d="M464.03 174.584l27.56 84.815a18.773 18.773 0 0 1-6.822 20.99L246.255 453.68z" fill="#fca326"/><path d="M464.03 174.584H336.94L391.557 6.487c2.811-8.65 15.047-8.65 17.856 0z" fill="#e24329"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/pipelines_pending.svg b/app/assets/images/illustrations/pipelines_pending.svg
new file mode 100644
index 00000000000..25038366e92
--- /dev/null
+++ b/app/assets/images/illustrations/pipelines_pending.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="430" height="220" viewBox="0 0 430 220"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M189.8 182l2.4-12H114c-5.523 0-10-4.477-10-10V34c0-5.523 4.477-10 10-10h200c5.523 0 10 4.477 10 10v126c0 5.523-4.477 10-10 10h-78.2l2.4 12h22.52a9.651 9.651 0 0 1 9.28 7 5.491 5.491 0 0 1-5.28 7H164.159a5.787 5.787 0 0 1-5.659-7 8.855 8.855 0 0 1 8.659-7H189.8zM114 28a6 6 0 0 0-6 6v126a6 6 0 0 0 6 6h200a6 6 0 0 0 6-6V34a6 6 0 0 0-6-6H114zm5 6h190a5 5 0 0 1 5 5v116a5 5 0 0 1-5 5H119a5 5 0 0 1-5-5V39a5 5 0 0 1 5-5zm0 4a1 1 0 0 0-1 1v116a1 1 0 0 0 1 1h190a1 1 0 0 0 1-1V39a1 1 0 0 0-1-1H119zm112.72 132h-35.44l-2.4 12h40.24l-2.4-12zm-64.561 16c-2.29 0-4.268 1.6-4.748 3.838A1.787 1.787 0 0 0 164.16 192h100.56a1.491 1.491 0 0 0 1.435-1.901A5.651 5.651 0 0 0 260.72 186h-93.561z"/><path fill="#FEF0E8" d="M177.965 99H194a2 2 0 1 1 0 4h-16.322c-1.374 6.29-6.976 11-13.678 11-6.702 0-12.304-4.71-13.678-11h-3.365l-7.395 9.249a2 2 0 0 1-3.049.089L128.11 103h-5.844a2 2 0 1 1 0-4H129a2 2 0 0 1 1.487.662l7.423 8.248 6.523-8.159a2 2 0 0 1 1.562-.751h4.04c.513-7.265 6.57-13 13.965-13 7.396 0 13.452 5.735 13.965 13zM164 110c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10z"/><path fill="#EFEDF8" d="M273.847 103c-.962 6.23-6.347 11-12.847 11-6.5 0-11.885-4.77-12.847-11H232a2 2 0 0 1 0-4h16.153c.962-6.23 6.347-11 12.847-11 6.5 0 11.885 4.77 12.847 11h3.998l8.404-9.338a2 2 0 0 1 3.048.09L296.692 99H305a2 2 0 0 1 0 4h-9.27a2 2 0 0 1-1.562-.751l-6.523-8.16-7.423 8.249a2 2 0 0 1-1.487.662h-4.888zM261 110a9 9 0 1 0 0-18 9 9 0 0 0 0 18z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M213 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15z"/><path fill="#FC6D26" d="M211.586 101.828L208.757 99a2 2 0 1 0-2.828 2.828l4.243 4.243c.39.39.902.586 1.414.586.512 0 1.023-.195 1.414-.586L220.071 99a2 2 0 1 0-2.828-2.828l-5.657 5.656z"/><path fill="#FDC4A8" d="M162.95 101.07l-1.768-1.767a1.5 1.5 0 0 0-2.121 2.121l2.828 2.829c.293.293.677.439 1.06.439.385 0 .769-.146 1.062-.44l4.242-4.242a1.5 1.5 0 1 0-2.121-2.121l-3.182 3.182z"/><path fill="#6B4FBB" d="M256.39 104.841A6 6 0 1 0 261 95v6l-4.61 3.841z"/><path fill="#FEF0E8" fill-rule="nonzero" d="M99 99h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-14.384-.078l-3.643-3.425a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-11.657-10.96l-3.642-3.425a2 2 0 1 0-2.74 2.914l3.642 3.425a2 2 0 0 0 2.74-2.914zm-11.656-10.96l-3.643-3.425a2 2 0 0 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-14.367-3.885l-3.593 3.477a2 2 0 0 0 2.782 2.875l3.593-3.477a2 2 0 0 0-2.782-2.875zM19.44 84.244l-3.593 3.477a2 2 0 1 0 2.781 2.874l3.593-3.477a2 2 0 0 0-2.781-2.874zM7.94 95.371l-3.593 3.477a2 2 0 1 0 2.782 2.874l3.593-3.477a2 2 0 1 0-2.782-2.874z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M423.611 99.56l-3.598 3.472a2 2 0 0 0 2.777 2.879l3.599-3.472a2 2 0 0 0-2.778-2.878zm-11.514 11.11l-3.598 3.472a2 2 0 0 0 2.777 2.878l3.598-3.471a2 2 0 0 0-2.777-2.879zm-11.514 11.11l-3.599 3.471a2 2 0 1 0 2.778 2.879l3.598-3.472a2 2 0 1 0-2.777-2.879zm-8.799 4.48l-3.642-3.426a2 2 0 0 0-2.74 2.915l3.642 3.425a2 2 0 0 0 2.74-2.915zm-11.656-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.426a2 2 0 1 0 2.74-2.915zm-11.657-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zM353.001 99h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/slack_logo.svg b/app/assets/images/illustrations/slack_logo.svg
new file mode 100644
index 00000000000..b8d7906c2e1
--- /dev/null
+++ b/app/assets/images/illustrations/slack_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 121.94154 121.84154" width="121.942" height="121.842"><style id="style200">.st0{fill:#ecb32d}.st1{fill:#63c1a0}.st2{fill:#e01a59}.st3{fill:#331433}.st4{fill:#d62027}.st5{fill:#89d3df}.st6{fill:#258b74}.st7{fill:#819c3c}</style><path class="st0" d="M79.03 7.511c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path202" fill="#ecb32d"/><path class="st1" d="M35.53 21.611c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path204" fill="#63c1a0"/><path class="st2" d="M114.43 79.011c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.5 28.2c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.5-28.1 86.5-28.1z" id="path206" fill="#e01a59"/><path class="st3" d="M39.23 103.511c5.6-1.8 12.9-4.2 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path208" fill="#331433"/><path class="st4" d="M82.83 89.311c7.8-2.5 15.1-4.9 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path210" fill="#d62027"/><path class="st5" d="M100.23 35.511c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.4 28.1c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.4-28 86.4-28z" id="path212" fill="#89d3df"/><path class="st6" d="M25.13 59.911c5.6-1.8 12.9-4.2 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path214" fill="#258b74"/><path class="st7" d="M68.63 45.811c7.8-2.5 15.1-4.9 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path216" fill="#819c3c"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/wiki-fro-logged-out-users.svg b/app/assets/images/illustrations/wiki-fro-logged-out-users.svg
new file mode 100644
index 00000000000..c71841f72e5
--- /dev/null
+++ b/app/assets/images/illustrations/wiki-fro-logged-out-users.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
index 3de192d56eb..d2d3a257c0d 100644
--- a/app/assets/javascripts/abuse_reports.js
+++ b/app/assets/javascripts/abuse_reports.js
@@ -1,3 +1,5 @@
+import { truncate } from './lib/utils/text_utility';
+
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
}
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 242b3e2b990..d963101028a 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -16,6 +16,7 @@ const Api = {
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
+ createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index e00af4b2fa8..add43b81f6d 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,8 +1,8 @@
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
document.addEventListener('DOMContentLoaded', () => {
const autosizeEls = document.querySelectorAll('.js-autosize');
- autosize(autosizeEls);
- autosize.update(autosizeEls);
+ Autosize(autosizeEls);
+ Autosize.update(autosizeEls);
});
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js
index 93b0cbf4209..e7dc4ef8304 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/copy_as_gfm.js
@@ -1,7 +1,8 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
+
import _ from 'underscore';
-import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils';
-import { placeholderImage } from './lazy_loader';
+import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
+import { placeholderImage } from '../lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
@@ -284,7 +285,7 @@ const gfmRules = {
},
};
-class CopyAsGFM {
+export class CopyAsGFM {
constructor() {
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
@@ -469,7 +470,12 @@ class CopyAsGFM {
}
}
-window.gl = window.gl || {};
-window.gl.CopyAsGFM = CopyAsGFM;
+// Export CopyAsGFM as a global for rspec to access
+// see /spec/features/copy_as_gfm_spec.rb
+if (process.env.NODE_ENV !== 'production') {
+ window.CopyAsGFM = CopyAsGFM;
+}
-new CopyAsGFM();
+export default function initCopyAsGFM() {
+ return new CopyAsGFM();
+}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 44b2c974b9e..671532394a9 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,5 +1,6 @@
import './autosize';
import './bind_in_out';
+import initCopyAsGFM from './copy_as_gfm';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
@@ -7,3 +8,4 @@ import './requires_input';
import './toggler_behavior';
installGlEmojiElement();
+initCopyAsGFM();
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index b5500ac116f..6b06344f5ba 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,7 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
-/* global NewCommitForm */
-
+import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index ef4093b59e3..20d23162940 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,12 +1,13 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global BoardService */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
+import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
+import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
-import './services/board_service';
+import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
@@ -77,11 +78,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
- // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
+ eventHub.$on('newDetailIssue', this.updateDetailIssue);
+ eventHub.$on('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
+ eventHub.$off('newDetailIssue', this.updateDetailIssue);
+ eventHub.$off('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
@@ -112,6 +118,46 @@ $(() => {
methods: {
updateTokens() {
this.filterManager.updateTokens();
+ },
+ updateDetailIssue(newIssue) {
+ const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
+ newIssue.setFetchingState('subscriptions', true);
+ BoardService.getIssueInfo(sidebarInfoEndpoint)
+ .then(res => res.json())
+ .then((data) => {
+ newIssue.setFetchingState('subscriptions', false);
+ newIssue.updateData({
+ subscribed: data.subscribed,
+ });
+ })
+ .catch(() => {
+ newIssue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred while fetching sidebar data'));
+ });
+ }
+
+ Store.detail.issue = newIssue;
+ },
+ clearDetailIssue() {
+ Store.detail.issue = {};
+ },
+ toggleSubscription(id) {
+ const issue = Store.detail.issue;
+ if (issue.id === id && issue.toggleSubscriptionEndpoint) {
+ issue.setFetchingState('subscriptions', true);
+ BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
+ .then(() => {
+ issue.setFetchingState('subscriptions', false);
+ issue.updateData({
+ subscribed: !issue.subscribed,
+ });
+ })
+ .catch(() => {
+ issue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred when toggling the notification subscription'));
+ });
+ }
}
},
});
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.vue
index 079fb6438b9..0b220a56e0b 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,25 +1,11 @@
+<script>
import './issue_card_inner';
+import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardsIssueCard',
- template: `
- <li class="card"
- :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
- :index="index"
- :data-issue-id="issue.id"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)">
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :update-filters="true" />
- </li>
- `,
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
@@ -56,12 +42,30 @@ export default {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- Store.detail.issue = {};
+ eventHub.$emit('clearDetailIssue');
} else {
- Store.detail.issue = this.issue;
+ eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
}
}
},
},
};
+</script>
+
+<template>
+ <li class="card"
+ :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+ :index="index"
+ :data-issue-id="issue.id"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)">
+ <issue-card-inner
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 6159680f1e6..29aeb8e84aa 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,6 +1,6 @@
/* global Sortable */
import boardNewIssue from './board_new_issue';
-import boardCard from './board_card';
+import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 5d27518ac85..faa76da964f 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,16 +1,17 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global MilestoneSelect */
-/* global LabelsSelect */
/* global Sidebar */
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
-import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
-import Assignees from '../../sidebar/components/assignees/assignees';
+import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
+import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
+import LabelsSelect from '../../labels_select';
+import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
- gl.Subscription.bindAll('.subscription');
},
components: {
+ assigneeTitle,
+ assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
- 'assignee-title': AssigneeTitle,
- assignees: Assignees,
+ subscriptions,
},
});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index de9e44cef35..182957113a2 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -3,6 +3,7 @@
import Vue from 'vue';
import Flash from '../../../flash';
import './lists_dropdown';
+import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore;
@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() {
const count = ModalStore.selectedCount();
- return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
},
},
methods: {
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 407db176446..10f85c1d676 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -17,6 +17,11 @@ class ListIssue {
this.assignees = [];
this.selected = false;
this.position = obj.relative_position || Infinity;
+ this.isFetching = {
+ subscriptions: true,
+ };
+ this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@@ -73,6 +78,14 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
+ updateData(newData) {
+ Object.assign(this, newData);
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
update (url) {
const data = {
issue: {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 97e80afa3f8..fa7ddd25e1f 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -2,7 +2,7 @@
import Vue from 'vue';
-class BoardService {
+export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
+
+ static getIssueInfo(endpoint) {
+ return Vue.http.get(endpoint);
+ }
+
+ static toggleIssueSubscription(endpoint) {
+ return Vue.http.post(endpoint);
+ }
}
window.BoardService = BoardService;
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js
deleted file mode 100644
index 180aa30e98c..00000000000
--- a/app/assets/javascripts/clusters.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/* globals Flash */
-import Visibility from 'visibilityjs';
-import axios from 'axios';
-import Poll from './lib/utils/poll';
-import { s__ } from './locale';
-import initSettingsPanels from './settings_panels';
-import Flash from './flash';
-
-/**
- * Cluster page has 2 separate parts:
- * Toggle button
- *
- * - Polling status while creating or scheduled
- * -- Update status area with the response result
- */
-
-class ClusterService {
- constructor(options = {}) {
- this.options = options;
- }
- fetchData() {
- return axios.get(this.options.endpoint);
- }
-}
-
-export default class Clusters {
- constructor() {
- initSettingsPanels();
-
- const dataset = document.querySelector('.js-edit-cluster-form').dataset;
-
- this.state = {
- statusPath: dataset.statusPath,
- clusterStatus: dataset.clusterStatus,
- clusterStatusReason: dataset.clusterStatusReason,
- toggleStatus: dataset.toggleStatus,
- };
-
- this.service = new ClusterService({ endpoint: this.state.statusPath });
- this.toggleButton = document.querySelector('.js-toggle-cluster');
- this.toggleInput = document.querySelector('.js-toggle-input');
- this.errorContainer = document.querySelector('.js-cluster-error');
- this.successContainer = document.querySelector('.js-cluster-success');
- this.creatingContainer = document.querySelector('.js-cluster-creating');
- this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
-
- this.toggleButton.addEventListener('click', this.toggle.bind(this));
-
- if (this.state.clusterStatus !== 'created') {
- this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
- }
-
- if (this.state.statusPath) {
- this.initPolling();
- }
- }
-
- toggle() {
- this.toggleButton.classList.toggle('checked');
- this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
- }
-
- initPolling() {
- this.poll = new Poll({
- resource: this.service,
- method: 'fetchData',
- successCallback: (data) => {
- const { status, status_reason } = data.data;
- this.updateContainer(status, status_reason);
- },
- errorCallback: () => {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- },
- });
-
- if (!Visibility.hidden()) {
- this.poll.makeRequest();
- } else {
- this.service.fetchData();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- }
-
- hideAll() {
- this.errorContainer.classList.add('hidden');
- this.successContainer.classList.add('hidden');
- this.creatingContainer.classList.add('hidden');
- }
-
- updateContainer(status, error) {
- this.hideAll();
- switch (status) {
- case 'created':
- this.successContainer.classList.remove('hidden');
- break;
- case 'errored':
- this.errorContainer.classList.remove('hidden');
- this.errorReasonContainer.textContent = error;
- break;
- case 'scheduled':
- case 'creating':
- this.creatingContainer.classList.remove('hidden');
- break;
- default:
- this.hideAll();
- }
- }
-}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
new file mode 100644
index 00000000000..dc443475952
--- /dev/null
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -0,0 +1,221 @@
+import Visibility from 'visibilityjs';
+import Vue from 'vue';
+import { s__, sprintf } from '../locale';
+import Flash from '../flash';
+import Poll from '../lib/utils/poll';
+import initSettingsPanels from '../settings_panels';
+import eventHub from './event_hub';
+import {
+ APPLICATION_INSTALLED,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from './constants';
+import ClustersService from './services/clusters_service';
+import ClustersStore from './stores/clusters_store';
+import applications from './components/applications.vue';
+
+/**
+ * Cluster page has 2 separate parts:
+ * Toggle button and applications section
+ *
+ * - Polling status while creating or scheduled
+ * - Update status area with the response result
+ */
+
+export default class Clusters {
+ constructor() {
+ const {
+ statusPath,
+ installHelmPath,
+ installIngressPath,
+ installRunnerPath,
+ clusterStatus,
+ clusterStatusReason,
+ helpPath,
+ } = document.querySelector('.js-edit-cluster-form').dataset;
+
+ this.store = new ClustersStore();
+ this.store.setHelpPath(helpPath);
+ this.store.updateStatus(clusterStatus);
+ this.store.updateStatusReason(clusterStatusReason);
+ this.service = new ClustersService({
+ endpoint: statusPath,
+ installHelmEndpoint: installHelmPath,
+ installIngressEndpoint: installIngressPath,
+ installRunnerEndpoint: installRunnerPath,
+ });
+
+ this.toggle = this.toggle.bind(this);
+ this.installApplication = this.installApplication.bind(this);
+
+ this.toggleButton = document.querySelector('.js-toggle-cluster');
+ this.toggleInput = document.querySelector('.js-toggle-input');
+ this.errorContainer = document.querySelector('.js-cluster-error');
+ this.successContainer = document.querySelector('.js-cluster-success');
+ this.creatingContainer = document.querySelector('.js-cluster-creating');
+ this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
+ this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
+
+ initSettingsPanels();
+ this.initApplications();
+
+ if (this.store.state.status !== 'created') {
+ this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
+ }
+
+ this.addListeners();
+ if (statusPath) {
+ this.initPolling();
+ }
+ }
+
+ initApplications() {
+ const store = this.store;
+ const el = document.querySelector('#js-cluster-applications');
+
+ this.applications = new Vue({
+ el,
+ components: {
+ applications,
+ },
+ data() {
+ return {
+ state: store.state,
+ };
+ },
+ render(createElement) {
+ return createElement('applications', {
+ props: {
+ applications: this.state.applications,
+ helpPath: this.state.helpPath,
+ },
+ });
+ },
+ });
+ }
+
+ addListeners() {
+ this.toggleButton.addEventListener('click', this.toggle);
+ eventHub.$on('installApplication', this.installApplication);
+ }
+
+ removeListeners() {
+ this.toggleButton.removeEventListener('click', this.toggle);
+ eventHub.$off('installApplication', this.installApplication);
+ }
+
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: data => this.handleSuccess(data),
+ errorCallback: () => Clusters.handleError(),
+ });
+
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ } else {
+ this.service.fetchData()
+ .then(data => this.handleSuccess(data))
+ .catch(() => Clusters.handleError());
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden() && !this.destroyed) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ static handleError() {
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ }
+
+ handleSuccess(data) {
+ const prevStatus = this.store.state.status;
+ const prevApplicationMap = Object.assign({}, this.store.state.applications);
+
+ this.store.updateStateFromServer(data.data);
+
+ this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
+ this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
+ }
+
+ toggle() {
+ this.toggleButton.classList.toggle('checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ }
+
+ hideAll() {
+ this.errorContainer.classList.add('hidden');
+ this.successContainer.classList.add('hidden');
+ this.creatingContainer.classList.add('hidden');
+ }
+
+ checkForNewInstalls(prevApplicationMap, newApplicationMap) {
+ const appTitles = Object.keys(newApplicationMap)
+ .filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED &&
+ prevApplicationMap[appId].status !== APPLICATION_INSTALLED &&
+ prevApplicationMap[appId].status !== null)
+ .map(appId => newApplicationMap[appId].title);
+
+ if (appTitles.length > 0) {
+ const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
+ appList: appTitles.join(', '),
+ });
+ Flash(text, 'notice', this.successApplicationContainer);
+ }
+ }
+
+ updateContainer(prevStatus, status, error) {
+ this.hideAll();
+
+ // We poll all the time but only want the `created` banner to show when newly created
+ if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) {
+ switch (status) {
+ case 'created':
+ this.successContainer.classList.remove('hidden');
+ break;
+ case 'errored':
+ this.errorContainer.classList.remove('hidden');
+ this.errorReasonContainer.textContent = error;
+ break;
+ case 'scheduled':
+ case 'creating':
+ this.creatingContainer.classList.remove('hidden');
+ break;
+ default:
+ this.hideAll();
+ }
+ }
+ }
+
+ installApplication(appId) {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
+ this.store.updateAppProperty(appId, 'requestReason', null);
+
+ this.service.installApplication(appId)
+ .then(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
+ })
+ .catch(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed'));
+ });
+ }
+
+ destroy() {
+ this.destroyed = true;
+
+ this.removeListeners();
+
+ if (this.poll) {
+ this.poll.stop();
+ }
+
+ this.applications.$destroy();
+ }
+}
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
new file mode 100644
index 00000000000..872abf03ef1
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -0,0 +1,185 @@
+<script>
+import { s__, sprintf } from '../../locale';
+import eventHub from '../event_hub';
+import loadingButton from '../../vue_shared/components/loading_button.vue';
+import {
+ APPLICATION_NOT_INSTALLABLE,
+ APPLICATION_SCHEDULED,
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_INSTALLED,
+ APPLICATION_ERROR,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from '../constants';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ titleLink: {
+ type: String,
+ required: false,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ status: {
+ type: String,
+ required: false,
+ },
+ statusReason: {
+ type: String,
+ required: false,
+ },
+ requestStatus: {
+ type: String,
+ required: false,
+ },
+ requestReason: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ loadingButton,
+ },
+ computed: {
+ rowJsClass() {
+ return `js-cluster-application-row-${this.id}`;
+ },
+ installButtonLoading() {
+ return !this.status ||
+ this.status === APPLICATION_SCHEDULED ||
+ this.status === APPLICATION_INSTALLING ||
+ this.requestStatus === REQUEST_LOADING;
+ },
+ installButtonDisabled() {
+ // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
+ // we already made a request to install and are just waiting for the real-time
+ // to sync up.
+ return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) ||
+ this.requestStatus === REQUEST_LOADING ||
+ this.requestStatus === REQUEST_SUCCESS;
+ },
+ installButtonLabel() {
+ let label;
+ if (
+ this.status === APPLICATION_NOT_INSTALLABLE ||
+ this.status === APPLICATION_INSTALLABLE ||
+ this.status === APPLICATION_ERROR
+ ) {
+ label = s__('ClusterIntegration|Install');
+ } else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) {
+ label = s__('ClusterIntegration|Installing');
+ } else if (this.status === APPLICATION_INSTALLED) {
+ label = s__('ClusterIntegration|Installed');
+ }
+
+ return label;
+ },
+ hasError() {
+ return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE;
+ },
+ generalErrorDescription() {
+ return sprintf(
+ s__('ClusterIntegration|Something went wrong while installing %{title}'), {
+ title: this.title,
+ },
+ );
+ },
+ },
+ methods: {
+ installClicked() {
+ eventHub.$emit('installApplication', this.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-responsive-table-row gl-responsive-table-row-col-span"
+ :class="rowJsClass"
+ >
+ <div
+ class="gl-responsive-table-row-layout"
+ role="row"
+ >
+ <a
+ v-if="titleLink"
+ :href="titleLink"
+ target="blank"
+ rel="noopener noreferrer"
+ role="gridcell"
+ class="table-section section-15 section-align-top js-cluster-application-title"
+ >
+ {{ title }}
+ </a>
+ <span
+ v-else
+ class="table-section section-15 section-align-top js-cluster-application-title"
+ >
+ {{ title }}
+ </span>
+ <div
+ class="table-section section-wrap"
+ role="gridcell"
+ >
+ <div v-html="description"></div>
+ </div>
+ <div
+ class="table-section table-button-footer section-15 section-align-top"
+ role="gridcell"
+ >
+ <div class="btn-group table-action-buttons">
+ <loading-button
+ class="js-cluster-application-install-button"
+ :loading="installButtonLoading"
+ :disabled="installButtonDisabled"
+ :label="installButtonLabel"
+ @click="installClicked"
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ v-if="hasError"
+ class="gl-responsive-table-row-layout"
+ role="row"
+ >
+ <div
+ class="alert alert-danger alert-block append-bottom-0 table-section section-100"
+ role="gridcell"
+ >
+ <div>
+ <p class="js-cluster-application-general-error-message">
+ {{ generalErrorDescription }}
+ </p>
+ <ul v-if="statusReason || requestReason">
+ <li
+ v-if="statusReason"
+ class="js-cluster-application-status-error-message"
+ >
+ {{ statusReason }}
+ </li>
+ <li
+ v-if="requestReason"
+ class="js-cluster-application-request-error-message"
+ >
+ {{ requestReason }}
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
new file mode 100644
index 00000000000..e5ae439d26e
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -0,0 +1,114 @@
+<script>
+import _ from 'underscore';
+import { s__, sprintf } from '../../locale';
+import applicationRow from './application_row.vue';
+
+export default {
+ props: {
+ applications: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ applicationRow,
+ },
+ computed: {
+ generalApplicationDescription() {
+ return sprintf(
+ _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), {
+ helpLink: `<a href="${this.helpPath}">
+ ${_.escape(s__('ClusterIntegration|installing applications'))}
+ </a>`,
+ },
+ false,
+ );
+ },
+ helmTillerDescription() {
+ return _.escape(s__(
+ `ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
+ Tiller runs inside of your Kubernetes Cluster, and manages
+ releases of your charts.`,
+ ));
+ },
+ ingressDescription() {
+ const descriptionParagraph = _.escape(s__(
+ `ClusterIntegration|Ingress gives you a way to route requests to services based on the
+ request host or path, centralizing a number of services into a single entrypoint.`,
+ ));
+
+ const extraCostParagraph = sprintf(
+ _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), {
+ boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
+ pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+ ${_.escape(s__('ClusterIntegration|GKE pricing'))}
+ </a>`,
+ },
+ false,
+ );
+
+ return `
+ <p>
+ ${descriptionParagraph}
+ </p>
+ <p class="append-bottom-0">
+ ${extraCostParagraph}
+ </p>
+ `;
+ },
+ gitlabRunnerDescription() {
+ return _.escape(s__(
+ `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
+ and send the results back to GitLab.`,
+ ));
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings no-animate expanded">
+ <div class="settings-header">
+ <h4>
+ {{ s__('ClusterIntegration|Applications') }}
+ </h4>
+ <p
+ class="append-bottom-0"
+ v-html="generalApplicationDescription"
+ >
+ </p>
+ </div>
+
+ <div class="settings-content">
+ <div class="append-bottom-20">
+ <application-row
+ id="helm"
+ :title="applications.helm.title"
+ title-link="https://docs.helm.sh/"
+ :description="helmTillerDescription"
+ :status="applications.helm.status"
+ :status-reason="applications.helm.statusReason"
+ :request-status="applications.helm.requestStatus"
+ :request-reason="applications.helm.requestReason"
+ />
+ <application-row
+ id="ingress"
+ :title="applications.ingress.title"
+ title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
+ :description="ingressDescription"
+ :status="applications.ingress.status"
+ :status-reason="applications.ingress.statusReason"
+ :request-status="applications.ingress.requestStatus"
+ :request-reason="applications.ingress.requestReason"
+ />
+ <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
+ <!-- Add GitLab Runner row, all other plumbing is complete -->
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
new file mode 100644
index 00000000000..93223aefff8
--- /dev/null
+++ b/app/assets/javascripts/clusters/constants.js
@@ -0,0 +1,12 @@
+// These need to match what is returned from the server
+export const APPLICATION_NOT_INSTALLABLE = 'not_installable';
+export const APPLICATION_INSTALLABLE = 'installable';
+export const APPLICATION_SCHEDULED = 'scheduled';
+export const APPLICATION_INSTALLING = 'installing';
+export const APPLICATION_INSTALLED = 'installed';
+export const APPLICATION_ERROR = 'errored';
+
+// These are only used client-side
+export const REQUEST_LOADING = 'request-loading';
+export const REQUEST_SUCCESS = 'request-success';
+export const REQUEST_FAILURE = 'request-failure';
diff --git a/app/assets/javascripts/repo/event_hub.js b/app/assets/javascripts/clusters/event_hub.js
index 0948c2e5352..0948c2e5352 100644
--- a/app/assets/javascripts/repo/event_hub.js
+++ b/app/assets/javascripts/clusters/event_hub.js
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
new file mode 100644
index 00000000000..ce14c9a9945
--- /dev/null
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -0,0 +1,20 @@
+import axios from '../../lib/utils/axios_utils';
+
+export default class ClusterService {
+ constructor(options = {}) {
+ this.options = options;
+ this.appInstallEndpointMap = {
+ helm: this.options.installHelmEndpoint,
+ ingress: this.options.installIngressEndpoint,
+ runner: this.options.installRunnerEndpoint,
+ };
+ }
+
+ fetchData() {
+ return axios.get(this.options.endpoint);
+ }
+
+ installApplication(appId) {
+ return axios.post(this.appInstallEndpointMap[appId]);
+ }
+}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
new file mode 100644
index 00000000000..e731cdc3042
--- /dev/null
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -0,0 +1,68 @@
+import { s__ } from '../../locale';
+
+export default class ClusterStore {
+ constructor() {
+ this.state = {
+ helpPath: null,
+ status: null,
+ statusReason: null,
+ applications: {
+ helm: {
+ title: s__('ClusterIntegration|Helm Tiller'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ ingress: {
+ title: s__('ClusterIntegration|Ingress'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ runner: {
+ title: s__('ClusterIntegration|GitLab Runner'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ },
+ };
+ }
+
+ setHelpPath(helpPath) {
+ this.state.helpPath = helpPath;
+ }
+
+ updateStatus(status) {
+ this.state.status = status;
+ }
+
+ updateStatusReason(reason) {
+ this.state.statusReason = reason;
+ }
+
+ updateAppProperty(appId, prop, value) {
+ this.state.applications[appId][prop] = value;
+ }
+
+ updateStateFromServer(serverState = {}) {
+ this.state.status = serverState.status;
+ this.state.statusReason = serverState.status_reason;
+ serverState.applications.forEach((serverAppEntry) => {
+ const {
+ name: appId,
+ status,
+ status_reason: statusReason,
+ } = serverAppEntry;
+
+ this.state.applications[appId] = {
+ ...(this.state.applications[appId] || {}),
+ status,
+ statusReason,
+ };
+ });
+ }
+}
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index ae6b8902032..9b952ea7b60 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -3,6 +3,8 @@
prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */
+import { pluralize } from './lib/utils/text_utility';
+
export default (function () {
const CommitsList = {};
@@ -86,7 +88,7 @@ export default (function () {
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
- $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`);
+ $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
gl.utils.localTimeAgo($processedData.find('.js-timeago'));
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 3bed0678350..9a4c9bfcc80 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, prefer-arrow-callback */
import Api from './api';
+import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) {
@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message;
} else {
errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
+ `${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>');
}
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 8bf9ae17de0..a8cd8c20f8f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import { __ } from '../locale';
-import '../lib/utils/text_utility';
+import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
@@ -36,7 +36,7 @@ export default {
});
newData.stages.forEach((item) => {
- const stageSlug = gl.text.dasherize(item.name.toLowerCase());
+ const stageSlug = dasherize(item.name.toLowerCase());
item.active = false;
item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index b516fda84b9..b4307761c6b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,11 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
-/* global ProjectSelect */
+import { s__ } from './locale';
+import projectSelect from './project_select';
import IssuableIndex from './issuable_index';
-/* global Milestone */
+import Milestone from './milestone';
import IssuableForm from './issuable_form';
-/* global LabelsSelect */
+import LabelsSelect from './labels_select';
/* global MilestoneSelect */
-/* global NewBranchForm */
+import NewBranchForm from './new_branch_form';
/* global NotificationsForm */
/* global NotificationsDropdown */
import groupAvatar from './group_avatar';
@@ -16,22 +17,21 @@ import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
-/* global NamespaceSelects */
-/* global NewCommitForm */
-/* global NewBranchForm */
-/* global Project */
-/* global ProjectAvatar */
+import NamespaceSelect from './namespace_select';
+import NewCommitForm from './new_commit_form';
+import Project from './project';
+import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global ProjectFindFile */
-/* global ProjectNew */
-/* global ProjectShow */
-/* global ProjectImport */
+import ProjectNew from './project_new';
+import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
/* global Sidebar */
+import Flash from './flash';
import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
@@ -89,6 +89,8 @@ import Members from './members';
import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
import Diff from './diff';
+import ProjectLabelSubscription from './project_label_subscription';
+import ProjectVariables from './project_variables';
(function() {
var Dispatcher;
@@ -185,7 +187,7 @@ import Diff from './diff';
initIssuableSidebar();
break;
case 'dashboard:milestones:index':
- new ProjectSelect();
+ projectSelect();
break;
case 'projects:milestones:show':
case 'groups:milestones:show':
@@ -195,7 +197,7 @@ import Diff from './diff';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
- new ProjectSelect();
+ projectSelect();
initLegacyFilters();
break;
case 'groups:issues':
@@ -204,7 +206,7 @@ import Diff from './diff';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
- new ProjectSelect();
+ projectSelect();
break;
case 'dashboard:todos:index':
new Todos();
@@ -337,7 +339,8 @@ import Diff from './diff';
container: '.js-commit-pipeline-graph',
}).bindEvents();
initNotes();
- initChangesDropdown();
+ const stickyBarPaddingTop = 16;
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break;
case 'projects:commit:pipelines':
@@ -376,7 +379,7 @@ import Diff from './diff';
initSettingsPanels();
break;
case 'projects:imports:show':
- new ProjectImport();
+ projectImport();
break;
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
@@ -482,7 +485,7 @@ import Diff from './diff';
if ($el.find('.dropdown-group-label').length) {
new GroupLabelSubscription($el);
} else {
- new gl.ProjectLabelSubscription($el);
+ new ProjectLabelSubscription($el);
}
});
break;
@@ -518,7 +521,7 @@ import Diff from './diff';
// Initialize expandable settings panels
initSettingsPanels();
case 'groups:settings:ci_cd:show':
- new gl.ProjectVariables();
+ new ProjectVariables();
break;
case 'ci:lints:create':
case 'ci:lints:show':
@@ -543,9 +546,12 @@ import Diff from './diff';
new DueDateSelectors();
break;
case 'projects:clusters:show':
- import(/* webpackChunkName: "clusters" */ './clusters')
+ import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
- .catch(() => {});
+ .catch((err) => {
+ Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
+ throw err;
+ });
break;
}
switch (path[0]) {
@@ -575,7 +581,8 @@ import Diff from './diff';
new UsersSelect();
break;
case 'projects':
- new NamespaceSelects();
+ document.querySelectorAll('.js-namespace-select')
+ .forEach(dropdown => new NamespaceSelect({ dropdown }));
break;
case 'labels':
switch (path[2]) {
@@ -598,7 +605,7 @@ import Diff from './diff';
break;
case 'projects':
new Project();
- new ProjectAvatar();
+ projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
@@ -617,7 +624,6 @@ import Diff from './diff';
case 'show':
new Star();
new ProjectNew();
- new ProjectShow();
new NotificationsDropdown();
break;
case 'wikis':
diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js
index d6a1aadd49c..404d707cf7a 100644
--- a/app/assets/javascripts/droplab/plugins/filter.js
+++ b/app/assets/javascripts/droplab/plugins/filter.js
@@ -79,8 +79,6 @@ const Filter = {
this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
-
- this.debounceKeydown({ detail: { hook: this.hook } });
},
destroy: function destroy() {
diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js
index 4da7344604e..bfe056a0fcc 100644
--- a/app/assets/javascripts/droplab/utils.js
+++ b/app/assets/javascripts/droplab/utils.js
@@ -30,7 +30,7 @@ const utils = {
},
isDropDownParts(target) {
- if (!target || target.tagName === 'HTML') return false;
+ if (!target || !target.hasAttribute || target.tagName === 'HTML') return false;
return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
},
};
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 7a17adcd44e..b7747ee3f83 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -119,11 +119,9 @@ export default function dropzoneInput(form) {
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
- const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
-
e.preventDefault();
e.stopPropagation();
- Dropzone.forElement(target).removeAllFiles(true);
+ Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
});
// If 'error' event is fired, we store a failed files,
diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
index 3fd23efa9f8..e9defb62cf8 100644
--- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
@@ -7,6 +7,17 @@ function isFlagEmoji(emojiUnicode) {
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
+// Tested on mac OS 10.12.6 and Windows 10 FCU, it renders as two separate characters
+const baseFlagCodePoint = 127987; // parseInt('1F3F3', 16)
+const rainbowCodePoint = 127752; // parseInt('1F308', 16)
+function isRainbowFlagEmoji(emojiUnicode) {
+ const characters = Array.from(emojiUnicode);
+ // Length 4 because flags are made of 2 characters which are surrogate pairs
+ return emojiUnicode.length === 4 &&
+ characters[0].codePointAt(0) === baseFlagCodePoint &&
+ characters[1].codePointAt(0) === rainbowCodePoint;
+}
+
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
@@ -57,9 +68,11 @@ function isPersonZwjEmoji(emojiUnicode) {
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
+ const isRainbowFlagResult = isRainbowFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
- !isFlagResult
+ (unicodeSupportMap.rainbowFlag && isRainbowFlagResult) ||
+ (!isFlagResult && !isRainbowFlagResult)
);
}
@@ -113,6 +126,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
export {
isEmojiUnicodeSupported as default,
isFlagEmoji,
+ isRainbowFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index 755381c2f95..c18d07dad43 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -1,5 +1,7 @@
import AccessorUtilities from '../../lib/utils/accessor';
+const GL_EMOJI_VERSION = '0.2.0';
+
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
@@ -13,6 +15,7 @@ const unicodeSupportTestMap = {
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
+ rainbowFlag: '\u{1F3F3}\u{1F308}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
@@ -141,23 +144,31 @@ function generateUnicodeSupportMap(testMap) {
}
export default function getUnicodeSupportMap() {
- let unicodeSupportMap;
- let userAgentFromCache;
-
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ let glEmojiVersionFromCache;
+ let userAgentFromCache;
+ if (isLocalStorageAvailable) {
+ glEmojiVersionFromCache = window.localStorage.getItem('gl-emoji-version');
+ userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ }
+ let unicodeSupportMap;
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
- if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
+ if (
+ !unicodeSupportMap ||
+ glEmojiVersionFromCache !== GL_EMOJI_VERSION ||
+ userAgentFromCache !== navigator.userAgent
+ ) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) {
+ window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index c039ae85cfb..ffb7757bed8 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -227,25 +227,27 @@ export default {
/>
<div
- class="blank-state blank-state-no-icon"
+ class="blank-state-row"
v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </h2>
- <p class="blank-state-text">
- Environments are places where code gets deployed, such as staging or production.
- <br />
- <a :href="helpPagePath">
- Read more about environments
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
+ </a>
+ </p>
+
+ <a
+ v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create js-new-environment-button">
+ New environment
</a>
- </p>
-
- <a
- v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New environment
- </a>
+ </div>
</div>
<div
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 6de01fa53d0..9d25f806c0d 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -2,7 +2,7 @@
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import '../../lib/utils/text_utility';
+import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@@ -139,7 +139,7 @@ export default {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
- name: gl.text.humanize(action.name),
+ name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
@@ -421,7 +421,11 @@ export default {
</script>
<template>
<div
- :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }"
+ class="gl-responsive-table-row"
+ :class="{
+ 'js-child-row environment-child-row': model.isChildren,
+ 'folder-row': model.isFolder,
+ }"
role="row">
<div class="table-section section-10" role="gridcell">
<div
@@ -495,15 +499,16 @@ export default {
</a>
</div>
- <div class="table-section section-25" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-section section-25" role="gridcell">
<div
- v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Commit
</div>
<div
- v-if="!model.isFolder && hasLastDeploymentKey"
+ v-if="hasLastDeploymentKey"
class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
@@ -514,21 +519,22 @@ export default {
:author="commitAuthor"/>
</div>
<div
- v-if="!model.isFolder && !hasLastDeploymentKey"
+ v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content">
No deployments yet
</div>
</div>
- <div class="table-section section-10" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-section section-10" role="gridcell">
<div
- v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Updated
</div>
<span
- v-if="!model.isFolder && canShowDate"
+ v-if="canShowDate"
class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 5c624b79d45..a642464c920 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -338,7 +338,8 @@ class GfmAutoComplete {
let resultantValue = value;
if (value && !this.setting.skipSpecialCharacterTest) {
const withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) {
+ const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+ if (withoutAt && regex.test(withoutAt)) {
resultantValue = `${value.charAt()}"${withoutAt}"`;
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index e8d8fef8579..4e7a6e54f90 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import _ from 'underscore';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
@@ -330,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
- return $(selector);
+ return $(selector, this.instance.dropdown);
};
})(this),
data: (function(_this) {
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 48cd43d3348..d0f9e6af0f8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -2,6 +2,7 @@
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
+import textUtils from './lib/utils/text_markdown';
export default class GLForm {
constructor(form, enableGFM = false) {
@@ -46,7 +47,7 @@ export default class GLForm {
}
// form and textarea event listeners
this.addEventListeners();
- gl.text.init(this.form);
+ textUtils.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
@@ -85,7 +86,7 @@ export default class GLForm {
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
- gl.text.removeListeners(this.form);
+ textUtils.removeListeners(this.form);
}
addEventListeners() {
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index cdc4fcf6573..e7232ca3712 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -4,6 +4,7 @@ import _ from 'underscore';
import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
+import { n__ } from '../locale';
export default (function() {
function ContributorsStatGraph() {}
@@ -44,7 +45,7 @@ export default (function() {
commits = $('<span/>', {
"class": 'graph-author-commits-count'
});
- commits.text(author.commits + " commits");
+ commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits);
};
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index ea2e2205077..33a352e158a 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility';
* @param {jQuery.Event} e
* @param {String} count
*/
-$(document).on('todo:toggle', (e, count) => {
- const parsedCount = parseInt(count, 10);
- const $todoPendingCount = $('.todos-count');
+export default function initTodoToggle() {
+ $(document).on('todo:toggle', (e, count) => {
+ const parsedCount = parseInt(count, 10);
+ const $todoPendingCount = $('.todos-count');
- $todoPendingCount.text(highCountTrim(parsedCount));
- $todoPendingCount.toggleClass('hidden', parsedCount === 0);
-});
+ $todoPendingCount.text(highCountTrim(parsedCount));
+ $todoPendingCount.toggleClass('hidden', parsedCount === 0);
+ });
+}
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 6f476f96f72..ada693afc46 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,6 +1,6 @@
/* eslint-disable no-new */
/* global MilestoneSelect */
-/* global LabelsSelect */
+import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
/* global Sidebar */
@@ -14,7 +14,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
- gl.Subscription.bindAll('.subscription');
new DueDateSelectors();
window.sidebar = new Sidebar();
};
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
index 1211c2c802c..2cbb70220d0 100644
--- a/app/assets/javascripts/init_legacy_filters.js
+++ b/app/assets/javascripts/init_legacy_filters.js
@@ -1,15 +1,14 @@
/* eslint-disable no-new */
-/* global LabelsSelect */
+import LabelsSelect from './labels_select';
/* global MilestoneSelect */
-/* global IssueStatusSelect */
-/* global SubscriptionSelect */
-
+import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
+import issueStatusSelect from './issue_status_select';
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
+ issueStatusSelect();
+ subscriptionSelect();
};
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index bb509089b1d..ba2b6737988 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,14 +1,11 @@
/* eslint-disable class-methods-use-this, no-new */
-/* global LabelsSelect */
/* global MilestoneSelect */
-/* global IssueStatusSelect */
-/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
-import './issue_status_select';
-import './subscription_select';
-import './labels_select';
+import issueStatusSelect from './issue_status_select';
+import subscriptionSelect from './subscription_select';
+import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -49,8 +46,8 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
+ issueStatusSelect();
+ subscriptionSelect();
}
setupBulkUpdateActions() {
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 5bc7f8d9cb9..da99394ff90 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -2,11 +2,8 @@ import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select';
-const PARTICIPANTS_ROW_COUNT = 7;
-
export default class IssuableContext {
constructor(currentUser) {
- this.initParticipants();
this.userSelect = new UsersSelect(currentUser);
$('select.select2').select2({
@@ -51,29 +48,4 @@ export default class IssuableContext {
}
});
}
-
- initParticipants() {
- $(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
- return $('.js-participants-author').each(function forEachAuthor(i) {
- if (i >= PARTICIPANTS_ROW_COUNT) {
- $(this).addClass('js-participants-hidden').hide();
- }
- });
- }
-
- toggleHiddenParticipants() {
- const currentText = $(this).text().trim();
- const lessText = $(this).data('less-text');
- const originalText = $(this).data('original-text');
-
- if (currentText === originalText) {
- $(this).text(lessText);
-
- if (gl.lazyLoader) gl.lazyLoader.loadCheck();
- } else {
- $(this).text(originalText);
- }
-
- $('.js-participants-hidden').toggle();
- }
}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 3fc29f9a661..7de07e9403d 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,12 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages';
-import '~/lib/utils/text_utility';
+import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
-class Issue {
+export default class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new TaskList({
@@ -73,7 +73,7 @@ class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
- projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
+ projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
@@ -147,5 +147,3 @@ class Issue {
});
}
}
-
-export default Issue;
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index d1aa83ea57f..4e39d483b31 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -29,6 +29,11 @@ export default {
required: false,
default: false,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
issuableRef: {
type: String,
required: true,
@@ -92,6 +97,16 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
const store = new Store({
@@ -157,21 +172,21 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
- window.Flash('Error updating issue');
+ window.Flash(`Error updating ${this.issuableType}`);
});
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.then((data) => {
- // Stop the poll so we don't get 404's with the issue not existing
+ // Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
gl.utils.visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
- window.Flash('Error deleting issue');
+ window.Flash(`Error deleting ${this.issuableType}`);
});
},
},
@@ -223,6 +238,8 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
+ :show-delete-button="showDeleteButton"
+ :can-attach-file="canAttachFile"
/>
<div v-else>
<title-component
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 8c81575fe6f..a539506bce2 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -13,6 +13,11 @@
type: Object,
required: true,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -23,6 +28,9 @@
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
+ shouldShowDeleteButton() {
+ return this.canDestroy && this.showDeleteButton;
+ },
},
methods: {
closeForm() {
@@ -62,7 +70,7 @@
Cancel
</button>
<button
- v-if="canDestroy"
+ v-if="shouldShowDeleteButton"
class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 0aa1b2c2e31..4d2ef409bad 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -17,6 +17,11 @@
type: String,
required: true,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
markdownField,
@@ -36,7 +41,8 @@
</label>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath">
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 28bf6c67ea5..d61776d480d 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -36,6 +36,16 @@
type: String,
required: true,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
lockedWarning,
@@ -78,9 +88,11 @@
<description-field
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath" />
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile" />
<edit-actions
:form-state="formState"
- :can-destroy="canDestroy" />
+ :can-destroy="canDestroy"
+ :show-delete-button="showDeleteButton" />
</form>
</template>
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 56cb536dcde..03546f61d1f 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,34 +1,23 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
-(function() {
- this.IssueStatusSelect = (function() {
- function IssueStatusSelect() {
- $('.js-issue-status').each(function(i, el) {
- var fieldName;
- fieldName = $(el).data("field-name");
- return $(el).glDropdown({
- selectable: true,
- fieldName: fieldName,
- toggleLabel: (function(_this) {
- return function(selected, el, instance) {
- var $item, label;
- label = 'Author';
- $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- };
- })(this),
- clicked: function(options) {
- return options.e.preventDefault();
- },
- id: function(obj, el) {
- return $(el).data("id");
- }
- });
- });
- }
-
- return IssueStatusSelect;
- })();
-}).call(window);
+export default function issueStatusSelect() {
+ $('.js-issue-status').each((i, el) => {
+ const fieldName = $(el).data('field-name');
+ return $(el).glDropdown({
+ selectable: true,
+ fieldName,
+ toggleLabel(selected, element, instance) {
+ let label = 'Author';
+ const $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
+ }
+ return label;
+ },
+ clicked(options) {
+ return options.e.preventDefault();
+ },
+ id(obj, element) {
+ return $(element).data('id');
+ },
+ });
+ });
+}
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index c6b5844dff6..cf8fda9a4fa 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -14,8 +14,8 @@ export default class Job {
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
+ this.$window = $(window);
this.logBytes = 0;
- this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
@@ -54,23 +54,18 @@ export default class Job {
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
- $(window)
+ this.$window
.off('scroll')
.on('scroll', () => {
- const contentHeight = this.$buildTraceOutput.height();
- if (contentHeight > this.windowSize) {
- // means the user did not scroll, the content was updated.
- this.windowSize = contentHeight;
- } else {
- // User scrolled
- this.hasBeenScrolled = true;
+ if (!this.isScrolledToBottom()) {
this.toggleScrollAnimation(false);
+ } else if (this.isScrolledToBottom() && !this.isLogComplete) {
+ this.toggleScrollAnimation(true);
}
-
this.scrollThrottled();
});
- $(window)
+ this.$window
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
@@ -99,14 +94,14 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this
canScroll() {
- return $(document).height() > $(window).height();
+ return this.$document.height() > this.$window.height();
}
toggleScroll() {
- const currentPosition = $(document).scrollTop();
- const scrollHeight = $(document).height();
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
- const windowHeight = $(window).height();
+ const windowHeight = this.$window.height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
@@ -119,7 +114,7 @@ export default class Job {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (scrollHeight - currentPosition === windowHeight) {
+ } else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false);
@@ -131,9 +126,17 @@ export default class Job {
}
}
+ isScrolledToBottom() {
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
+
+ const windowHeight = this.$window.height();
+ return scrollHeight - currentPosition === windowHeight;
+ }
+
// eslint-disable-next-line class-methods-use-this
scrollDown() {
- $(document).scrollTop($(document).height());
+ this.$document.scrollTop(this.$document.height());
}
scrollToBottom() {
@@ -143,7 +146,7 @@ export default class Job {
}
scrollToTop() {
- $(document).scrollTop(0);
+ this.$document.scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
}
@@ -174,7 +177,7 @@ export default class Job {
this.state = log.state;
}
- this.windowSize = this.$buildTraceOutput.height();
+ this.isScrollInBottom = this.isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
@@ -194,14 +197,9 @@ export default class Job {
} else {
this.$truncatedInfo.addClass('hidden');
}
+ this.isLogComplete = log.complete;
if (!log.complete) {
- if (!this.hasBeenScrolled) {
- this.toggleScrollAnimation(true);
- } else {
- this.toggleScrollAnimation(false);
- }
-
this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
@@ -218,7 +216,7 @@ export default class Job {
this.$buildRefreshAnimation.remove();
})
.then(() => {
- if (!this.hasBeenScrolled) {
+ if (this.isScrollInBottom) {
this.scrollDown();
}
})
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
index 3e2658f9fc1..5a216f8fae2 100644
--- a/app/assets/javascripts/jobs/job_details_mediator.js
+++ b/app/assets/javascripts/jobs/job_details_mediator.js
@@ -29,8 +29,8 @@ export default class JobMediator {
this.poll = new Poll({
resource: this.service,
method: 'getJob',
- successCallback: this.successCallback.bind(this),
- errorCallback: this.errorCallback.bind(this),
+ successCallback: response => this.successCallback(response),
+ errorCallback: () => this.errorCallback(),
});
if (!Visibility.hidden()) {
@@ -57,7 +57,7 @@ export default class JobMediator {
successCallback(response) {
this.state.isLoading = false;
- return response.json().then(data => this.store.storeJob(data));
+ return this.store.storeJob(response.data);
}
errorCallback() {
diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js
index eaf1c6e500a..b746489c45c 100644
--- a/app/assets/javascripts/jobs/services/job_service.js
+++ b/app/assets/javascripts/jobs/services/job_service.js
@@ -1,14 +1,11 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class JobService {
constructor(endpoint) {
- this.job = Vue.resource(endpoint);
+ this.job = endpoint;
}
getJob() {
- return this.job.get();
+ return axios.get(this.job);
}
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 1e52963b1dd..f7a1c9f1e40 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -6,479 +6,475 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
-(function() {
- this.LabelsSelect = (function() {
- function LabelsSelect(els, options = {}) {
- var _this, $els;
- _this = this;
+export default class LabelsSelect {
+ constructor(els, options = {}) {
+ var _this, $els;
+ _this = this;
- $els = $(els);
+ $els = $(els);
- if (!els) {
- $els = $('.js-label-select');
- }
+ if (!els) {
+ $els = $('.js-label-select');
+ }
- $els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
- $dropdown = $(dropdown);
- $dropdownContainer = $dropdown.closest('.labels-filter');
- $toggleText = $dropdown.find('.dropdown-toggle-text');
- namespacePath = $dropdown.data('namespace-path');
- projectPath = $dropdown.data('project-path');
- labelUrl = $dropdown.data('labels');
- issueUpdateURL = $dropdown.data('issueUpdate');
- selectedLabel = $dropdown.data('selected');
- if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
- selectedLabel = selectedLabel.split(',');
- }
- showNo = $dropdown.data('show-no');
- showAny = $dropdown.data('show-any');
- showMenuAbove = $dropdown.data('showMenuAbove');
- defaultLabel = $dropdown.data('default-label');
- abilityName = $dropdown.data('ability-name');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- $form = $dropdown.closest('form, .js-issuable-update');
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
- $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
- $value = $block.find('.value');
- $loading = $block.find('.block-loading').fadeOut();
- fieldName = $dropdown.data('field-name');
- useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
- propertyName = useId ? 'id' : 'title';
- initialSelected = $selectbox
- .find('input[name="' + $dropdown.data('field-name') + '"]')
- .map(function () {
- return this.value;
- }).get();
- if (issueUpdateURL != null) {
- issueURLSplit = issueUpdateURL.split('/');
- }
- if (issueUpdateURL) {
- labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
- labelNoneHTMLTemplate = '<span class="no-value">None</span>';
- }
- const handleClick = options.handleClick;
+ $els.each(function(i, dropdown) {
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ $dropdown = $(dropdown);
+ $dropdownContainer = $dropdown.closest('.labels-filter');
+ $toggleText = $dropdown.find('.dropdown-toggle-text');
+ namespacePath = $dropdown.data('namespace-path');
+ projectPath = $dropdown.data('project-path');
+ labelUrl = $dropdown.data('labels');
+ issueUpdateURL = $dropdown.data('issueUpdate');
+ selectedLabel = $dropdown.data('selected');
+ if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = selectedLabel.split(',');
+ }
+ showNo = $dropdown.data('show-no');
+ showAny = $dropdown.data('show-any');
+ showMenuAbove = $dropdown.data('showMenuAbove');
+ defaultLabel = $dropdown.data('default-label');
+ abilityName = $dropdown.data('ability-name');
+ $selectbox = $dropdown.closest('.selectbox');
+ $block = $selectbox.closest('.block');
+ $form = $dropdown.closest('form, .js-issuable-update');
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
+ $value = $block.find('.value');
+ $loading = $block.find('.block-loading').fadeOut();
+ fieldName = $dropdown.data('field-name');
+ useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+ propertyName = useId ? 'id' : 'title';
+ initialSelected = $selectbox
+ .find('input[name="' + $dropdown.data('field-name') + '"]')
+ .map(function () {
+ return this.value;
+ }).get();
+ if (issueUpdateURL != null) {
+ issueURLSplit = issueUpdateURL.split('/');
+ }
+ if (issueUpdateURL) {
+ labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
+ labelNoneHTMLTemplate = '<span class="no-value">None</span>';
+ }
+ const handleClick = options.handleClick;
- $sidebarLabelTooltip.tooltip();
+ $sidebarLabelTooltip.tooltip();
- if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
- }
+ if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
+ new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ }
- saveLabelData = function() {
- var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
- return this.value;
- }).get();
+ saveLabelData = function() {
+ var data, selected;
+ selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
+ return this.value;
+ }).get();
- if (_.isEqual(initialSelected, selected)) return;
- initialSelected = selected;
+ if (_.isEqual(initialSelected, selected)) return;
+ initialSelected = selected;
- data = {};
- data[abilityName] = {};
- data[abilityName].label_ids = selected;
- if (!selected.length) {
- data[abilityName].label_ids = [''];
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].label_ids = selected;
+ if (!selected.length) {
+ data[abilityName].label_ids = [''];
+ }
+ $loading.removeClass('hidden').fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+ return $.ajax({
+ type: 'PUT',
+ url: issueUpdateURL,
+ dataType: 'JSON',
+ data: data
+ }).done(function(data) {
+ var labelCount, template, labelTooltipTitle, labelTitles;
+ $loading.fadeOut();
+ $dropdown.trigger('loaded.gl.dropdown');
+ $selectbox.hide();
+ data.issueURLSplit = issueURLSplit;
+ labelCount = 0;
+ if (data.labels.length) {
+ template = labelHTMLTemplate(data);
+ labelCount = data.labels.length;
}
- $loading.removeClass('hidden').fadeIn();
- $dropdown.trigger('loading.gl.dropdown');
- return $.ajax({
- type: 'PUT',
- url: issueUpdateURL,
- dataType: 'JSON',
- data: data
- }).done(function(data) {
- var labelCount, template, labelTooltipTitle, labelTitles;
- $loading.fadeOut();
- $dropdown.trigger('loaded.gl.dropdown');
- $selectbox.hide();
- data.issueURLSplit = issueURLSplit;
- labelCount = 0;
- if (data.labels.length) {
- template = labelHTMLTemplate(data);
- labelCount = data.labels.length;
- }
- else {
- template = labelNoneHTMLTemplate;
- }
- $value.removeAttr('style').html(template);
- $sidebarCollapsedValue.text(labelCount);
-
- if (data.labels.length) {
- labelTitles = data.labels.map(function(label) {
- return label.title;
- });
+ else {
+ template = labelNoneHTMLTemplate;
+ }
+ $value.removeAttr('style').html(template);
+ $sidebarCollapsedValue.text(labelCount);
- if (labelTitles.length > 5) {
- labelTitles = labelTitles.slice(0, 5);
- labelTitles.push('and ' + (data.labels.length - 5) + ' more');
- }
+ if (data.labels.length) {
+ labelTitles = data.labels.map(function(label) {
+ return label.title;
+ });
- labelTooltipTitle = labelTitles.join(', ');
- }
- else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ if (labelTitles.length > 5) {
+ labelTitles = labelTitles.slice(0, 5);
+ labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
- $sidebarLabelTooltip
- .attr('title', labelTooltipTitle)
- .tooltip('fixTitle');
+ labelTooltipTitle = labelTitles.join(', ');
+ }
+ else {
+ labelTooltipTitle = '';
+ $sidebarLabelTooltip.tooltip('destroy');
+ }
+
+ $sidebarLabelTooltip
+ .attr('title', labelTooltipTitle)
+ .tooltip('fixTitle');
- $('.has-tooltip', $value).tooltip({
- container: 'body'
- });
+ $('.has-tooltip', $value).tooltip({
+ container: 'body'
});
- };
- $dropdown.glDropdown({
- showMenuAbove: showMenuAbove,
- data: function(term, callback) {
- return $.ajax({
- url: labelUrl
- }).done(function(data) {
- data = _.chain(data).groupBy(function(label) {
- return label.title;
- }).map(function(label) {
- var color;
- color = _.map(label, function(dup) {
- return dup.color;
+ });
+ };
+ $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
+ data: function(term, callback) {
+ return $.ajax({
+ url: labelUrl
+ }).done(function(data) {
+ data = _.chain(data).groupBy(function(label) {
+ return label.title;
+ }).map(function(label) {
+ var color;
+ color = _.map(label, function(dup) {
+ return dup.color;
+ });
+ return {
+ id: label[0].id,
+ title: label[0].title,
+ color: color,
+ duplicate: color.length > 1
+ };
+ }).value();
+ if ($dropdown.hasClass('js-extra-options')) {
+ var extraData = [];
+ if (showNo) {
+ extraData.unshift({
+ id: 0,
+ title: 'No Label'
});
- return {
- id: label[0].id,
- title: label[0].title,
- color: color,
- duplicate: color.length > 1
- };
- }).value();
- if ($dropdown.hasClass('js-extra-options')) {
- var extraData = [];
- if (showNo) {
- extraData.unshift({
- id: 0,
- title: 'No Label'
- });
- }
- if (showAny) {
- extraData.unshift({
- isAny: true,
- title: 'Any Label'
- });
- }
- if (extraData.length) {
- extraData.push('divider');
- data = extraData.concat(data);
- }
- }
-
- callback(data);
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
}
- });
- },
- renderRow: function(label, instance) {
- var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
- $li = $('<li>');
- $a = $('<a href="#">');
- selectedClass = [];
- removesAll = label.id <= 0 || (label.id == null);
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = $dropdown.data('indeterminate') || [];
- marked = $dropdown.data('marked') || [];
-
- if (indeterminate.indexOf(label.id) !== -1) {
- selectedClass.push('is-indeterminate');
- }
-
- if (marked.indexOf(label.id) !== -1) {
- // Remove is-indeterminate class if the item will be marked as active
- i = selectedClass.indexOf('is-indeterminate');
- if (i !== -1) {
- selectedClass.splice(i, 1);
- }
- selectedClass.push('is-active');
- }
- } else {
- if (this.id(label)) {
- dropdownName = $dropdown.data('fieldName');
- dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
-
- if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
- selectedClass.push('is-active');
- }
+ if (showAny) {
+ extraData.unshift({
+ isAny: true,
+ title: 'Any Label'
+ });
}
-
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
+ if (extraData.length) {
+ extraData.push('divider');
+ data = extraData.concat(data);
}
}
- if (label.duplicate) {
- color = gl.DropdownUtils.duplicateLabelColor(label.color);
+
+ callback(data);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
}
- else {
- if (label.color != null) {
- color = label.color[0];
+ });
+ },
+ renderRow: function(label, instance) {
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
+ $li = $('<li>');
+ $a = $('<a href="#">');
+ selectedClass = [];
+ removesAll = label.id <= 0 || (label.id == null);
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
+ if (indeterminate.indexOf(label.id) !== -1) {
+ selectedClass.push('is-indeterminate');
+ }
+
+ if (marked.indexOf(label.id) !== -1) {
+ // Remove is-indeterminate class if the item will be marked as active
+ i = selectedClass.indexOf('is-indeterminate');
+ if (i !== -1) {
+ selectedClass.splice(i, 1);
}
+ selectedClass.push('is-active');
}
- if (color) {
- colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- }
- else {
- colorEl = '';
- }
- // We need to identify which items are actually labels
- if (label.id) {
- selectedClass.push('label-item');
- $a.attr('data-label-id', label.id);
- }
- $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
- // Return generated html
- return $li.html($a).prop('outerHTML');
- },
- search: {
- fields: ['title']
- },
- selectable: true,
- filterable: true,
- selected: $dropdown.data('selected') || [],
- toggleLabel: function(selected, el) {
- var isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected.title;
- var selectedLabels = this.selected;
-
- if (selected.id === 0) {
- this.selected = [];
- return 'No Label';
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
}
- else if (isSelected) {
- this.selected.push(title);
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
}
- else {
- var index = this.selected.indexOf(title);
- this.selected.splice(index, 1);
+ }
+ if (label.duplicate) {
+ color = gl.DropdownUtils.duplicateLabelColor(label.color);
+ }
+ else {
+ if (label.color != null) {
+ color = label.color[0];
}
+ }
+ if (color) {
+ colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
+ }
+ else {
+ colorEl = '';
+ }
+ // We need to identify which items are actually labels
+ if (label.id) {
+ selectedClass.push('label-item');
+ $a.attr('data-label-id', label.id);
+ }
+ $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
+ // Return generated html
+ return $li.html($a).prop('outerHTML');
+ },
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ filterable: true,
+ selected: $dropdown.data('selected') || [],
+ toggleLabel: function(selected, el) {
+ var isSelected = el !== null ? el.hasClass('is-active') : false;
+ var title = selected.title;
+ var selectedLabels = this.selected;
+
+ if (selected.id === 0) {
+ this.selected = [];
+ return 'No Label';
+ }
+ else if (isSelected) {
+ this.selected.push(title);
+ }
+ else {
+ var index = this.selected.indexOf(title);
+ this.selected.splice(index, 1);
+ }
+
+ if (selectedLabels.length === 1) {
+ return selectedLabels;
+ }
+ else if (selectedLabels.length) {
+ return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ }
+ else {
+ return defaultLabel;
+ }
+ },
+ fieldName: $dropdown.data('field-name'),
+ id: function(label) {
+ if (label.id <= 0) return label.title;
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return label.id;
+ }
- if (selectedLabels.length === 1) {
- return selectedLabels;
+ if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
+ return label.title;
+ }
+ else {
+ return label.id;
+ }
+ },
+ hidden: function() {
+ var isIssueIndex, isMRIndex, page, selectedLabels;
+ page = $('body').attr('data-page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
+ $selectbox.hide();
+ // display:block overrides the hide-collapse rule
+ $value.removeAttr('style');
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
+
+ if ($('html').hasClass('issue-boards-page')) {
+ return;
+ }
+ if ($dropdown.hasClass('js-multiselect')) {
+ if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
+ Issuable.filterResults($dropdown.closest('form'));
}
- else if (selectedLabels.length) {
- return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ else if ($dropdown.hasClass('js-filter-submit')) {
+ $dropdown.closest('form').submit();
}
else {
- return defaultLabel;
+ if (!$dropdown.hasClass('js-filter-bulk-update')) {
+ saveLabelData();
+ }
}
- },
- fieldName: $dropdown.data('field-name'),
- id: function(label) {
- if (label.id <= 0) return label.title;
+ }
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
+ clicked: function(options) {
+ const { $el, e, isMarking } = options;
+ const label = options.selectedObj;
+
+ var isIssueIndex, isMRIndex, page, boardsModel;
+ var fadeOutLoader = () => {
+ $loading.fadeOut();
+ };
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return label.id;
- }
+ page = $('body').attr('data-page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
- if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
- return label.title;
- }
- else {
- return label.id;
- }
- },
- hidden: function() {
- var isIssueIndex, isMRIndex, page, selectedLabels;
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
- $selectbox.hide();
- // display:block overrides the hide-collapse rule
- $value.removeAttr('style');
-
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return;
- }
+ if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
+ $dropdown.parent()
+ .find('.dropdown-clear-active')
+ .removeClass('is-active');
+ }
- if ($('html').hasClass('issue-boards-page')) {
- return;
- }
- if ($dropdown.hasClass('js-multiselect')) {
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
- Issuable.filterResults($dropdown.closest('form'));
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
- $dropdown.closest('form').submit();
- }
- else {
- if (!$dropdown.hasClass('js-filter-bulk-update')) {
- saveLabelData();
- }
- }
- }
- },
- multiSelect: $dropdown.hasClass('js-multiselect'),
- vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(clickEvent) {
- const { $el, e, isMarking } = clickEvent;
- const label = clickEvent.selectedObj;
-
- var isIssueIndex, isMRIndex, page, boardsModel;
- var fadeOutLoader = () => {
- $loading.fadeOut();
- };
-
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
-
- if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
- $dropdown.parent()
- .find('.dropdown-clear-active')
- .removeClass('is-active');
- }
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return;
- }
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, label.id);
+ return;
+ }
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- _this.enableBulkLabelDropdown();
- _this.setDropdownData($dropdown, isMarking, label.id);
- return;
- }
+ if ($dropdown.closest('.add-issues-modal').length) {
+ boardsModel = gl.issueBoards.ModalStore.store.filter;
+ }
- if ($dropdown.closest('.add-issues-modal').length) {
- boardsModel = gl.issueBoards.ModalStore.store.filter;
+ if (boardsModel) {
+ if (label.isAny) {
+ boardsModel['label_name'] = [];
+ } else if ($el.hasClass('is-active')) {
+ boardsModel['label_name'].push(label.title);
}
- if (boardsModel) {
- if (label.isAny) {
- boardsModel['label_name'] = [];
- } else if ($el.hasClass('is-active')) {
- boardsModel['label_name'].push(label.title);
- }
-
- e.preventDefault();
- return;
+ e.preventDefault();
+ return;
+ }
+ else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (!$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = label.title;
+ return Issuable.filterResults($dropdown.closest('form'));
}
- else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- if (!$dropdown.hasClass('js-multiselect')) {
- selectedLabel = label.title;
- return Issuable.filterResults($dropdown.closest('form'));
- }
+ }
+ else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ }
+ else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.hasClass('is-active')) {
+ gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff'
+ }));
}
- else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
+ else {
+ var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ labels = labels.filter(function (selectedLabel) {
+ return selectedLabel.id !== label.id;
+ });
+ gl.issueBoards.BoardsStore.detail.issue.labels = labels;
}
- else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if ($el.hasClass('is-active')) {
- gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color[0],
- textColor: '#fff'
- }));
- }
- else {
- var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
- labels = labels.filter(function (selectedLabel) {
- return selectedLabel.id !== label.id;
- });
- gl.issueBoards.BoardsStore.detail.issue.labels = labels;
- }
- $loading.fadeIn();
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(fadeOutLoader)
+ .catch(fadeOutLoader);
+ }
+ else if (handleClick) {
+ e.preventDefault();
+ handleClick(label);
+ }
+ else {
+ if ($dropdown.hasClass('js-multiselect')) {
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
- .then(fadeOutLoader)
- .catch(fadeOutLoader);
- }
- else if (handleClick) {
- e.preventDefault();
- handleClick(label);
}
else {
- if ($dropdown.hasClass('js-multiselect')) {
-
- }
- else {
- return saveLabelData();
- }
+ return saveLabelData();
}
- },
- });
-
- // Set dropdown data
- _this.setOriginalDropdownData($dropdownContainer, $dropdown);
+ }
+ },
});
- this.bindEvents();
- }
- LabelsSelect.prototype.bindEvents = function() {
- return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
- };
-
- LabelsSelect.prototype.onSelectCheckboxIssue = function() {
- if ($('.selected_issue:checked').length) {
- return;
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
+ });
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ onSelectCheckboxIssue() {
+ if ($('.selected_issue:checked').length) {
+ return;
+ }
+ return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
+ }
+ // eslint-disable-next-line class-methods-use-this
+ enableBulkLabelDropdown() {
+ IssuableBulkUpdateActions.willUpdateLabels = true;
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setDropdownData($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
}
- return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
- };
- LabelsSelect.prototype.enableBulkLabelDropdown = function() {
- IssuableBulkUpdateActions.willUpdateLabels = true;
- };
-
- LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
- var i, markedIds, unmarkedIds, indeterminateIds;
-
- markedIds = $dropdown.data('marked') || [];
- unmarkedIds = $dropdown.data('unmarked') || [];
- indeterminateIds = $dropdown.data('indeterminate') || [];
-
- if (isMarking) {
- markedIds.push(value);
-
- i = indeterminateIds.indexOf(value);
- if (i > -1) {
- indeterminateIds.splice(i, 1);
- }
-
- i = unmarkedIds.indexOf(value);
- if (i > -1) {
- unmarkedIds.splice(i, 1);
- }
- } else {
- // If marked item (not common) is unmarked
- i = markedIds.indexOf(value);
- if (i > -1) {
- markedIds.splice(i, 1);
- }
-
- // If an indeterminate item is being unmarked
- if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
- unmarkedIds.push(value);
- }
-
- // If a marked item is being unmarked
- // (a marked item could also be a label that is present in all selection)
- if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
- unmarkedIds.push(value);
- }
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
}
- $dropdown.data('marked', markedIds);
- $dropdown.data('unmarked', unmarkedIds);
- $dropdown.data('indeterminate', indeterminateIds);
- };
+ // If an indeterminate item is being unmarked
+ if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
- LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
- var labels = [];
- $container.find('[name="label_name[]"]').map(function() {
- return labels.push(this.value);
- });
- $dropdown.data('marked', labels);
- };
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
- return LabelsSelect;
- })();
-}).call(window);
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setOriginalDropdownData($container, $dropdown) {
+ const labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ }
+}
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 3d64b121fa7..dbbf1637a47 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -1,5 +1,3 @@
-/* eslint-disable one-export, one-var, one-var-declaration-per-line */
-
import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
@@ -21,7 +19,10 @@ export default class LazyLoader {
}
searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
- this.checkElementsInView();
+
+ if (this.lazyImages.length) {
+ this.checkElementsInView();
+ }
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
@@ -45,15 +46,13 @@ export default class LazyLoader {
checkElementsInView() {
const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
- let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
- imgBoundRect = selectedImage.getBoundingClientRect();
-
- imgTop = scrollTop + imgBoundRect.top;
- imgBound = imgTop + imgBoundRect.height;
+ const imgBoundRect = selectedImage.getBoundingClientRect();
+ const imgTop = scrollTop + imgBoundRect.top;
+ const imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage);
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
new file mode 100644
index 00000000000..7aeeca3b283
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -0,0 +1,22 @@
+import axios from 'axios';
+import csrf from './csrf';
+
+axios.defaults.headers.common[csrf.headerKey] = csrf.token;
+
+// Maintain a global counter for active requests
+// see: spec/support/wait_for_requests.rb
+axios.interceptors.request.use((config) => {
+ window.activeVueResources = window.activeVueResources || 0;
+ window.activeVueResources += 1;
+
+ return config;
+});
+
+// Remove the global counter
+axios.interceptors.response.use((config) => {
+ window.activeVueResources -= 1;
+
+ return config;
+});
+
+export default axios;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 07899777a1e..195e2ca6a78 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment;
};
-// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart;
@@ -311,6 +310,42 @@ export const setParamInURL = (param, value) => {
};
/**
+ * Given a string of query parameters creates an object.
+ *
+ * @example
+ * `scope=all&page=2` -> { scope: 'all', page: '2'}
+ * `scope=all` -> { scope: 'all' }
+ * ``-> {}
+ * @param {String} query
+ * @returns {Object}
+ */
+export const parseQueryStringIntoObject = (query = '') => {
+ if (query === '') return {};
+
+ return query
+ .split('&')
+ .reduce((acc, element) => {
+ const val = element.split('=');
+ Object.assign(acc, {
+ [val[0]]: decodeURIComponent(val[1]),
+ });
+ return acc;
+ }, {});
+};
+
+export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
+
+/**
+ * Based on the current location and the string parameters provided
+ * creates a new entry in the history without reloading the page.
+ *
+ * @param {String} param
+ */
+export const historyPushState = (newUrl) => {
+ window.history.pushState({}, document.title, newUrl);
+};
+
+/**
* Converts permission provided as strings to booleans.
*
* @param {String} string
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 29fc91733b3..5679b8c9a09 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,6 +2,7 @@
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
+import { pluralize } from './text_utility';
import {
lang,
@@ -143,9 +144,9 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = '';
if (minutes >= 1) {
- text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else {
- text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 917a45eb06b..a02c79b787e 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -52,3 +52,31 @@ export function bytesToKiB(number) {
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
+
+/**
+ * Utility function that calculates GiB of the given bytes.
+ * @param {Number} number
+ * @returns {Number}
+ */
+export function bytesToGiB(number) {
+ return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB);
+}
+
+/**
+ * Port of rails number_to_human_size
+ * Formats the bytes in number into a more understandable
+ * representation (e.g., giving it 1500 yields 1.5 KB).
+ *
+ * @param {Number} size
+ * @returns {String}
+ */
+export function numberToHumanSize(size) {
+ if (size < BYTES_IN_KIB) {
+ return `${size} bytes`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToKiB(size).toFixed(2)} KiB`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToMiB(size).toFixed(2)} MiB`;
+ }
+ return `${bytesToGiB(size).toFixed(2)} GiB`;
+}
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 1485e900945..7fca80c2fdb 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils';
/**
* Polling utility for handling realtime updates.
- * Service for vue resouce and method need to be provided as props
+ * Requirements: Promise based HTTP client
+ *
+ * Service for promise based http client and method need to be provided as props
*
* @example
* new Poll({
@@ -60,7 +62,6 @@ export default class Poll {
checkConditions(response) {
const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
-
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
this.makeRequest();
@@ -102,7 +103,12 @@ export default class Poll {
/**
* Restarts polling after it has been stoped
*/
- restart() {
+ restart(options) {
+ // update data
+ if (options && options.data) {
+ this.options.data = options.data;
+ }
+
this.canPoll = true;
this.makeRequest();
}
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
new file mode 100644
index 00000000000..2dc9cf0cc29
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -0,0 +1,153 @@
+/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
+
+const textUtils = {};
+
+textUtils.selectedText = function(text, textarea) {
+ return text.substring(textarea.selectionStart, textarea.selectionEnd);
+};
+
+textUtils.lineBefore = function(text, textarea) {
+ var split;
+ split = text.substring(0, textarea.selectionStart).trim().split('\n');
+ return split[split.length - 1];
+};
+
+textUtils.lineAfter = function(text, textarea) {
+ return text.substring(textarea.selectionEnd).trim().split('\n')[0];
+};
+
+textUtils.blockTagText = function(text, textArea, blockTag, selected) {
+ var lineAfter, lineBefore;
+ lineBefore = this.lineBefore(text, textArea);
+ lineAfter = this.lineAfter(text, textArea);
+ if (lineBefore === blockTag && lineAfter === blockTag) {
+ // To remove the block tag we have to select the line before & after
+ if (blockTag != null) {
+ textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
+ textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
+ }
+ return selected;
+ } else {
+ return blockTag + "\n" + selected + "\n" + blockTag;
+ }
+};
+
+textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
+ removedLastNewLine = false;
+ removedFirstNewLine = false;
+ currentLineEmpty = false;
+
+ // Remove the first newline
+ if (selected.indexOf('\n') === 0) {
+ removedFirstNewLine = true;
+ selected = selected.replace(/\n+/, '');
+ }
+
+ // Remove the last newline
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+
+ selectedSplit = selected.split('\n');
+
+ if (!wrap) {
+ lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+
+ // Check whether the current line is empty or consists only of spaces(=handle as empty)
+ if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+ currentLineEmpty = true;
+ }
+ }
+
+ startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+
+ if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
+ if (blockTag != null && blockTag !== '') {
+ insertText = this.blockTagText(text, textArea, blockTag, selected);
+ } else {
+ insertText = selectedSplit.map(function(val) {
+ if (val.indexOf(tag) === 0) {
+ return "" + (val.replace(tag, ''));
+ } else {
+ return "" + tag + val;
+ }
+ }).join('\n');
+ }
+ } else {
+ insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+ }
+
+ if (removedFirstNewLine) {
+ insertText = '\n' + insertText;
+ }
+
+ if (removedLastNewLine) {
+ insertText += '\n';
+ }
+
+ if (document.queryCommandSupported('insertText')) {
+ inserted = document.execCommand('insertText', false, insertText);
+ }
+ if (!inserted) {
+ try {
+ document.execCommand("ms-beginUndoUnit");
+ } catch (error) {}
+ textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
+ try {
+ document.execCommand("ms-endUndoUnit");
+ } catch (error) {}
+ }
+ return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
+};
+
+textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
+ var pos;
+ if (!textArea.setSelectionRange) {
+ return;
+ }
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (wrapped) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
+
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
+ return textArea.setSelectionRange(pos, pos);
+ }
+};
+
+textUtils.updateText = function(textArea, tag, blockTag, wrap) {
+ var $textArea, selected, text;
+ $textArea = $(textArea);
+ textArea = $textArea.get(0);
+ text = $textArea.val();
+ selected = this.selectedText(text, textArea);
+ $textArea.focus();
+ return this.insertText(textArea, text, tag, blockTag, selected, wrap);
+};
+
+textUtils.init = function(form) {
+ var self;
+ self = this;
+ return $('.js-md', form).off('click').on('click', function() {
+ var $this;
+ $this = $(this);
+ return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
+ });
+};
+
+textUtils.removeListeners = function(form) {
+ return $('.js-md', form).off('click');
+};
+
+textUtils.replaceRange = function(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+};
+
+export default textUtils;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index f776829f69c..a1475b92c7e 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,18 +1,13 @@
-/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
-
-import 'vendor/latinise';
-
-var base;
-var w = window;
-if (w.gl == null) {
- w.gl = {};
-}
-if ((base = w.gl).text == null) {
- base.text = {};
-}
-gl.text.addDelimiter = function(text) {
- return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
-};
+/**
+ * Adds a , to a string composed by numbers, at every 3 chars.
+ *
+ * 2333 -> 2,333
+ * 232324 -> 232,324
+ *
+ * @param {String} text
+ * @returns {String}
+ */
+export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
/**
* Returns '99+' for numbers bigger than 99.
@@ -20,178 +15,43 @@ gl.text.addDelimiter = function(text) {
* @param {Number} count
* @return {Number|String}
*/
-export function highCountTrim(count) {
- return count > 99 ? '99+' : count;
-}
-
-gl.text.randomString = function() {
- return Math.random().toString(36).substring(7);
-};
-gl.text.replaceRange = function(s, start, end, substitute) {
- return s.substring(0, start) + substitute + s.substring(end);
-};
-gl.text.getTextWidth = function(text, font) {
- /**
- * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
- *
- * @param {String} text The text to be rendered.
- * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
- *
- * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
- */
- // re-use canvas object for better performance
- var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
- var context = canvas.getContext('2d');
- context.font = font;
- return context.measureText(text).width;
-};
-gl.text.selectedText = function(text, textarea) {
- return text.substring(textarea.selectionStart, textarea.selectionEnd);
-};
-gl.text.lineBefore = function(text, textarea) {
- var split;
- split = text.substring(0, textarea.selectionStart).trim().split('\n');
- return split[split.length - 1];
-};
-gl.text.lineAfter = function(text, textarea) {
- return text.substring(textarea.selectionEnd).trim().split('\n')[0];
-};
-gl.text.blockTagText = function(text, textArea, blockTag, selected) {
- var lineAfter, lineBefore;
- lineBefore = this.lineBefore(text, textArea);
- lineAfter = this.lineAfter(text, textArea);
- if (lineBefore === blockTag && lineAfter === blockTag) {
- // To remove the block tag we have to select the line before & after
- if (blockTag != null) {
- textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
- textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
- }
- return selected;
- } else {
- return blockTag + "\n" + selected + "\n" + blockTag;
- }
-};
-gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
- removedLastNewLine = false;
- removedFirstNewLine = false;
- currentLineEmpty = false;
-
- // Remove the first newline
- if (selected.indexOf('\n') === 0) {
- removedFirstNewLine = true;
- selected = selected.replace(/\n+/, '');
- }
-
- // Remove the last newline
- if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
- removedLastNewLine = true;
- selected = selected.replace(/\n$/, '');
- }
-
- selectedSplit = selected.split('\n');
-
- if (!wrap) {
- lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
-
- // Check whether the current line is empty or consists only of spaces(=handle as empty)
- if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
- currentLineEmpty = true;
- }
- }
-
- startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+export const highCountTrim = count => (count > 99 ? '99+' : count);
- if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
- if (blockTag != null && blockTag !== '') {
- insertText = this.blockTagText(text, textArea, blockTag, selected);
- } else {
- insertText = selectedSplit.map(function(val) {
- if (val.indexOf(tag) === 0) {
- return "" + (val.replace(tag, ''));
- } else {
- return "" + tag + val;
- }
- }).join('\n');
- }
- } else {
- insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
- }
+/**
+ * Converts first char to uppercase and replaces undercores with spaces
+ * @param {String} string
+ * @requires {String}
+ */
+export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
- if (removedFirstNewLine) {
- insertText = '\n' + insertText;
- }
+/**
+ * Adds an 's' to the end of the string when count is bigger than 0
+ * @param {String} str
+ * @param {Number} count
+ * @returns {String}
+ */
+export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
- if (removedLastNewLine) {
- insertText += '\n';
- }
+/**
+ * Replaces underscores with dashes
+ * @param {*} str
+ * @returns {String}
+ */
+export const dasherize = str => str.replace(/[_\s]+/g, '-');
- if (document.queryCommandSupported('insertText')) {
- inserted = document.execCommand('insertText', false, insertText);
- }
- if (!inserted) {
- try {
- document.execCommand("ms-beginUndoUnit");
- } catch (error) {}
- textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
- try {
- document.execCommand("ms-endUndoUnit");
- } catch (error) {}
- }
- return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
-};
-gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
- var pos;
- if (!textArea.setSelectionRange) {
- return;
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (wrapped) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
- }
+/**
+ * Removes accents and converts to lower case
+ * @param {String} str
+ * @returns {String}
+ */
+export const slugify = str => str.trim().toLowerCase();
- if (removedLastNewLine) {
- pos -= 1;
- }
+/**
+ * Truncates given text
+ *
+ * @param {String} string
+ * @param {Number} maxLength
+ * @returns {String}
+ */
+export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
- return textArea.setSelectionRange(pos, pos);
- }
-};
-gl.text.updateText = function(textArea, tag, blockTag, wrap) {
- var $textArea, selected, text;
- $textArea = $(textArea);
- textArea = $textArea.get(0);
- text = $textArea.val();
- selected = this.selectedText(text, textArea);
- $textArea.focus();
- return this.insertText(textArea, text, tag, blockTag, selected, wrap);
-};
-gl.text.init = function(form) {
- var self;
- self = this;
- return $('.js-md', form).off('click').on('click', function() {
- var $this;
- $this = $(this);
- return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
- });
-};
-gl.text.removeListeners = function(form) {
- return $('.js-md', form).off('click');
-};
-gl.text.humanize = function(string) {
- return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
-};
-gl.text.pluralize = function(str, count) {
- return str + (count > 1 || count === 0 ? 's' : '');
-};
-gl.text.truncate = function(string, maxLength) {
- return string.substr(0, (maxLength - 3)) + '...';
-};
-gl.text.dasherize = function(str) {
- return str.replace(/[_\s]+/g, '-');
-};
-gl.text.slugify = function(str) {
- return str.trim().toLowerCase().latinise();
-};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 1aa63216baf..17236c91490 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
}
}
+export function redirectTo(url) {
+ return window.location.assign(url);
+}
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 729baa2e1a7..3688a57937e 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,7 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
-
-(function() {
- window.addEventListener('beforeunload', function() {
+export default function initLogoAnimation() {
+ window.addEventListener('beforeunload', () => {
$('.tanuki-logo').addClass('animate');
});
-}).call(window);
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index fd9d0c335a5..d908452399c 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -12,7 +12,6 @@ import svg4everybody from 'svg4everybody';
// libraries with import side-effects
import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
-import 'vendor/fuzzaldrin-plus';
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
@@ -30,8 +29,6 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
-import './lib/utils/pretty_time';
-import './lib/utils/text_utility';
import './lib/utils/url_utility';
// behaviors
@@ -47,55 +44,37 @@ import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
-import './copy_as_gfm';
import './copy_to_clipboard';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
-import './header';
+import initTodoToggle from './header';
import initImporterStatus from './importer_status';
-import './issuable_form';
-import './issue';
-import './issue_status_select';
-import './labels_select';
import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
-import './logo';
+import initLogoAnimation from './logo';
import './merge_request';
import './merge_request_tabs';
-import './milestone';
import './milestone_select';
-import './namespace_select';
-import './new_branch_form';
-import './new_commit_form';
import './notes';
import './notifications_dropdown';
import './notifications_form';
import './pager';
import './preview_markdown';
-import './project';
-import './project_avatar';
import './project_find_file';
import './project_import';
-import './project_label_subscription';
-import './project_new';
-import './project_select';
-import './project_show';
-import './project_variables';
import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
import './render_math';
+import './render_mermaid';
import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
-import './smart_interval';
-import './subscription';
-import './subscription_select';
import initBreadcrumbs from './breadcrumb';
import './dispatcher';
@@ -137,6 +116,8 @@ $(function () {
initBreadcrumbs();
initImporterStatus();
+ initTodoToggle();
+ initLogoAnimation();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 6264750a4fb..52315e969d1 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -5,7 +5,6 @@ export default class Members {
}
addListeners() {
- $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
@@ -33,17 +32,6 @@ export default class Members {
});
});
}
- // eslint-disable-next-line class-methods-use-this
- removeRow(e) {
- const $target = $(e.target);
-
- if ($target.hasClass('btn-remove')) {
- $target.closest('.member')
- .fadeOut(function fadeOutMemberRow() {
- $(this).remove();
- });
- }
- }
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index af0658eb668..d30ff12bb59 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
+import { addDelimiter } from './lib/utils/text_utility';
(function() {
this.MergeRequest = (function() {
@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
- $el.text(gl.text.addDelimiter(count));
+ $el.text(addDelimiter(count));
};
MergeRequest.prototype.hideCloseButton = function() {
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 8f3f1986763..f76a998bf8c 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,54 +1,49 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Sortable */
import Flash from './flash';
-(function() {
- this.Milestone = (function() {
- function Milestone() {
- this.bindTabsSwitching();
+export default class Milestone {
+ constructor() {
+ this.bindTabsSwitching();
- // Load merge request tab if it is active
- // merge request tab is active based on different conditions in the backend
- this.loadTab($('.js-milestone-tabs .active a'));
+ // Load merge request tab if it is active
+ // merge request tab is active based on different conditions in the backend
+ this.loadTab($('.js-milestone-tabs .active a'));
- this.loadInitialTab();
- }
+ this.loadInitialTab();
+ }
+
+ bindTabsSwitching() {
+ return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
+ const $target = $(e.target);
- Milestone.prototype.bindTabsSwitching = function() {
- return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
- const $target = $(e.target);
+ location.hash = $target.attr('href');
+ this.loadTab($target);
+ });
+ }
+ // eslint-disable-next-line class-methods-use-this
+ loadInitialTab() {
+ const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
- location.hash = $target.attr('href');
- this.loadTab($target);
+ if ($target.length) {
+ $target.tab('show');
+ }
+ }
+ // eslint-disable-next-line class-methods-use-this
+ loadTab($target) {
+ const endpoint = $target.data('endpoint');
+ const tabElId = $target.attr('href');
+
+ if (endpoint && !$target.hasClass('is-loaded')) {
+ $.ajax({
+ url: endpoint,
+ dataType: 'JSON',
+ })
+ .fail(() => new Flash('Error loading milestone tab'))
+ .done((data) => {
+ $(tabElId).html(data.html);
+ $target.addClass('is-loaded');
});
- };
-
- Milestone.prototype.loadInitialTab = function() {
- const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
-
- if ($target.length) {
- $target.tab('show');
- }
- };
-
- Milestone.prototype.loadTab = function($target) {
- const endpoint = $target.data('endpoint');
- const tabElId = $target.attr('href');
-
- if (endpoint && !$target.hasClass('is-loaded')) {
- $.ajax({
- url: endpoint,
- dataType: 'JSON',
- })
- .fail(() => new Flash('Error loading milestone tab'))
- .done((data) => {
- $(tabElId).html(data.html);
- $target.addClass('is-loaded');
- });
- }
- };
-
- return Milestone;
- })();
-}).call(window);
+ }
+ }
+}
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 5aa3865f96a..f8782fde927 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -138,7 +138,7 @@
renderAxesPaths() {
this.timeSeries = createTimeSeries(
- this.graphData.queries[0],
+ this.graphData.queries,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset,
@@ -153,8 +153,9 @@
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
- axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
- axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
+ const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
+ axisXScale.domain(d3.extent(allValues, d => d.time));
+ axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
const xAxis = d3.svg.axis()
.scale(axisXScale)
@@ -246,6 +247,7 @@
:key="index"
:generated-line-path="path.linePath"
:generated-area-path="path.areaPath"
+ :line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index 85b6d7f4cbe..440b1b12631 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -79,7 +79,8 @@
},
formatMetricUsage(series) {
- const value = series.values[this.currentDataIndex].value;
+ const value = series.values[this.currentDataIndex] &&
+ series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
@@ -92,6 +93,12 @@
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
+
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
+ },
},
mounted() {
this.$nextTick(() => {
@@ -162,13 +169,15 @@
v-for="(series, index) in timeSeries"
:key="index"
:transform="translateLegendGroup(index)">
- <rect
- :fill="series.areaColor"
- :width="measurements.legends.width"
- :height="measurements.legends.height"
- x="20"
- :y="graphHeight - measurements.legendOffset">
- </rect>
+ <line
+ :stroke="series.lineColor"
+ :stroke-width="measurements.legends.height"
+ :stroke-dasharray="strokeDashArray(series.lineStyle)"
+ :x1="measurements.legends.offsetX"
+ :x2="measurements.legends.offsetX + measurements.legends.width"
+ :y1="graphHeight - measurements.legends.offsetY"
+ :y2="graphHeight - measurements.legends.offsetY">
+ </line>
<text
v-if="timeSeries.length > 1"
class="legend-metric-title"
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 043f1bf66bb..5e6d409033a 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -9,6 +9,10 @@
type: String,
required: true,
},
+ lineStyle: {
+ type: String,
+ required: false,
+ },
lineColor: {
type: String,
required: true,
@@ -18,6 +22,13 @@
required: true,
},
},
+ computed: {
+ strokeDashArray() {
+ if (this.lineStyle === 'dashed') return '3, 1';
+ if (this.lineStyle === 'dotted') return '1, 1';
+ return null;
+ },
+ },
};
</script>
<template>
@@ -34,6 +45,7 @@
:stroke="lineColor"
fill="none"
stroke-width="1"
+ :stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)">
</path>
</g>
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
index ee3c45efacc..ee866850e13 100644
--- a/app/assets/javascripts/monitoring/utils/measurements.js
+++ b/app/assets/javascripts/monitoring/utils/measurements.js
@@ -7,15 +7,16 @@ export default {
left: 40,
},
legends: {
- width: 10,
+ width: 15,
height: 3,
+ offsetX: 20,
+ offsetY: 32,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
- legendOffset: 33,
},
large: { // This covers both md and lg screen sizes
margin: {
@@ -27,13 +28,14 @@ export default {
legends: {
width: 15,
height: 3,
+ offsetX: 20,
+ offsetY: 34,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
- legendOffset: 36,
},
xTicks: 8,
yTicks: 3,
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 65eec0d8d02..d21a265bd43 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -11,7 +11,9 @@ const defaultColorPalette = {
const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
-export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
+const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
+
+function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = [];
function pickColor(name) {
@@ -31,17 +33,7 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
return defaultColorPalette[pick];
}
- const maxValues = queryData.result.map((timeSeries, index) => {
- const maxValue = d3.max(timeSeries.values.map(d => d.value));
- return {
- maxValue,
- index,
- };
- });
-
- const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
-
- return queryData.result.map((timeSeries, timeSeriesNumber) => {
+ return query.result.map((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
let areaColor = '';
@@ -52,9 +44,9 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]);
- timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
+ timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.time.minute, 60);
- timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
+ timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null;
@@ -72,10 +64,10 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
.y1(d => timeSeriesScaleY(d.value));
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
- const seriesCustomizationData = queryData.series != null &&
- _.findWhere(queryData.series[0].when,
- { value: timeSeriesMetricLabel });
- if (seriesCustomizationData != null) {
+ const seriesCustomizationData = query.series != null &&
+ _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
+
+ if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
@@ -83,14 +75,35 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
[lineColor, areaColor] = pickColor();
}
+ if (query.track) {
+ metricTag += ` - ${query.track}`;
+ }
+
return {
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
+ lineStyle,
lineColor,
areaColor,
metricTag,
};
});
}
+
+export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
+ const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
+ query.result.reduce((allResults, result) => allResults.concat(result.values), []),
+ ), []);
+
+ const xDom = d3.extent(allValues, d => d.time);
+ const yDom = [0, d3.max(allValues.map(d => d.value))];
+
+ return queries.reduce((series, query, index) => {
+ const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
+ return series.concat(
+ queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle),
+ );
+ }, []);
+}
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 5da2db063a4..1d496c64e53 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,85 +1,57 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api';
+import './lib/utils/url_utility';
-(function() {
- window.NamespaceSelect = (function() {
- function NamespaceSelect(opts) {
- this.onSelectItem = this.onSelectItem.bind(this);
- var fieldName, showAny;
- this.dropdown = opts.dropdown;
- showAny = true;
- fieldName = 'namespace_id';
- if (this.dropdown.attr('data-field-name')) {
- fieldName = this.dropdown.data('fieldName');
- }
- if (this.dropdown.attr('data-show-any')) {
- showAny = this.dropdown.data('showAny');
- }
- this.dropdown.glDropdown({
- filterable: true,
- selectable: true,
- filterRemote: true,
- search: {
- fields: ['path']
- },
- fieldName: fieldName,
- toggleLabel: function(selected) {
- if (selected.id == null) {
- return selected.text;
- } else {
- return selected.kind + ": " + selected.full_path;
- }
- },
- data: function(term, dataCallback) {
- return Api.namespaces(term, function(namespaces) {
- var anyNamespace;
- if (showAny) {
- anyNamespace = {
- text: 'Any namespace',
- id: null
- };
- namespaces.unshift(anyNamespace);
- namespaces.splice(1, 0, 'divider');
- }
- return dataCallback(namespaces);
- });
- },
- text: function(namespace) {
- if (namespace.id == null) {
- return namespace.text;
- } else {
- return namespace.kind + ": " + namespace.full_path;
- }
- },
- renderRow: this.renderRow,
- clicked: this.onSelectItem
- });
- }
-
- NamespaceSelect.prototype.onSelectItem = function(options) {
- const { e } = options;
- return e.preventDefault();
- };
+export default class NamespaceSelect {
+ constructor(opts) {
+ const isFilter = opts.dropdown.dataset.isFilter === 'true';
+ const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
- return NamespaceSelect;
- })();
-
- window.NamespaceSelects = (function() {
- function NamespaceSelects(opts) {
- var ref;
- if (opts == null) {
- opts = {};
- }
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select');
- this.$dropdowns.each(function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new window.NamespaceSelect({
- dropdown: $dropdown
+ $(opts.dropdown).glDropdown({
+ filterable: true,
+ selectable: true,
+ filterRemote: true,
+ search: {
+ fields: ['path']
+ },
+ fieldName: fieldName,
+ toggleLabel: function(selected) {
+ if (selected.id == null) {
+ return selected.text;
+ } else {
+ return selected.kind + ": " + selected.full_path;
+ }
+ },
+ data: function(term, dataCallback) {
+ return Api.namespaces(term, function(namespaces) {
+ if (isFilter) {
+ const anyNamespace = {
+ text: 'Any namespace',
+ id: null
+ };
+ namespaces.unshift(anyNamespace);
+ namespaces.splice(1, 0, 'divider');
+ }
+ return dataCallback(namespaces);
});
- });
- }
-
- return NamespaceSelects;
- })();
-}).call(window);
+ },
+ text: function(namespace) {
+ if (namespace.id == null) {
+ return namespace.text;
+ } else {
+ return namespace.kind + ": " + namespace.full_path;
+ }
+ },
+ renderRow: this.renderRow,
+ clicked(options) {
+ if (!isFilter) {
+ const { e } = options;
+ e.preventDefault();
+ }
+ },
+ url(namespace) {
+ return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 39fb302b644..77733b67c4d 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,97 +1,93 @@
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
-import RefSelectDropdown from '~/ref_select_dropdown';
+import RefSelectDropdown from './ref_select_dropdown';
-(function() {
- this.NewBranchForm = (function() {
- function NewBranchForm(form, availableRefs) {
- this.validate = this.validate.bind(this);
- this.branchNameError = form.find('.js-branch-name-error');
- this.name = form.find('.js-branch-name');
- this.ref = form.find('#ref');
- new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
- this.setupRestrictions();
- this.addBinding();
- this.init();
+export default class NewBranchForm {
+ constructor(form, availableRefs) {
+ this.validate = this.validate.bind(this);
+ this.branchNameError = form.find('.js-branch-name-error');
+ this.name = form.find('.js-branch-name');
+ this.ref = form.find('#ref');
+ new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
+ this.setupRestrictions();
+ this.addBinding();
+ this.init();
+ }
+
+ addBinding() {
+ return this.name.on('blur', this.validate);
+ }
+
+ init() {
+ if (this.name.length && this.name.val().length > 0) {
+ return this.name.trigger('blur');
}
+ }
- NewBranchForm.prototype.addBinding = function() {
- return this.name.on('blur', this.validate);
+ setupRestrictions() {
+ var endsWith, invalid, single, startsWith;
+ startsWith = {
+ pattern: /^(\/|\.)/g,
+ prefix: "can't start with",
+ conjunction: "or"
};
-
- NewBranchForm.prototype.init = function() {
- if (this.name.length && this.name.val().length > 0) {
- return this.name.trigger('blur');
- }
+ endsWith = {
+ pattern: /(\/|\.|\.lock)$/g,
+ prefix: "can't end in",
+ conjunction: "or"
};
-
- NewBranchForm.prototype.setupRestrictions = function() {
- var endsWith, invalid, single, startsWith;
- startsWith = {
- pattern: /^(\/|\.)/g,
- prefix: "can't start with",
- conjunction: "or"
- };
- endsWith = {
- pattern: /(\/|\.|\.lock)$/g,
- prefix: "can't end in",
- conjunction: "or"
- };
- invalid = {
- pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
- prefix: "can't contain",
- conjunction: ", "
- };
- single = {
- pattern: /^@+$/g,
- prefix: "can't be",
- conjunction: "or"
- };
- return this.restrictions = [startsWith, invalid, endsWith, single];
+ invalid = {
+ pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
+ prefix: "can't contain",
+ conjunction: ", "
+ };
+ single = {
+ pattern: /^@+$/g,
+ prefix: "can't be",
+ conjunction: "or"
};
+ return this.restrictions = [startsWith, invalid, endsWith, single];
+ }
- NewBranchForm.prototype.validate = function() {
- var errorMessage, errors, formatter, unique, validator;
- const indexOf = [].indexOf;
+ validate() {
+ var errorMessage, errors, formatter, unique, validator;
+ const indexOf = [].indexOf;
- this.branchNameError.empty();
- unique = function(values, value) {
- if (indexOf.call(values, value) === -1) {
- values.push(value);
- }
- return values;
- };
- formatter = function(values, restriction) {
- var formatted;
- formatted = values.map(function(value) {
- switch (false) {
- case !/\s/.test(value):
- return 'spaces';
- case !/\/{2,}/g.test(value):
- return 'consecutive slashes';
- default:
- return "'" + value + "'";
- }
- });
- return restriction.prefix + " " + (formatted.join(restriction.conjunction));
- };
- validator = (function(_this) {
- return function(errors, restriction) {
- var matched;
- matched = _this.name.val().match(restriction.pattern);
- if (matched) {
- return errors.concat(formatter(matched.reduce(unique, []), restriction));
- } else {
- return errors;
- }
- };
- })(this);
- errors = this.restrictions.reduce(validator, []);
- if (errors.length > 0) {
- errorMessage = $("<span/>").text(errors.join(', '));
- return this.branchNameError.append(errorMessage);
+ this.branchNameError.empty();
+ unique = function(values, value) {
+ if (indexOf.call(values, value) === -1) {
+ values.push(value);
}
+ return values;
};
-
- return NewBranchForm;
- })();
-}).call(window);
+ formatter = function(values, restriction) {
+ var formatted;
+ formatted = values.map(function(value) {
+ switch (false) {
+ case !/\s/.test(value):
+ return 'spaces';
+ case !/\/{2,}/g.test(value):
+ return 'consecutive slashes';
+ default:
+ return "'" + value + "'";
+ }
+ });
+ return restriction.prefix + " " + (formatted.join(restriction.conjunction));
+ };
+ validator = (function(_this) {
+ return function(errors, restriction) {
+ var matched;
+ matched = _this.name.val().match(restriction.pattern);
+ if (matched) {
+ return errors.concat(formatter(matched.reduce(unique, []), restriction));
+ } else {
+ return errors;
+ }
+ };
+ })(this);
+ errors = this.restrictions.reduce(validator, []);
+ if (errors.length > 0) {
+ errorMessage = $("<span/>").text(errors.join(', '));
+ return this.branchNameError.append(errorMessage);
+ }
+ }
+}
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 04073ef7270..6e152497d20 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,32 +1,28 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
-(function() {
- this.NewCommitForm = (function() {
- function NewCommitForm(form) {
- this.form = form;
- this.renderDestination = this.renderDestination.bind(this);
- this.branchName = form.find('.js-branch-name');
- this.originalBranch = form.find('.js-original-branch');
- this.createMergeRequest = form.find('.js-create-merge-request');
- this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
- this.branchName.keyup(this.renderDestination);
- this.renderDestination();
- }
+export default class NewCommitForm {
+ constructor(form) {
+ this.form = form;
+ this.renderDestination = this.renderDestination.bind(this);
+ this.branchName = form.find('.js-branch-name');
+ this.originalBranch = form.find('.js-original-branch');
+ this.createMergeRequest = form.find('.js-create-merge-request');
+ this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.branchName.keyup(this.renderDestination);
+ this.renderDestination();
+ }
- NewCommitForm.prototype.renderDestination = function() {
- var different;
- different = this.branchName.val() !== this.originalBranch.val();
- if (different) {
- this.createMergeRequestContainer.show();
- if (!this.wasDifferent) {
- this.createMergeRequest.prop('checked', true);
- }
- } else {
- this.createMergeRequestContainer.hide();
- this.createMergeRequest.prop('checked', false);
+ renderDestination() {
+ var different;
+ different = this.branchName.val() !== this.originalBranch.val();
+ if (different) {
+ this.createMergeRequestContainer.show();
+ if (!this.wasDifferent) {
+ this.createMergeRequest.prop('checked', true);
}
- return this.wasDifferent = different;
- };
-
- return NewCommitForm;
- })();
-}).call(window);
+ } else {
+ this.createMergeRequestContainer.hide();
+ this.createMergeRequest.prop('checked', false);
+ }
+ return this.wasDifferent = different;
+ }
+}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index ab101a56db8..e1ab28978e8 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -12,7 +12,7 @@ newline-per-chained-call, no-useless-escape, class-methods-use-this */
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
@@ -25,7 +25,7 @@ import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
-window.autosize = autosize;
+window.autosize = Autosize;
function normalizeNewlines(str) {
return str.replace(/\r\n/g, '\n');
@@ -413,8 +413,9 @@ export default class Notes {
return;
}
this.note_ids.push(noteEntity.id);
+
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
- row = form.closest('tr');
+ row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`);
if (noteEntity.on_image) {
row = form;
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index ad384a1cc36..30e02554b65 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
- import autosize from 'vendor/autosize';
+ import Autosize from 'autosize';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
@@ -219,7 +219,7 @@
},
resizeTextarea() {
this.$nextTick(() => {
- autosize.update(this.$refs.textarea);
+ Autosize.update(this.$refs.textarea);
});
},
},
@@ -357,7 +357,8 @@
@click="handleSave(true)"
v-if="canUpdateIssue"
:class="actionButtonClassNames"
- class="btn btn-comment btn-comment-and-close">
+ :disabled="isSubmitting"
+ class="btn btn-comment btn-comment-and-close js-action-button">
{{issueActionButtonTitle}}
</button>
<button
diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
index e73ec2aaf71..64466b04b40 100644
--- a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
@@ -1,18 +1,21 @@
<script>
+ import Icon from '../../vue_shared/components/icon.vue';
+
export default {
- computed: {
- lockIcon() {
- return gl.utils.spriteIcon('lock');
- },
+ component: {
+ Icon,
},
};
-
</script>
<template>
<div class="disabled-comment text-center">
- <span class="issuable-note-warning">
- <span class="icon" v-html="lockIcon"></span>
+ <span class="issuable-note-warning inline">
+ <icon
+ name="lock"
+ :size="16"
+ class="icon">
+ </icon>
<span>This issue is locked. Only <b>project members</b> can comment.</span>
</span>
</div>
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
index 0ddbd672bed..40318f9a600 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -122,7 +122,9 @@
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
+ if (this.$refs.noteBody) {
+ this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
+ }
},
},
created() {
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 54227425d2a..19d8e1f49cf 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,7 +1,7 @@
<script>
- import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltip from '../../../vue_shared/directives/tooltip';
-
+ import icon from '../../../vue_shared/components/icon.vue';
+ import { dasherize } from '../../../lib/utils/text_utility';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
@@ -29,17 +29,18 @@
},
},
+ components: {
+ icon,
+ },
+
directives: {
tooltip,
},
computed: {
- actionIconSvg() {
- return getActionIcon(this.actionIcon);
- },
-
cssClass() {
- return `js-${gl.text.dasherize(this.actionIcon)}`;
+ const actionIconDash = dasherize(this.actionIcon);
+ return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
};
@@ -50,14 +51,9 @@
:data-method="actionMethod"
:title="tooltipText"
:href="link"
- class="ci-action-icon-container"
+ class="ci-action-icon-container ci-action-icon-wrapper"
+ :class="cssClass"
data-container="body">
-
- <i
- class="ci-action-icon-wrapper"
- :class="cssClass"
- v-html="actionIconSvg"
- aria-hidden="true"
- />
+ <icon :name="actionIcon"/>
</a>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
index 18fe1847eef..1c0944d45fc 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
@@ -1,5 +1,5 @@
<script>
- import getActionIcon from '../../../vue_shared/ci_action_icons';
+ import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
@@ -29,14 +29,12 @@
},
},
- directives: {
- tooltip,
+ components: {
+ icon,
},
- computed: {
- actionIconSvg() {
- return getActionIcon(this.actionIcon);
- },
+ directives: {
+ tooltip,
},
};
</script>
@@ -49,7 +47,7 @@
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
- v-html="actionIconSvg"
aria-label="Job's action">
+ <icon :name="actionIcon"/>
</a>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 3e5d6d15909..7006d05e7b2 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -18,7 +18,7 @@
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
- * "icon": "icon_action_retry",
+ * "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 3933509a6f4..5dea4555515 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -19,7 +19,7 @@
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
- * "icon": "icon_action_retry",
+ * "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
index 73f7e3a0cad..07befd23500 100644
--- a/app/assets/javascripts/pipelines/components/navigation_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
@@ -2,16 +2,8 @@
export default {
name: 'PipelineNavigationTabs',
props: {
- scope: {
- type: String,
- required: true,
- },
- count: {
- type: Object,
- required: true,
- },
- paths: {
- type: Object,
+ tabs: {
+ type: Array,
required: true,
},
},
@@ -23,68 +15,37 @@
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
+
+ onTabClick(tab) {
+ this.$emit('onChangeTab', tab.scope);
+ },
},
};
</script>
<template>
<ul class="nav-links scrolling-tabs">
<li
- class="js-pipelines-tab-all"
- :class="{ active: scope === 'all'}">
- <a :href="paths.allPath">
- All
- <span
- v-if="shouldRenderBadge(count.all)"
- class="badge js-totalbuilds-count">
- {{count.all}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-pending"
- :class="{ active: scope === 'pending'}">
- <a :href="paths.pendingPath">
- Pending
- <span
- v-if="shouldRenderBadge(count.pending)"
- class="badge">
- {{count.pending}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-running"
- :class="{ active: scope === 'running'}">
- <a :href="paths.runningPath">
- Running
- <span
- v-if="shouldRenderBadge(count.running)"
- class="badge">
- {{count.running}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-finished"
- :class="{ active: scope === 'finished'}">
- <a :href="paths.finishedPath">
- Finished
+ v-for="(tab, i) in tabs"
+ :key="i"
+ :class="{
+ active: tab.isActive,
+ }"
+ >
+ <a
+ role="button"
+ @click="onTabClick(tab)"
+ :class="`js-pipelines-tab-${tab.scope}`"
+ >
+ {{ tab.name }}
+
<span
- v-if="shouldRenderBadge(count.finished)"
- class="badge">
- {{count.finished}}
+ v-if="shouldRenderBadge(tab.count)"
+ class="badge"
+ >
+ {{tab.count}}
</span>
+
</a>
</li>
- <li
- class="js-pipelines-tab-branches"
- :class="{ active: scope === 'branches'}">
- <a :href="paths.branchesPath">Branches</a>
- </li>
- <li
- class="js-pipelines-tab-tags"
- :class="{ active: scope === 'tags'}">
- <a :href="paths.tagsPath">Tags</a>
- </li>
</ul>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 3da60e88474..233be8a49c8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,10 +1,17 @@
<script>
+ import _ from 'underscore';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
- import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
+ import {
+ convertPermissionToBoolean,
+ getParameterByName,
+ historyPushState,
+ buildUrlWithCurrentLocation,
+ parseQueryStringIntoObject,
+ } from '../../lib/utils/common_utils';
export default {
props: {
@@ -41,27 +48,18 @@
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
- allPath: pipelinesData.allPath,
- pendingPath: pipelinesData.pendingPath,
- runningPath: pipelinesData.runningPath,
- finishedPath: pipelinesData.finishedPath,
- branchesPath: pipelinesData.branchesPath,
- tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
- apiScope: 'all',
- pagenum: 1,
+ scope: getParameterByName('scope') || 'all',
+ page: getParameterByName('page') || '1',
+ requestData: {},
};
},
computed: {
canCreatePipelineParsed() {
return convertPermissionToBoolean(this.canCreatePipeline);
},
- scope() {
- const scope = getParameterByName('scope');
- return scope === null ? 'all' : scope;
- },
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
@@ -106,46 +104,112 @@
hasCiEnabled() {
return this.hasCi !== undefined;
},
- paths() {
- return {
- allPath: this.allPath,
- pendingPath: this.pendingPath,
- finishedPath: this.finishedPath,
- runningPath: this.runningPath,
- branchesPath: this.branchesPath,
- tagsPath: this.tagsPath,
- };
- },
- pageParameter() {
- return getParameterByName('page') || this.pagenum;
- },
- scopeParameter() {
- return getParameterByName('scope') || this.apiScope;
+
+ tabs() {
+ const { count } = this.state;
+ return [
+ {
+ name: 'All',
+ scope: 'all',
+ count: count.all,
+ isActive: this.scope === 'all',
+ },
+ {
+ name: 'Pending',
+ scope: 'pending',
+ count: count.pending,
+ isActive: this.scope === 'pending',
+ },
+ {
+ name: 'Running',
+ scope: 'running',
+ count: count.running,
+ isActive: this.scope === 'running',
+ },
+ {
+ name: 'Finished',
+ scope: 'finished',
+ count: count.finished,
+ isActive: this.scope === 'finished',
+ },
+ {
+ name: 'Branches',
+ scope: 'branches',
+ isActive: this.scope === 'branches',
+ },
+ {
+ name: 'Tags',
+ scope: 'tags',
+ isActive: this.scope === 'tags',
+ },
+ ];
},
},
created() {
this.service = new PipelinesService(this.endpoint);
- this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
+ this.requestData = { page: this.page, scope: this.scope };
},
methods: {
+ successCallback(resp) {
+ return resp.json().then((response) => {
+ // Because we are polling & the user is interacting verify if the response received
+ // matches the last request made
+ if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
+ this.store.storeCount(response.count);
+ this.store.storePagination(resp.headers);
+ this.setCommonData(response.pipelines);
+ }
+ });
+ },
/**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
*/
- change(pageNumber) {
- const param = setParamInURL('page', pageNumber);
+ updateContent(parameters) {
+ // stop polling
+ this.poll.stop();
+
+ const queryString = Object.keys(parameters).map((parameter) => {
+ const value = parameters[parameter];
+ // update internal state for UI
+ this[parameter] = value;
+ return `${parameter}=${encodeURIComponent(value)}`;
+ }).join('&');
- gl.utils.visitUrl(param);
- return param;
+ // update polling parameters
+ this.requestData = parameters;
+
+ historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
+
+ this.isLoading = true;
+ // fetch new data
+ return this.service.getPipelines(this.requestData)
+ .then((response) => {
+ this.isLoading = false;
+ this.successCallback(response);
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart();
+ });
},
- successCallback(resp) {
- return resp.json().then((response) => {
- this.store.storeCount(response.count);
- this.store.storePagination(resp.headers);
- this.setCommonData(response.pipelines);
- });
+ onChangeTab(scope) {
+ this.updateContent({ scope, page: '1' });
+ },
+ onChangePage(page) {
+ /* URLS parameters are strings, we need to parse to match types */
+ this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
},
};
@@ -154,7 +218,7 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
- v-if="!isLoading && !shouldRenderEmptyState">
+ v-if="!shouldRenderEmptyState">
<div class="fade-left">
<i
class="fa fa-angle-left"
@@ -167,17 +231,17 @@
aria-hidden="true">
</i>
</div>
+
<navigation-tabs
- :scope="scope"
- :count="state.count"
- :paths="paths"
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
/>
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
- :ciLintPath="ciLintPath"
+ :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
</div>
@@ -188,6 +252,7 @@
label="Loading Pipelines"
size="3"
v-if="isLoading"
+ class="prepend-top-20"
/>
<empty-state
@@ -202,9 +267,11 @@
/>
<div
- class="blank-state blank-state-no-icon"
+ class="blank-state-row"
v-if="shouldRenderNoPipelinesMessage">
- <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
</div>
<div
@@ -221,8 +288,8 @@
<table-pagination
v-if="shouldRenderPagination"
- :change="change"
- :pageInfo="state.pageInfo"
+ :change="onChangePage"
+ :page-info="state.pageInfo"
/>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 1a7a5c2a415..ac9d9c901ca 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -14,7 +14,7 @@
*/
import Flash from '../../flash';
-import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
+import icon from '../../vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -45,6 +45,7 @@ export default {
components: {
loadingIcon,
+ icon,
},
updated() {
@@ -122,8 +123,8 @@ export default {
return `ci-status-icon-${this.stage.status.group}`;
},
- svgIcon() {
- return borderlessStatusIconEntityMap[this.stage.status.icon];
+ borderlessIcon() {
+ return `${this.stage.status.icon}_borderless`;
},
},
};
@@ -145,9 +146,10 @@ export default {
aria-expanded="false">
<span
- v-html="svgIcon"
aria-hidden="true"
:aria-label="stage.title">
+ <icon
+ :name="borderlessIcon"/>
</span>
<i
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index b2b34cb83e1..6348a2e331d 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
@toggle="toggleOpen"
@submit="onSubmit">
- <template slot="body" scope="props">
+ <template slot="body" slot-scope="props">
<p v-html="props.text"></p>
<form
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index fe6602259e2..36b6a5ed376 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,139 +1,131 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
-/* global ProjectSelect */
+/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie';
+import projectSelect from './project_select';
-(function() {
- this.Project = (function() {
- function Project() {
- const $cloneOptions = $('ul.clone-options-dropdown');
- const $projectCloneField = $('#project_clone');
- const $cloneBtnText = $('a.clone-dropdown-btn span');
+export default class Project {
+ constructor() {
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnText = $('a.clone-dropdown-btn span');
- const selectedCloneOption = $cloneBtnText.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
-
- $('a', $cloneOptions).on('click', (e) => {
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
-
- e.preventDefault();
-
- $('.is-active', $cloneOptions).not($this).removeClass('is-active');
- $this.toggleClass('is-active');
- $projectCloneField.val(url);
- $cloneBtnText.text($this.text());
-
- return $('.clone').text(url);
- });
- // Ref switcher
- this.initRefSwitcher();
- $('.project-refs-select').on('change', function() {
- return $(this).parents('form').submit();
- });
- $('.hide-no-ssh-message').on('click', function(e) {
- Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
- return e.preventDefault();
- });
- $('.hide-no-password-message').on('click', function(e) {
- Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
- return e.preventDefault();
- });
- this.projectSelectDropdown();
+ const selectedCloneOption = $cloneBtnText.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- Project.prototype.projectSelectDropdown = function() {
- new ProjectSelect();
- $('.project-item-select').on('click', (function(_this) {
- return function(e) {
- return _this.changeProject($(e.currentTarget).val());
- };
- })(this));
- };
-
- Project.prototype.changeProject = function(url) {
- return window.location = url;
- };
-
- Project.prototype.initRefSwitcher = function() {
- var refListItem = document.createElement('li');
- var refLink = document.createElement('a');
-
- refLink.href = '#';
-
- return $('.js-project-refs-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
- return $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
- ref: $dropdown.data('ref'),
- search: term
- },
- dataType: "json"
- }).done(function(refs) {
- return callback(refs);
- });
- },
- selectable: true,
- filterable: true,
- filterRemote: true,
- filterByText: true,
- inputFieldName: $dropdown.data('input-field-name'),
- fieldName: $dropdown.data('field-name'),
- renderRow: function(ref) {
- var li = refListItem.cloneNode(false);
-
- if (ref.header != null) {
- li.className = 'dropdown-header';
- li.textContent = ref.header;
- } else {
- var link = refLink.cloneNode(false);
-
- if (ref === selected) {
- link.className = 'is-active';
- }
-
- link.textContent = ref;
- link.dataset.ref = ref;
-
- li.appendChild(link);
+ $('a', $cloneOptions).on('click', (e) => {
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+
+ e.preventDefault();
+
+ $('.is-active', $cloneOptions).not($this).removeClass('is-active');
+ $this.toggleClass('is-active');
+ $projectCloneField.val(url);
+ $cloneBtnText.text($this.text());
+
+ return $('.clone').text(url);
+ });
+ // Ref switcher
+ Project.initRefSwitcher();
+ $('.project-refs-select').on('change', function() {
+ return $(this).parents('form').submit();
+ });
+ $('.hide-no-ssh-message').on('click', function(e) {
+ Cookies.set('hide_no_ssh_message', 'false');
+ $(this).parents('.no-ssh-key-message').remove();
+ return e.preventDefault();
+ });
+ $('.hide-no-password-message').on('click', function(e) {
+ Cookies.set('hide_no_password_message', 'false');
+ $(this).parents('.no-password-message').remove();
+ return e.preventDefault();
+ });
+ Project.projectSelectDropdown();
+ }
+
+ static projectSelectDropdown () {
+ projectSelect();
+ $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
+ }
+
+ static changeProject(url) {
+ return window.location = url;
+ }
+
+ static initRefSwitcher() {
+ var refListItem = document.createElement('li');
+ var refLink = document.createElement('a');
+
+ refLink.href = '#';
+
+ return $('.js-project-refs-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref'),
+ search: term,
+ },
+ dataType: 'json',
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ filterByText: true,
+ inputFieldName: $dropdown.data('input-field-name'),
+ fieldName: $dropdown.data('field-name'),
+ renderRow: function(ref) {
+ var li = refListItem.cloneNode(false);
+
+ if (ref.header != null) {
+ li.className = 'dropdown-header';
+ li.textContent = ref.header;
+ } else {
+ var link = refLink.cloneNode(false);
+
+ if (ref === selected) {
+ link.className = 'is-active';
}
- return li;
- },
- id: function(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(options) {
- const { e } = options;
- e.preventDefault();
- if ($('input[name="ref"]').length) {
- var $form = $dropdown.closest('form');
-
- var $visit = $dropdown.data('visit');
- var shouldVisit = $visit ? true : $visit;
- var action = $form.attr('action');
- var divider = action.indexOf('?') === -1 ? '?' : '&';
- if (shouldVisit) {
- gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
- }
+ link.textContent = ref;
+ link.dataset.ref = ref;
+
+ li.appendChild(link);
+ }
+
+ return li;
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(options) {
+ const { e } = options;
+ e.preventDefault();
+ if ($('input[name="ref"]').length) {
+ var $form = $dropdown.closest('form');
+
+ var $visit = $dropdown.data('visit');
+ var shouldVisit = $visit ? true : $visit;
+ var action = $form.attr('action');
+ var divider = action.indexOf('?') === -1 ? '?' : '&';
+ if (shouldVisit) {
+ gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
- });
+ },
});
- };
-
- return Project;
- })();
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index aabdfbf65e2..56627aa155c 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,20 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-(function() {
- this.ProjectAvatar = (function() {
- function ProjectAvatar() {
- $('.js-choose-project-avatar-button').bind('click', function() {
- var form;
- form = $(this).closest('form');
- return form.find('.js-project-avatar-input').click();
- });
- $('.js-project-avatar-input').bind('change', function() {
- var filename, form;
- form = $(this).closest('form');
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find('.js-avatar-filename').text(filename);
- });
- }
+export default function projectAvatar() {
+ $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
+ const form = $(this).closest('form');
+ return form.find('.js-project-avatar-input').click();
+ });
- return ProjectAvatar;
- })();
-}).call(window);
+ $('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
+ const form = $(this).closest('form');
+ // eslint-disable-next-line no-useless-escape
+ const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-avatar-filename').text(filename);
+ });
+}
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 11f9754780d..19682b20a4a 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
-/* global fuzzaldrinPlus */
+
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
(function() {
this.ProjectFindFile = (function() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 08334bf1ec5..d2d26d6f67e 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,13 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
+import { visitUrl } from './lib/utils/url_utility';
-(function() {
- this.ProjectImport = (function() {
- function ProjectImport() {
- setTimeout(function() {
- return gl.utils.visitUrl(location.href);
- }, 5000);
- }
+export default function projectImport() {
+ setTimeout(() => {
+ visitUrl(location.href);
+ }, 5000);
+}
- return ProjectImport;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 0a811627600..b65521b278f 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,55 +1,50 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
+export default class ProjectLabelSubscription {
+ constructor(container) {
+ this.$container = $(container);
+ this.$buttons = this.$container.find('.js-subscribe-button');
-(function(global) {
- class ProjectLabelSubscription {
- constructor(container) {
- this.$container = $(container);
- this.$buttons = this.$container.find('.js-subscribe-button');
-
- this.$buttons.on('click', this.toggleSubscription.bind(this));
- }
+ this.$buttons.on('click', this.toggleSubscription.bind(this));
+ }
- toggleSubscription(event) {
- event.preventDefault();
+ toggleSubscription(event) {
+ event.preventDefault();
- const $btn = $(event.currentTarget);
- const $span = $btn.find('span');
- const url = $btn.attr('data-url');
- const oldStatus = $btn.attr('data-status');
+ const $btn = $(event.currentTarget);
+ const $span = $btn.find('span');
+ const url = $btn.attr('data-url');
+ const oldStatus = $btn.attr('data-status');
- $btn.addClass('disabled');
- $span.toggleClass('hidden');
+ $btn.addClass('disabled');
+ $span.toggleClass('hidden');
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- let newStatus, newAction;
+ $.ajax({
+ type: 'POST',
+ url,
+ }).done(() => {
+ let newStatus;
+ let newAction;
- if (oldStatus === 'unsubscribed') {
- [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
- } else {
- [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
- }
+ if (oldStatus === 'unsubscribed') {
+ [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ } else {
+ [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ }
- $span.toggleClass('hidden');
- $btn.removeClass('disabled');
+ $span.toggleClass('hidden');
+ $btn.removeClass('disabled');
- this.$buttons.attr('data-status', newStatus);
- this.$buttons.find('> span').text(newAction);
+ this.$buttons.attr('data-status', newStatus);
+ this.$buttons.find('> span').text(newAction);
- this.$buttons.map((button) => {
- const $button = $(button);
+ this.$buttons.map((button) => {
+ const $button = $(button);
- if ($button.attr('data-original-title')) {
- $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
- }
+ if ($button.attr('data-original-title')) {
+ $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ }
- return button;
- });
+ return button;
});
- }
+ });
}
-
- global.ProjectLabelSubscription = ProjectLabelSubscription;
-})(window.gl || (window.gl = {}));
+}
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index fd89a1a85c3..ca548d011b6 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
import VisibilitySelect from './visibility_select';
@@ -7,153 +7,145 @@ function highlightChanges($elm) {
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
-(function() {
- this.ProjectNew = (function() {
- function ProjectNew() {
- this.toggleSettings = this.toggleSettings.bind(this);
- this.$selects = $('.features select');
- this.$repoSelects = this.$selects.filter('.js-repo-select');
- this.$projectSelects = this.$selects.not('.js-repo-select');
-
- $('.project-edit-container').on('ajax:before', (function(_this) {
- return function() {
- $('.project-edit-container').hide();
- return $('.save-project-loader').show();
- };
- })(this));
-
- this.initVisibilitySelect();
-
- this.toggleSettings();
- this.toggleSettingsOnclick();
- this.toggleRepoVisibility();
- }
-
- ProjectNew.prototype.initVisibilitySelect = function() {
- const visibilityContainer = document.querySelector('.js-visibility-select');
- if (!visibilityContainer) return;
- const visibilitySelect = new VisibilitySelect(visibilityContainer);
- visibilitySelect.init();
-
- const $visibilitySelect = $(visibilityContainer).find('select');
- let projectVisibility = $visibilitySelect.val();
- const PROJECT_VISIBILITY_PRIVATE = '0';
-
- $visibilitySelect.on('change', () => {
- const newProjectVisibility = $visibilitySelect.val();
-
- if (projectVisibility !== newProjectVisibility) {
- this.$projectSelects.each((idx, select) => {
- const $select = $(select);
- const $options = $select.find('option');
- const values = $.map($options, e => e.value);
-
- // if switched to "private", limit visibility options
- if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- if ($select.val() !== values[0] && $select.val() !== values[1]) {
- $select.val(values[1]).trigger('change');
- highlightChanges($select);
- }
- $options.slice(2).disable();
+export default class ProjectNew {
+ constructor() {
+ this.toggleSettings = this.toggleSettings.bind(this);
+ this.$selects = $('.features select');
+ this.$repoSelects = this.$selects.filter('.js-repo-select');
+ this.$projectSelects = this.$selects.not('.js-repo-select');
+
+ $('.project-edit-container').on('ajax:before', () => {
+ $('.project-edit-container').hide();
+ return $('.save-project-loader').show();
+ });
+
+ this.initVisibilitySelect();
+
+ this.toggleSettings();
+ this.toggleSettingsOnclick();
+ this.toggleRepoVisibility();
+ }
+
+ initVisibilitySelect() {
+ const visibilityContainer = document.querySelector('.js-visibility-select');
+ if (!visibilityContainer) return;
+ const visibilitySelect = new VisibilitySelect(visibilityContainer);
+ visibilitySelect.init();
+
+ const $visibilitySelect = $(visibilityContainer).find('select');
+ let projectVisibility = $visibilitySelect.val();
+ const PROJECT_VISIBILITY_PRIVATE = '0';
+
+ $visibilitySelect.on('change', () => {
+ const newProjectVisibility = $visibilitySelect.val();
+
+ if (projectVisibility !== newProjectVisibility) {
+ this.$projectSelects.each((idx, select) => {
+ const $select = $(select);
+ const $options = $select.find('option');
+ const values = $.map($options, e => e.value);
+
+ // if switched to "private", limit visibility options
+ if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ if ($select.val() !== values[0] && $select.val() !== values[1]) {
+ $select.val(values[1]).trigger('change');
+ highlightChanges($select);
}
+ $options.slice(2).disable();
+ }
- // if switched from "private", increase visibility for non-disabled options
- if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- $options.enable();
- if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
- $select.val(values[values.length - 1]).trigger('change');
- highlightChanges($select);
- }
+ // if switched from "private", increase visibility for non-disabled options
+ if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ $options.enable();
+ if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
+ $select.val(values[values.length - 1]).trigger('change');
+ highlightChanges($select);
}
- });
+ }
+ });
- projectVisibility = newProjectVisibility;
- }
- });
- };
-
- ProjectNew.prototype.toggleSettings = function() {
- var self = this;
-
- this.$selects.each(function () {
- var $select = $(this);
- var className = $select.data('field')
- .replace(/_/g, '-')
- .replace('access-level', 'feature');
- self._showOrHide($select, '.' + className);
- });
- };
-
- ProjectNew.prototype.toggleSettingsOnclick = function() {
- this.$selects.on('change', this.toggleSettings);
- };
-
- ProjectNew.prototype._showOrHide = function(checkElement, container) {
- var $container = $(container);
-
- if ($(checkElement).val() !== '0') {
- return $container.show();
- } else {
- return $container.hide();
+ projectVisibility = newProjectVisibility;
}
- };
-
- ProjectNew.prototype.toggleRepoVisibility = function () {
- var $repoAccessLevel = $('.js-repo-access-level select');
- var $lfsEnabledOption = $('.js-lfs-enabled select');
- var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
- var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
- var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
- .nextAll()
- .hide();
-
- $repoAccessLevel.off('change')
- .on('change', function () {
- var selectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.each(function () {
- var $this = $(this);
- var repoSelectVal = parseInt($this.val(), 10);
-
- $this.find('option').enable();
-
- if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
- $this.val(selectedVal).trigger('change');
- highlightChanges($this);
- }
-
- $this.find("option[value='" + selectedVal + "']").nextAll().disable();
- });
+ });
+ }
+
+ toggleSettings() {
+ this.$selects.each(function () {
+ var $select = $(this);
+ var className = $select.data('field')
+ .replace(/_/g, '-')
+ .replace('access-level', 'feature');
+ ProjectNew._showOrHide($select, '.' + className);
+ });
+ }
+
+ toggleSettingsOnclick() {
+ this.$selects.on('change', this.toggleSettings);
+ }
+
+ static _showOrHide(checkElement, container) {
+ const $container = $(container);
+
+ if ($(checkElement).val() !== '0') {
+ return $container.show();
+ }
+ return $container.hide();
+ }
+
+ toggleRepoVisibility() {
+ var $repoAccessLevel = $('.js-repo-access-level select');
+ var $lfsEnabledOption = $('.js-lfs-enabled select');
+ var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
+ var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+ var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
+
+ this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
+ .nextAll()
+ .hide();
+
+ $repoAccessLevel
+ .off('change')
+ .on('change', function () {
+ var selectedVal = parseInt($repoAccessLevel.val(), 10);
+
+ this.$repoSelects.each(function () {
+ var $this = $(this);
+ var repoSelectVal = parseInt($this.val(), 10);
+
+ $this.find('option').enable();
+
+ if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
+ $this.val(selectedVal).trigger('change');
+ highlightChanges($this);
+ }
- if (selectedVal) {
- this.$repoSelects.removeClass('disabled');
+ $this.find("option[value='" + selectedVal + "']").nextAll().disable();
+ });
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.removeClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = '';
- }
- } else {
- this.$repoSelects.addClass('disabled');
+ if (selectedVal) {
+ this.$repoSelects.removeClass('disabled');
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.val('false').addClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = 'none';
- containerRegistryCheckbox.checked = false;
- }
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.removeClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
+ if (containerRegistry) {
+ containerRegistry.style.display = '';
}
+ } else {
+ this.$repoSelects.addClass('disabled');
- prevSelectedVal = selectedVal;
- }.bind(this));
- };
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.val('false').addClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
+ if (containerRegistry) {
+ containerRegistry.style.display = 'none';
+ containerRegistryCheckbox.checked = false;
+ }
+ }
- return ProjectNew;
- })();
-}).call(window);
+ prevSelectedVal = selectedVal;
+ }.bind(this));
+ }
+}
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index bffc85e6315..07a49d1506c 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -2,79 +2,73 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
-(function () {
- this.ProjectSelect = (function () {
- function ProjectSelect() {
- $('.ajax-project-select').each(function(i, select) {
- var placeholder;
- const simpleFilter = $(select).data('simple-filter') || false;
- this.groupId = $(select).data('group-id');
- this.includeGroups = $(select).data('include-groups');
- this.allProjects = $(select).data('all-projects') || false;
- this.orderBy = $(select).data('order-by') || 'id';
- this.withIssuesEnabled = $(select).data('with-issues-enabled');
- this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
+export default function projectSelect() {
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ const simpleFilter = $(select).data('simple-filter') || false;
+ this.groupId = $(select).data('group-id');
+ this.includeGroups = $(select).data('include-groups');
+ this.allProjects = $(select).data('all-projects') || false;
+ this.orderBy = $(select).data('order-by') || 'id';
+ this.withIssuesEnabled = $(select).data('with-issues-enabled');
+ this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
- placeholder = "Search for project";
- if (this.includeGroups) {
- placeholder += " or group";
- }
+ placeholder = "Search for project";
+ if (this.includeGroups) {
+ placeholder += " or group";
+ }
- $(select).select2({
- placeholder: placeholder,
- minimumInputLength: 0,
- query: (function (_this) {
- return function (query) {
- var finalCallback, projectsCallback;
- finalCallback = function (projects) {
+ $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function (_this) {
+ return function (query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function (projects) {
+ var data;
+ data = {
+ results: projects
+ };
+ return query.callback(data);
+ };
+ if (_this.includeGroups) {
+ projectsCallback = function (projects) {
+ var groupsCallback;
+ groupsCallback = function (groups) {
var data;
- data = {
- results: projects
- };
- return query.callback(data);
+ data = groups.concat(projects);
+ return finalCallback(data);
};
- if (_this.includeGroups) {
- projectsCallback = function (projects) {
- var groupsCallback;
- groupsCallback = function (groups) {
- var data;
- data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(query.term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(_this.groupId, query.term, projectsCallback);
- } else {
- return Api.projects(query.term, {
- order_by: _this.orderBy,
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- membership: !_this.allProjects,
- }, projectsCallback);
- }
+ return Api.groups(query.term, {}, groupsCallback);
};
- })(this),
- id: function(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text: function (project) {
- return project.name_with_namespace || project.name;
- },
- dropdownCssClass: "ajax-project-dropdown"
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(_this.groupId, query.term, projectsCallback);
+ } else {
+ return Api.projects(query.term, {
+ order_by: _this.orderBy,
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
+ }, projectsCallback);
+ }
+ };
+ })(this),
+ id: function(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
});
- if (simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
- }
-
- return ProjectSelect;
- })();
-}).call(window);
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ dropdownCssClass: "ajax-project-dropdown"
+ });
+ if (simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+}
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
deleted file mode 100644
index 3a51c1f26ac..00000000000
--- a/app/assets/javascripts/project_show.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife */
-
-(function() {
- this.ProjectShow = (function() {
- function ProjectShow() {}
-
- return ProjectShow;
- })();
-}).call(window);
-
-// I kept class for future
diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js
index 4ee2e49306d..567c311f119 100644
--- a/app/assets/javascripts/project_variables.js
+++ b/app/assets/javascripts/project_variables.js
@@ -1,43 +1,39 @@
-(() => {
- const HIDDEN_VALUE_TEXT = '******';
- class ProjectVariables {
- constructor() {
- this.$revealBtn = $('.js-btn-toggle-reveal-values');
- this.$revealBtn.on('click', this.toggleRevealState.bind(this));
- }
+const HIDDEN_VALUE_TEXT = '******';
+
+export default class ProjectVariables {
+ constructor() {
+ this.$revealBtn = $('.js-btn-toggle-reveal-values');
+ this.$revealBtn.on('click', this.toggleRevealState.bind(this));
+ }
- toggleRevealState(e) {
- e.preventDefault();
+ toggleRevealState(e) {
+ e.preventDefault();
- const oldStatus = this.$revealBtn.attr('data-status');
- let newStatus = 'hidden';
- let newAction = 'Reveal Values';
+ const oldStatus = this.$revealBtn.attr('data-status');
+ let newStatus = 'hidden';
+ let newAction = 'Reveal Values';
- if (oldStatus === 'hidden') {
- newStatus = 'revealed';
- newAction = 'Hide Values';
- }
+ if (oldStatus === 'hidden') {
+ newStatus = 'revealed';
+ newAction = 'Hide Values';
+ }
- this.$revealBtn.attr('data-status', newStatus);
+ this.$revealBtn.attr('data-status', newStatus);
- const $variables = $('.variable-value');
+ const $variables = $('.variable-value');
- $variables.each((_, variable) => {
- const $variable = $(variable);
- let newText = HIDDEN_VALUE_TEXT;
+ $variables.each((_, variable) => {
+ const $variable = $(variable);
+ let newText = HIDDEN_VALUE_TEXT;
- if (newStatus === 'revealed') {
- newText = $variable.attr('data-value');
- }
+ if (newStatus === 'revealed') {
+ newText = $variable.attr('data-value');
+ }
- $variable.text(newText);
- });
+ $variable.text(newText);
+ });
- this.$revealBtn.text(newAction);
- }
+ this.$revealBtn.text(newAction);
}
-
- window.gl = window.gl || {};
- window.gl.ProjectVariables = ProjectVariables;
-})();
+}
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index e917279947e..14d43e135fe 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -8,6 +8,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
+ import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
props: {
@@ -41,6 +42,10 @@
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
@@ -97,7 +102,7 @@
</span>
</td>
<td>
- {{item.size}}
+ {{formatSize(item.size)}}
<template v-if="item.size && item.layers">
&middot;
</template>
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index bcdc0fd67b8..bf6fc0ec305 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -2,12 +2,13 @@
// Render Gitlab flavoured Markdown
//
-// Delegates to syntax highlight and render math
+// Delegates to syntax highlight and render math & mermaid diagrams.
//
(function() {
$.fn.renderGFM = function() {
this.find('.js-syntax-highlight').syntaxHighlight();
this.find('.js-render-math').renderMath();
+ this.find('.js-render-mermaid').renderMermaid();
return this;
};
diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js
new file mode 100644
index 00000000000..a253601f8e8
--- /dev/null
+++ b/app/assets/javascripts/render_mermaid.js
@@ -0,0 +1,30 @@
+// Renders diagrams and flowcharts from text using Mermaid in any element with the
+// `js-render-mermaid` class.
+//
+// Example markup:
+//
+// <pre class="js-render-mermaid">
+// graph TD;
+// A-- > B;
+// A-- > C;
+// B-- > D;
+// C-- > D;
+// </pre>
+//
+
+import Flash from './flash';
+
+$.fn.renderMermaid = function renderMermaid() {
+ if (this.length === 0) return;
+
+ import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => {
+ mermaid.initialize({
+ loadOnStart: false,
+ theme: 'neutral',
+ });
+
+ mermaid.init(undefined, this);
+ }).catch((err) => {
+ Flash(`Can't load mermaid module: ${err}`);
+ });
+};
diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/repo/components/new_branch_form.vue
index eac43e692b0..ba7090e4a9d 100644
--- a/app/assets/javascripts/repo/components/new_branch_form.vue
+++ b/app/assets/javascripts/repo/components/new_branch_form.vue
@@ -1,18 +1,12 @@
<script>
+ import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import eventHub from '../event_hub';
export default {
components: {
loadingIcon,
},
- props: {
- currentBranch: {
- type: String,
- required: true,
- },
- },
data() {
return {
branchName: '',
@@ -20,11 +14,17 @@
};
},
computed: {
+ ...mapState([
+ 'currentBranch',
+ ]),
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
+ ...mapActions([
+ 'createNewBranch',
+ ]),
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
@@ -38,19 +38,21 @@
hideFlash(flashEl, false);
}
- eventHub.$emit('createNewBranch', this.branchName);
- },
- showErrorMessage(message) {
- this.loading = false;
- flash(message, 'alert', this.$el);
- },
- createdNewBranch(newBranchName) {
- this.loading = false;
- this.branchName = '';
+ this.createNewBranch(this.branchName)
+ .then(() => {
+ this.loading = false;
+ this.branchName = '';
- if (this.dropdownText) {
- this.dropdownText.textContent = newBranchName;
- }
+ if (this.dropdownText) {
+ this.dropdownText.textContent = this.currentBranch;
+ }
+
+ this.toggleDropdown();
+ })
+ .catch(res => res.json().then((data) => {
+ this.loading = false;
+ flash(data.message, 'alert', this.$el);
+ }));
},
},
created() {
@@ -59,15 +61,6 @@
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
-
- eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
- eventHub.$on('createNewBranchError', this.showErrorMessage);
- eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
- },
- destroyed() {
- eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
- eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
- eventHub.$off('createNewBranchError', this.showErrorMessage);
},
};
</script>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
index 3ccb50213ab..a5ee4f71281 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue
@@ -1,20 +1,24 @@
<script>
- import RepoStore from '../../stores/repo_store';
- import RepoHelper from '../../helpers/repo_helper';
- import eventHub from '../../event_hub';
+ import { mapState } from 'vuex';
import newModal from './modal.vue';
+ import upload from './upload.vue';
export default {
components: {
newModal,
+ upload,
},
data() {
return {
openModal: false,
modalType: '',
- currentPath: RepoStore.path,
};
},
+ computed: {
+ ...mapState([
+ 'path',
+ ]),
+ },
methods: {
createNewItem(type) {
this.modalType = type;
@@ -23,17 +27,6 @@
toggleModalOpen() {
this.openModal = !this.openModal;
},
- createNewEntryInStore(name, type) {
- RepoHelper.createNewEntry(name, type);
-
- this.toggleModalOpen();
- },
- },
- created() {
- eventHub.$on('createNewEntry', this.createNewEntryInStore);
- },
- beforeDestroy() {
- eventHub.$off('createNewEntry', this.createNewEntryInStore);
},
};
</script>
@@ -65,6 +58,11 @@
</a>
</li>
<li>
+ <upload
+ :path="path"
+ />
+ </li>
+ <li>
<a
href="#"
role="button"
@@ -79,7 +77,7 @@
<new-modal
v-if="openModal"
:type="modalType"
- :current-path="currentPath"
+ :path="path"
@toggle="toggleModalOpen"
/>
</div>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
index 5ef629e0dde..ac1f613bb71 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
@@ -1,30 +1,38 @@
<script>
+ import { mapActions } from 'vuex';
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
- import eventHub from '../../event_hub';
export default {
props: {
- currentPath: {
+ type: {
type: String,
required: true,
},
- type: {
+ path: {
type: String,
required: true,
},
},
data() {
return {
- entryName: this.currentPath !== '' ? `${this.currentPath}/` : '',
+ entryName: this.path !== '' ? `${this.path}/` : '',
};
},
components: {
popupDialog,
},
methods: {
+ ...mapActions([
+ 'createTempEntry',
+ ]),
createEntryInStore() {
- eventHub.$emit('createNewEntry', this.entryName, this.type);
+ this.createTempEntry({
+ name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
+ type: this.type,
+ });
+
+ this.toggleModalOpen();
},
toggleModalOpen() {
this.$emit('toggle');
diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
new file mode 100644
index 00000000000..14ad32f4ae0
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
@@ -0,0 +1,68 @@
+<script>
+ import { mapActions } from 'vuex';
+
+ export default {
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'createTempEntry',
+ ]),
+ createFile(target, file, isText) {
+ const { name } = file;
+ let { result } = target;
+
+ if (!isText) {
+ result = result.split('base64,')[1];
+ }
+
+ this.createTempEntry({
+ name,
+ type: 'blob',
+ content: result,
+ base64: !isText,
+ });
+ },
+ readFile(file) {
+ const reader = new FileReader();
+ const isText = file.type.match(/text.*/) !== null;
+
+ reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
+
+ if (isText) {
+ reader.readAsText(file);
+ } else {
+ reader.readAsDataURL(file);
+ }
+ },
+ openFile() {
+ Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
+ },
+ },
+ mounted() {
+ this.$refs.fileUpload.addEventListener('change', this.openFile);
+ },
+ beforeDestroy() {
+ this.$refs.fileUpload.removeEventListener('change', this.openFile);
+ },
+ };
+</script>
+
+<template>
+ <label
+ role="button"
+ class="menu-item"
+ >
+ {{ __('Upload file') }}
+ <input
+ id="file-upload"
+ type="file"
+ class="hidden"
+ ref="fileUpload"
+ />
+ </label>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index 788976a9804..98117802016 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -1,102 +1,59 @@
<script>
+import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
-import RepoMixin from '../mixins/repo_mixin';
-import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import Service from '../services/repo_service';
-import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
-import eventHub from '../event_hub';
+import repoEditor from './repo_editor.vue';
export default {
- data() {
- return Store;
+ computed: {
+ ...mapState([
+ 'currentBlobView',
+ ]),
+ ...mapGetters([
+ 'isCollapsed',
+ 'changedFiles',
+ ]),
},
- mixins: [RepoMixin],
components: {
RepoSidebar,
RepoTabs,
RepoFileButtons,
- 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
+ repoEditor,
RepoCommitSection,
- PopupDialog,
RepoPreview,
},
- created() {
- eventHub.$on('createNewBranch', this.createNewBranch);
- },
mounted() {
- Helper.getContent().catch(Helper.loadingError);
- },
- destroyed() {
- eventHub.$off('createNewBranch', this.createNewBranch);
- },
- methods: {
- getCurrentLocation() {
- return location.href;
- },
- toggleDialogOpen(toggle) {
- this.dialog.open = toggle;
- },
-
- dialogSubmitted(status) {
- this.toggleDialogOpen(false);
- this.dialog.status = status;
-
- // remove tmp files
- Helper.removeAllTmpFiles('openedFiles');
- Helper.removeAllTmpFiles('files');
- },
- toggleBlobView: Store.toggleBlobView,
- createNewBranch(branch) {
- Service.createBranch({
- branch,
- ref: Store.currentBranch,
- }).then((res) => {
- const newBranchName = res.data.name;
- const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
-
- Store.currentBranch = newBranchName;
-
- history.pushState({ key: Helper.key }, '', newUrl);
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = (e) => {
+ if (!this.changedFiles.length) return undefined;
- eventHub.$emit('createNewBranchSuccess', newBranchName);
- eventHub.$emit('toggleNewBranchDropdown');
- }).catch((err) => {
- eventHub.$emit('createNewBranchError', err.response.data.message);
+ Object.assign(e, {
+ returnValue,
});
- },
+ return returnValue;
+ };
},
};
</script>
<template>
<div class="repository-view">
- <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
+ <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
<repo-sidebar/>
- <div v-if="isMini"
- class="panel-right"
- :class="{'edit-mode': editMode}">
+ <div
+ v-if="isCollapsed"
+ class="panel-right"
+ >
<repo-tabs/>
<component
:is="currentBlobView"
- class="blob-viewer-container"/>
+ />
<repo-file-buttons/>
</div>
</div>
- <repo-commit-section/>
- <popup-dialog
- v-show="dialog.open"
- :primary-button-label="__('Discard changes')"
- kind="warning"
- :title="__('Are you sure?')"
- :text="__('Are you sure you want to discard your changes?')"
- @toggle="toggleDialogOpen"
- @submit="dialogSubmitted"
- />
+ <repo-commit-section v-if="changedFiles.length" />
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 0d6259a37a8..377e3d65348 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -1,141 +1,100 @@
<script>
-import Flash from '../../flash';
-import Store from '../stores/repo_store';
-import RepoMixin from '../mixins/repo_mixin';
-import Service from '../services/repo_service';
+import { mapGetters, mapState, mapActions } from 'vuex';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
-import { visitUrl } from '../../lib/utils/url_utility';
+import { n__ } from '../../locale';
export default {
- mixins: [RepoMixin],
-
- data() {
- return Store;
- },
-
components: {
PopupDialog,
},
-
+ data() {
+ return {
+ showNewBranchDialog: false,
+ submitCommitsLoading: false,
+ startNewMR: false,
+ commitMessage: '',
+ };
+ },
computed: {
- showCommitable() {
- return this.isCommitable && this.changedFiles.length;
- },
-
- branchPaths() {
- return this.changedFiles.map(f => f.path);
- },
-
- cantCommitYet() {
+ ...mapState([
+ 'currentBranch',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ ]),
+ commitButtonDisabled() {
return !this.commitMessage || this.submitCommitsLoading;
},
-
- filePluralize() {
- return this.changedFiles.length > 1 ? 'files' : 'file';
+ commitButtonText() {
+ return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
},
},
-
methods: {
- commitToNewBranch(status) {
- if (status) {
- this.showNewBranchDialog = false;
- this.tryCommit(null, true, true);
- } else {
- // reset the state
- }
- },
+ ...mapActions([
+ 'checkCommitStatus',
+ 'commitChanges',
+ 'getTreeData',
+ ]),
+ makeCommit(newBranch = false) {
+ const createNewBranch = newBranch || this.startNewMR;
- makeCommit(newBranch) {
- // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const commitMessage = this.commitMessage;
- const actions = this.changedFiles.map(f => ({
- action: f.tempFile ? 'create' : 'update',
- file_path: f.path,
- content: f.newContent,
- }));
- const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {
- branch,
- commit_message: commitMessage,
- actions,
+ branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
+ commit_message: this.commitMessage,
+ actions: this.changedFiles.map(f => ({
+ action: f.tempFile ? 'create' : 'update',
+ file_path: f.path,
+ content: f.content,
+ encoding: f.base64 ? 'base64' : 'text',
+ })),
+ start_branch: createNewBranch ? this.currentBranch : undefined,
};
- if (newBranch) {
- payload.start_branch = this.currentBranch;
- }
- Service.commitFiles(payload)
+
+ this.showNewBranchDialog = false;
+ this.submitCommitsLoading = true;
+
+ this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
- this.resetCommitState();
- if (this.startNewMR) {
- this.redirectToNewMr(branch);
- } else {
- this.redirectToBranch(branch);
- }
+ this.submitCommitsLoading = false;
+ this.getTreeData();
})
.catch(() => {
- Flash('An error occurred while committing your changes');
+ this.submitCommitsLoading = false;
});
},
-
- tryCommit(e, skipBranchCheck = false, newBranch = false) {
+ tryCommit() {
this.submitCommitsLoading = true;
- if (skipBranchCheck) {
- this.makeCommit(newBranch);
- } else {
- Store.setBranchHash()
- .then(() => {
- if (Store.branchChanged) {
- Store.showNewBranchDialog = true;
- return;
- }
- this.makeCommit(newBranch);
- })
- .catch(() => {
- this.submitCommitsLoading = false;
- Flash('An error occurred while committing your changes');
- });
- }
- },
-
- redirectToNewMr(branch) {
- visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
- },
-
- redirectToBranch(branch) {
- visitUrl(this.customBranchURL.replace('{{branch}}', branch));
- },
-
- resetCommitState() {
- this.submitCommitsLoading = false;
- this.openedFiles = this.openedFiles.map((file) => {
- const f = file;
- f.changed = false;
- return f;
- });
- this.changedFiles = [];
- this.commitMessage = '';
- this.editMode = false;
- window.scrollTo(0, 0);
+ this.checkCommitStatus()
+ .then((branchChanged) => {
+ if (branchChanged) {
+ this.showNewBranchDialog = true;
+ } else {
+ this.makeCommit();
+ }
+ })
+ .catch(() => {
+ this.submitCommitsLoading = false;
+ });
},
},
};
</script>
<template>
-<div
- v-if="showCommitable"
- id="commit-area">
+<div id="commit-area">
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
- @submit="commitToNewBranch"
+ @toggle="showNewBranchDialog = false"
+ @submit="makeCommit(true)"
/>
<form
class="form-horizontal"
- @submit.prevent="tryCommit">
+ @submit.prevent="tryCommit()">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">
@@ -144,10 +103,10 @@ export default {
<div class="col-md-6">
<ul class="list-unstyled changed-files">
<li
- v-for="branchPath in branchPaths"
- :key="branchPath">
+ v-for="(file, index) in changedFiles"
+ :key="index">
<span class="help-block">
- {{branchPath}}
+ {{ file.path }}
</span>
</li>
</ul>
@@ -182,9 +141,8 @@ export default {
</div>
<div class="col-md-offset-4 col-md-6">
<button
- ref="submitCommit"
type="submit"
- :disabled="cantCommitYet"
+ :disabled="commitButtonDisabled"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
@@ -193,7 +151,7 @@ export default {
aria-label="loading">
</i>
<span class="commit-summary">
- Commit {{changedFiles.length}} {{filePluralize}}
+ {{ commitButtonText }}
</span>
</button>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue
index e6e8b2e5205..6c1bb4b8566 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/repo/components/repo_edit_button.vue
@@ -1,50 +1,57 @@
<script>
-import Store from '../stores/repo_store';
-import RepoMixin from '../mixins/repo_mixin';
+import { mapGetters, mapActions, mapState } from 'vuex';
+import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default {
- data() {
- return Store;
+ components: {
+ popupDialog,
},
- mixins: [RepoMixin],
computed: {
+ ...mapState([
+ 'editMode',
+ 'discardPopupOpen',
+ ]),
+ ...mapGetters([
+ 'canEditFile',
+ ]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
-
- showButton() {
- return this.isCommitable &&
- !this.activeFile.render_error &&
- !this.binary &&
- this.openedFiles.length;
- },
},
methods: {
- editCancelClicked() {
- if (this.changedFiles.length) {
- this.dialog.open = true;
- return;
- }
- this.editMode = !this.editMode;
- Store.toggleBlobView();
- },
+ ...mapActions([
+ 'toggleEditMode',
+ 'closeDiscardPopup',
+ ]),
},
};
</script>
<template>
-<button
- v-if="showButton"
- class="btn btn-default"
- type="button"
- @click.prevent="editCancelClicked">
- <i
- v-if="!editMode"
- class="fa fa-pencil"
- aria-hidden="true">
- </i>
- <span>
- {{buttonLabel}}
- </span>
-</button>
+ <div class="editable-mode">
+ <button
+ v-if="canEditFile"
+ class="btn btn-default"
+ type="button"
+ @click.prevent="toggleEditMode()">
+ <i
+ v-if="!editMode"
+ class="fa fa-pencil"
+ aria-hidden="true">
+ </i>
+ <span>
+ {{buttonLabel}}
+ </span>
+ </button>
+ <popup-dialog
+ v-if="discardPopupOpen"
+ class="text-left"
+ :primary-button-label="__('Discard changes')"
+ kind="warning"
+ :title="__('Are you sure?')"
+ :text="__('Are you sure you want to discard your changes?')"
+ @toggle="closeDiscardPopup"
+ @submit="toggleEditMode(true)"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index df4caba51d8..1c864b176b1 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -1,124 +1,107 @@
<script>
/* global monaco */
-import Store from '../stores/repo_store';
-import Service from '../services/repo_service';
-import Helper from '../helpers/repo_helper';
-
-const RepoEditor = {
- data() {
- return Store;
- },
+import { mapGetters, mapActions } from 'vuex';
+import flash from '../../flash';
+import monacoLoader from '../monaco_loader';
+export default {
destroyed() {
- if (Helper.monacoInstance) {
- Helper.monacoInstance.destroy();
+ if (this.monacoInstance) {
+ this.monacoInstance.destroy();
}
},
-
mounted() {
- Service.getRaw(this.activeFile)
- .then((rawResponse) => {
- Store.blobRaw = rawResponse.data;
- Store.activeFile.plain = rawResponse.data;
-
- const monacoInstance = Helper.monaco.editor.create(this.$el, {
- model: null,
- readOnly: false,
- contextmenu: true,
- scrollBeyondLastLine: false,
- });
+ if (this.monaco) {
+ this.initMonaco();
+ } else {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ this.monaco = monaco;
+
+ this.initMonaco();
+ });
+ }
+ },
+ methods: {
+ ...mapActions([
+ 'getRawFileData',
+ 'changeFileContent',
+ ]),
+ initMonaco() {
+ if (this.shouldHideEditor) return;
+
+ if (this.monacoInstance) {
+ this.monacoInstance.setModel(null);
+ }
- Helper.monacoInstance = monacoInstance;
+ this.getRawFileData(this.activeFile)
+ .then(() => {
+ if (!this.monacoInstance) {
+ this.monacoInstance = this.monaco.editor.create(this.$el, {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ });
- this.addMonacoEvents();
+ this.languages = this.monaco.languages.getLanguages();
- this.setupEditor();
- })
- .catch(Helper.loadingError);
- },
+ this.addMonacoEvents();
+ }
- methods: {
+ this.setupEditor();
+ })
+ .catch(() => flash('Error setting up monaco. Please try again.'));
+ },
setupEditor() {
- this.showHide();
+ if (!this.activeFile) return;
+ const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
- Helper.setMonacoModelFromLanguage();
- },
+ const foundLang = this.languages.find(lang =>
+ lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
+ );
+ const newModel = this.monaco.editor.createModel(
+ content, foundLang ? foundLang.id : 'plaintext',
+ );
- showHide() {
- if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
- this.$el.style.display = 'none';
- } else {
- this.$el.style.display = 'inline-block';
- }
+ this.monacoInstance.setModel(newModel);
},
-
addMonacoEvents() {
- Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
- Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
- },
-
- onMonacoEditorKeysPressed() {
- Store.setActiveFileContents(Helper.monacoInstance.getValue());
- },
-
- onMonacoEditorMouseUp(e) {
- if (!e.target.position) return;
- const lineNumber = e.target.position.lineNumber;
- if (e.target.element.classList.contains('line-numbers')) {
- location.hash = `L${lineNumber}`;
- Store.setActiveLine(lineNumber);
- }
+ this.monacoInstance.onKeyUp(() => {
+ this.changeFileContent({
+ file: this.activeFile,
+ content: this.monacoInstance.getValue(),
+ });
+ });
},
},
-
watch: {
- dialog: {
- handler(obj) {
- const newObj = obj;
- if (newObj.status) {
- newObj.status = false;
- this.openedFiles = this.openedFiles.map((file) => {
- const f = file;
- if (f.active) {
- this.blobRaw = f.plain;
- }
- f.changed = false;
- delete f.newContent;
-
- return f;
- });
- this.editMode = false;
- Store.toggleBlobView();
- }
- },
- deep: true,
- },
-
- blobRaw() {
- if (Helper.monacoInstance) {
- this.setupEditor();
- }
- },
-
- activeLine() {
- if (Helper.monacoInstance) {
- Helper.monacoInstance.setPosition({
- lineNumber: this.activeLine,
- column: 1,
- });
+ activeFile(oldVal, newVal) {
+ if (newVal && !newVal.active) {
+ this.initMonaco();
}
},
},
computed: {
+ ...mapGetters([
+ 'activeFile',
+ 'activeFileExtension',
+ ]),
shouldHideEditor() {
- return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
+ return this.activeFile.binary && !this.activeFile.raw;
},
},
};
-
-export default RepoEditor;
</script>
<template>
-<div id="ide" v-if='!shouldHideEditor'></div>
+ <div
+ id="ide"
+ class="blob-viewer-container blob-editor-container"
+ >
+ <div
+ v-if="shouldHideEditor"
+ v-html="activeFile.html"
+ >
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 8c86e87ed3a..5be47d568e7 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -1,13 +1,15 @@
<script>
+ import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
- import eventHub from '../event_hub';
- import repoMixin from '../mixins/repo_mixin';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
mixins: [
- repoMixin,
timeAgoMixin,
],
+ components: {
+ skeletonLoadingContainer,
+ },
props: {
file: {
type: Object,
@@ -15,13 +17,18 @@
},
},
computed: {
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
+ isSubmodule() {
+ return this.file.type === 'submodule';
+ },
fileIcon() {
- const classObj = {
+ return {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
- return classObj;
},
levelIndentation() {
return {
@@ -31,11 +38,14 @@
shortId() {
return this.file.id.substr(0, 8);
},
+ submoduleColSpan() {
+ return !this.isCollapsed && this.isSubmodule ? 3 : 1;
+ },
},
methods: {
- linkClicked(file) {
- eventHub.$emit('fileNameClicked', file);
- },
+ ...mapActions([
+ 'clickedTreeRow',
+ ]),
},
};
</script>
@@ -43,8 +53,11 @@
<template>
<tr
class="file"
- @click.prevent="linkClicked(file)">
- <td>
+ @click.prevent="clickedTreeRow(file)">
+ <td
+ class="multi-file-table-col-name"
+ :colspan="submoduleColSpan"
+ >
<i
class="fa fa-fw file-icon"
:class="fileIcon"
@@ -58,7 +71,7 @@
>
{{ file.name }}
</a>
- <template v-if="file.type === 'submodule' && file.id">
+ <template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
<a
@@ -71,15 +84,20 @@
</template>
</td>
- <template v-if="!isMini">
+ <template v-if="!isCollapsed && !isSubmodule">
<td class="hidden-sm hidden-xs">
<a
+ v-if="file.lastCommit.message"
@click.stop
:href="file.lastCommit.url"
class="commit-message"
>
{{ file.lastCommit.message }}
</a>
+ <skeleton-loading-container
+ v-else
+ :small="true"
+ />
</td>
<td class="commit-update hidden-xs text-right">
@@ -89,6 +107,11 @@
>
{{ timeFormated(file.lastCommit.updatedAt) }}
</span>
+ <skeleton-loading-container
+ v-else
+ class="animation-container-right"
+ :small="true"
+ />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
index c98f641c853..dd948ee84fb 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -1,37 +1,22 @@
<script>
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import RepoMixin from '../mixins/repo_mixin';
-
-const RepoFileButtons = {
- data() {
- return Store;
- },
-
- mixins: [RepoMixin],
+import { mapGetters } from 'vuex';
+export default {
computed: {
+ ...mapGetters([
+ 'activeFile',
+ ]),
showButtons() {
- return this.activeFile.raw_path ||
- this.activeFile.blame_path ||
- this.activeFile.commits_path ||
+ return this.activeFile.rawPath ||
+ this.activeFile.blamePath ||
+ this.activeFile.commitsPath ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() {
- return this.binary ? 'Download' : 'Raw';
- },
-
- canPreview() {
- return Helper.isRenderable();
+ return this.activeFile.binary ? 'Download' : 'Raw';
},
},
-
- methods: {
- rawPreviewToggle: Store.toggleRawPreview,
- },
};
-
-export default RepoFileButtons;
</script>
<template>
@@ -40,11 +25,11 @@ export default RepoFileButtons;
class="repo-file-buttons"
>
<a
- :href="activeFile.raw_path"
+ :href="activeFile.rawPath"
target="_blank"
class="btn btn-default raw"
rel="noopener noreferrer">
- {{rawDownloadButtonLabel}}
+ {{ rawDownloadButtonLabel }}
</a>
<div
@@ -52,12 +37,12 @@ export default RepoFileButtons;
role="group"
aria-label="File actions">
<a
- :href="activeFile.blame_path"
+ :href="activeFile.blamePath"
class="btn btn-default blame">
Blame
</a>
<a
- :href="activeFile.commits_path"
+ :href="activeFile.commitsPath"
class="btn btn-default history">
History
</a>
@@ -67,13 +52,5 @@ export default RepoFileButtons;
Permalink
</a>
</div>
-
- <a
- v-if="canPreview"
- href="#"
- @click.prevent="rawPreviewToggle"
- class="btn btn-default preview">
- {{activeFileLabel}}
- </a>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
index 832b45b2b29..8fa637d771f 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -1,14 +1,15 @@
<script>
- import repoMixin from '../mixins/repo_mixin';
+ import { mapGetters } from 'vuex';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
- mixins: [
- repoMixin,
- ],
- methods: {
- lineOfCode(n) {
- return `skeleton-line-${n}`;
- },
+ components: {
+ skeletonLoadingContainer,
+ },
+ computed: {
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
},
};
</script>
@@ -18,37 +19,25 @@
class="loading-file"
aria-label="Loading files"
>
- <td>
- <div
- class="animation-container animation-container-small">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <td class="multi-file-table-col-name">
+ <skeleton-loading-container
+ :small="true"
+ />
</td>
- <template v-if="!isMini">
+ <template v-if="!isCollapsed">
<td
class="hidden-sm hidden-xs">
- <div class="animation-container">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <skeleton-loading-container
+ :small="true"
+ />
</td>
<td
class="hidden-xs">
- <div class="animation-container animation-container-small animation-container-right">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <skeleton-loading-container
+ class="animation-container-right"
+ :small="true"
+ />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue
index c4bf6dcdec2..a2b305bbd05 100644
--- a/app/assets/javascripts/repo/components/repo_prev_directory.vue
+++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue
@@ -1,26 +1,22 @@
<script>
- import eventHub from '../event_hub';
- import repoMixin from '../mixins/repo_mixin';
+ import { mapGetters, mapState, mapActions } from 'vuex';
export default {
- mixins: [
- repoMixin,
- ],
- props: {
- prevUrl: {
- type: String,
- required: true,
- },
- },
computed: {
+ ...mapState([
+ 'parentTreeUrl',
+ ]),
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
colSpanCondition() {
- return this.isMini ? undefined : 3;
+ return this.isCollapsed ? undefined : 3;
},
},
methods: {
- linkClicked(file) {
- eventHub.$emit('goToPreviousDirectoryClicked', file);
- },
+ ...mapActions([
+ 'getTreeData',
+ ]),
},
};
</script>
@@ -30,9 +26,9 @@
<td
:colspan="colSpanCondition"
class="table-cell"
- @click.prevent="linkClicked(prevUrl)"
+ @click.prevent="getTreeData({ endpoint: parentTreeUrl })"
>
- <a :href="prevUrl">...</a>
+ <a :href="parentTreeUrl">...</a>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index b5be771d539..d1883299bd9 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -1,26 +1,20 @@
<script>
/* global LineHighlighter */
-
-import Store from '../stores/repo_store';
+import { mapGetters } from 'vuex';
export default {
- data() {
- return Store;
- },
computed: {
- html() {
- return this.activeFile.html;
+ ...mapGetters([
+ 'activeFile',
+ ]),
+ renderErrorTooLarge() {
+ return this.activeFile.renderError === 'too_large';
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
- highlightLine() {
- if (Store.activeLine > -1) {
- this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
- }
- },
},
mounted() {
this.highlightFile();
@@ -29,38 +23,39 @@ export default {
scrollFileHolder: true,
});
},
- watch: {
- html() {
- this.$nextTick(() => {
- this.highlightFile();
- this.highlightLine();
- });
- },
- activeLine() {
- this.highlightLine();
- },
+ updated() {
+ this.$nextTick(() => {
+ this.highlightFile();
+ });
},
};
</script>
<template>
-<div>
+<div class="blob-viewer-container">
<div
- v-if="!activeFile.render_error"
+ v-if="!activeFile.renderError"
v-html="activeFile.html">
</div>
<div
- v-else-if="activeFile.tooLarge"
+ v-else-if="activeFile.tempFile"
+ class="vertical-center render-error">
+ <p class="text-center">
+ The source could not be displayed for this temporary file.
+ </p>
+ </div>
+ <div
+ v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
- The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
+ The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
- The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
+ The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 09dc9ee25d7..9365b09326f 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -1,120 +1,55 @@
<script>
-import _ from 'underscore';
-import Service from '../services/repo_service';
-import Helper from '../helpers/repo_helper';
-import Store from '../stores/repo_store';
-import eventHub from '../event_hub';
+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';
-import RepoMixin from '../mixins/repo_mixin';
export default {
- mixins: [RepoMixin],
components: {
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
- window.addEventListener('popstate', this.checkHistory);
+ window.addEventListener('popstate', this.popHistoryState);
},
destroyed() {
- eventHub.$off('fileNameClicked', this.fileClicked);
- eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
- window.removeEventListener('popstate', this.checkHistory);
+ window.removeEventListener('popstate', this.popHistoryState);
},
mounted() {
- eventHub.$on('fileNameClicked', this.fileClicked);
- eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
- },
- data() {
- return Store;
+ this.getTreeData();
},
computed: {
- flattendFiles() {
- const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
-
- return _.chain(this.files)
- .map(arr => [arr, mapFiles(arr)])
- .flatten()
- .value();
- },
+ ...mapState([
+ 'loading',
+ 'isRoot',
+ ]),
+ ...mapState({
+ projectName(state) {
+ return state.project.name;
+ },
+ }),
+ ...mapGetters([
+ 'treeList',
+ 'isCollapsed',
+ ]),
},
methods: {
- checkHistory() {
- let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
- if (!selectedFile) {
- // Maybe it is not in the current tree but in the opened tabs
- selectedFile = Helper.getFileFromPath(location.pathname);
- }
-
- let lineNumber = null;
- if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
-
- if (selectedFile) {
- if (selectedFile.url !== this.activeFile.url) {
- this.fileClicked(selectedFile, lineNumber);
- } else {
- Store.setActiveLine(lineNumber);
- }
- } else {
- // Not opened at all lets open new tab
- this.fileClicked({
- url: location.href,
- }, lineNumber);
- }
- },
-
- fileClicked(clickedFile, lineNumber) {
- const file = clickedFile;
-
- if (file.loading) return;
-
- if (file.type === 'tree' && file.opened) {
- Helper.setDirectoryToClosed(file);
- Store.setActiveLine(lineNumber);
- } else if (file.type === 'submodule') {
- file.loading = true;
-
- gl.utils.visitUrl(file.url);
- } else {
- const openFile = Helper.getFileFromPath(file.url);
-
- if (openFile) {
- Store.setActiveFiles(openFile);
- Store.setActiveLine(lineNumber);
- } else {
- file.loading = true;
- Service.url = file.url;
- Helper.getContent(file)
- .then(() => {
- file.loading = false;
- Helper.scrollTabsRight();
- Store.setActiveLine(lineNumber);
- })
- .catch(Helper.loadingError);
- }
- }
- },
-
- goToPreviousDirectoryClicked(prevURL) {
- Service.url = prevURL;
- Helper.getContent(null, true)
- .then(() => Helper.scrollTabsRight())
- .catch(Helper.loadingError);
- },
+ ...mapActions([
+ 'getTreeData',
+ 'popHistoryState',
+ ]),
},
};
</script>
<template>
-<div id="sidebar" :class="{'sidebar-mini' : isMini}">
+<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
<table class="table">
<thead>
<tr>
<th
- v-if="isMini"
+ v-if="isCollapsed"
class="repo-file-options title"
>
<strong class="clgray">
@@ -122,7 +57,7 @@ export default {
</strong>
</th>
<template v-else>
- <th class="name">
+ <th class="name multi-file-table-col-name">
Name
</th>
<th class="hidden-sm hidden-xs last-commit">
@@ -136,17 +71,16 @@ export default {
</thead>
<tbody>
<repo-previous-directory
- v-if="!isRoot && !loading.tree"
- :prev-url="prevURL"
+ v-if="!isRoot && treeList.length"
/>
<repo-loading-file
- v-if="!flattendFiles.length && loading.tree"
+ v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
/>
<repo-file
- v-for="file in flattendFiles"
- :key="file.id"
+ v-for="(file, index) in treeList"
+ :key="file.key"
:file="file"
/>
</tbody>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
index 405d7b4cf86..da0714c368c 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -1,7 +1,7 @@
<script>
-import Store from '../stores/repo_store';
+import { mapActions } from 'vuex';
-const RepoTab = {
+export default {
props: {
tab: {
type: Object,
@@ -11,7 +11,7 @@ const RepoTab = {
computed: {
closeLabel() {
- if (this.tab.changed) {
+ if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
@@ -26,29 +26,23 @@ const RepoTab = {
},
methods: {
- tabClicked(file) {
- Store.setActiveFiles(file);
- },
- closeTab(file) {
- if (file.changed || file.tempFile) return;
-
- Store.removeFromOpenedFiles(file);
- },
+ ...mapActions([
+ 'setFileActive',
+ 'closeFile',
+ ]),
},
};
-
-export default RepoTab;
</script>
<template>
<li
:class="{ active : tab.active }"
- @click="tabClicked(tab)"
+ @click="setFileActive(tab)"
>
<button
type="button"
class="close-btn"
- @click.stop.prevent="closeTab(tab)"
+ @click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel">
<i
class="fa"
@@ -61,7 +55,7 @@ export default RepoTab;
href="#"
class="repo-tab"
:title="tab.url"
- @click.prevent="tabClicked(tab)">
+ @click.prevent.stop="setFileActive(tab)">
{{tab.name}}
</a>
</li>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
index b57cd0960de..59beae53e8d 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -1,15 +1,15 @@
<script>
- import Store from '../stores/repo_store';
+ import { mapState } from 'vuex';
import RepoTab from './repo_tab.vue';
- import RepoMixin from '../mixins/repo_mixin';
export default {
- mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
- data() {
- return Store;
+ computed: {
+ ...mapState([
+ 'openFiles',
+ ]),
},
};
</script>
@@ -20,7 +20,7 @@
class="list-unstyled"
>
<repo-tab
- v-for="tab in openedFiles"
+ v-for="tab in openFiles"
:key="tab.id"
:tab="tab"
/>
diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
deleted file mode 100644
index f8729bbf585..00000000000
--- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* global monaco */
-import RepoEditor from '../components/repo_editor.vue';
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import monacoLoader from '../monaco_loader';
-
-function repoEditorLoader() {
- Store.monacoLoading = true;
- return new Promise((resolve, reject) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- Helper.monaco = monaco;
- Store.monacoLoading = false;
- resolve(RepoEditor);
- }, () => {
- Store.monacoLoading = false;
- reject();
- });
- });
-}
-
-const MonacoLoaderHelper = {
- repoEditorLoader,
-};
-
-export default MonacoLoaderHelper;
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
deleted file mode 100644
index fb26f3b7380..00000000000
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ /dev/null
@@ -1,317 +0,0 @@
-import Service from '../services/repo_service';
-import Store from '../stores/repo_store';
-import Flash from '../../flash';
-
-const RepoHelper = {
- monacoInstance: null,
-
- getDefaultActiveFile() {
- return {
- id: '',
- active: true,
- binary: false,
- extension: '',
- html: '',
- mime_type: '',
- name: '',
- plain: '',
- size: 0,
- url: '',
- raw: false,
- newContent: '',
- changed: false,
- loading: false,
- };
- },
-
- key: '',
-
- Time: window.performance
- && window.performance.now
- ? window.performance
- : Date,
-
- getFileExtension(fileName) {
- return fileName.split('.').pop();
- },
-
- getLanguageIDForFile(file, langs) {
- const ext = RepoHelper.getFileExtension(file.name);
- const foundLang = RepoHelper.findLanguage(ext, langs);
-
- return foundLang ? foundLang.id : 'plaintext';
- },
-
- setMonacoModelFromLanguage() {
- RepoHelper.monacoInstance.setModel(null);
- const languages = RepoHelper.monaco.languages.getLanguages();
- const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
- const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
- RepoHelper.monacoInstance.setModel(newModel);
- },
-
- findLanguage(ext, langs) {
- return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
- },
-
- setDirectoryOpen(tree, title) {
- if (!tree) return;
-
- Object.assign(tree, {
- opened: true,
- });
-
- RepoHelper.updateHistoryEntry(tree.url, title);
- Store.path = tree.path;
- },
-
- setDirectoryToClosed(entry) {
- Object.assign(entry, {
- opened: false,
- files: [],
- });
- },
-
- isRenderable() {
- const okExts = ['md', 'svg'];
- return okExts.indexOf(Store.activeFile.extension) > -1;
- },
-
- setBinaryDataAsBase64(file) {
- Service.getBase64Content(file.raw_path)
- .then((response) => {
- Store.blobRaw = response;
- file.base64 = response; // eslint-disable-line no-param-reassign
- })
- .catch(RepoHelper.loadingError);
- },
-
- getContent(treeOrFile, emptyFiles = false) {
- let file = treeOrFile;
-
- if (!Store.files.length) {
- Store.loading.tree = true;
- }
-
- return Service.getContent()
- .then((response) => {
- const data = response.data;
- if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
- if (data.path && !Store.isInitialRoot) {
- Store.isRoot = data.path === '/';
- Store.isInitialRoot = Store.isRoot;
- }
-
- if (file && file.type === 'blob') {
- if (!file) file = data;
- Store.binary = data.binary;
-
- if (data.binary) {
- // file might be undefined
- RepoHelper.setBinaryDataAsBase64(data);
- Store.setViewToPreview();
- } else if (!Store.isPreviewView() && !data.render_error) {
- Service.getRaw(data)
- .then((rawResponse) => {
- Store.blobRaw = rawResponse.data;
- data.plain = rawResponse.data;
- RepoHelper.setFile(data, file);
- }).catch(RepoHelper.loadingError);
- }
-
- if (Store.isPreviewView()) {
- RepoHelper.setFile(data, file);
- }
- } else {
- Store.loading.tree = false;
- RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
-
- if (emptyFiles) {
- Store.files = [];
- }
-
- this.addToDirectory(file, data);
-
- Store.prevURL = Service.blobURLtoParentTree(Service.url);
- }
- }).catch(RepoHelper.loadingError);
- },
-
- addToDirectory(file, data) {
- const tree = file || Store;
-
- // TODO: Figure out why `popstate` is being trigger in the specs
- if (!tree.files) return;
-
- const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
-
- tree.files = files;
- },
-
- setFile(data, file) {
- const newFile = data;
- newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
-
- if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
- newFile.tooLarge = true;
- }
- newFile.newContent = '';
-
- Store.addToOpenedFiles(newFile);
- Store.setActiveFiles(newFile);
- },
-
- serializeRepoEntity(type, entity, level = 0) {
- const {
- id,
- url,
- name,
- icon,
- last_commit,
- tree_url,
- path,
- tempFile,
- active,
- opened,
- } = entity;
-
- return {
- id,
- type,
- name,
- url,
- tree_url,
- path,
- level,
- tempFile,
- icon: `fa-${icon}`,
- files: [],
- loading: false,
- opened,
- active,
- // eslint-disable-next-line camelcase
- lastCommit: last_commit ? {
- url: `${Store.projectUrl}/commit/${last_commit.id}`,
- message: last_commit.message,
- updatedAt: last_commit.committed_date,
- } : {},
- };
- },
-
- scrollTabsRight() {
- const tabs = document.getElementById('tabs');
- if (!tabs) return;
- tabs.scrollLeft = tabs.scrollWidth;
- },
-
- dataToListOfFiles(data, level) {
- const { blobs, trees, submodules } = data;
- return [
- ...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
- ...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
- ...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
- ];
- },
-
- genKey() {
- return RepoHelper.Time.now().toFixed(3);
- },
-
- updateHistoryEntry(url, title) {
- const history = window.history;
-
- RepoHelper.key = RepoHelper.genKey();
-
- if (document.location.pathname !== url) {
- history.pushState({ key: RepoHelper.key }, '', url);
- }
-
- if (title) {
- document.title = title;
- }
- },
-
- findOpenedFileFromActive() {
- return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
- },
-
- getFileFromPath(path) {
- return Store.openedFiles.find(file => file.url === path);
- },
-
- loadingError() {
- Flash('Unable to load this content at this time.');
- },
- openEditMode() {
- Store.editMode = true;
- Store.currentBlobView = 'repo-editor';
- },
- updateStorePath(path) {
- Store.path = path;
- },
- findOrCreateEntry(type, tree, name) {
- let exists = true;
- let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name);
-
- if (!foundEntry) {
- foundEntry = RepoHelper.serializeRepoEntity(type, {
- id: name,
- name,
- path: tree.path ? `${tree.path}/${name}` : name,
- icon: type === 'tree' ? 'folder' : 'file-text-o',
- tempFile: true,
- opened: true,
- active: true,
- }, tree.level !== undefined ? tree.level + 1 : 0);
-
- exists = false;
- tree.files.push(foundEntry);
- }
-
- return {
- entry: foundEntry,
- exists,
- };
- },
- removeAllTmpFiles(storeFilesKey) {
- Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile);
- },
- createNewEntry(name, type) {
- const originalPath = Store.path;
- let entryName = name;
-
- if (entryName.indexOf(`${originalPath}/`) !== 0) {
- this.updateStorePath('');
- } else {
- entryName = entryName.replace(`${originalPath}/`, '');
- }
-
- if (entryName === '') return;
-
- const fileName = type === 'tree' ? '.gitkeep' : entryName;
- let tree = Store;
-
- if (type === 'tree') {
- const dirNames = entryName.split('/');
-
- dirNames.forEach((dirName) => {
- if (dirName === '') return;
-
- tree = this.findOrCreateEntry('tree', tree, dirName).entry;
- });
- }
-
- if ((type === 'tree' && tree.tempFile) || type === 'blob') {
- const file = this.findOrCreateEntry('blob', tree, fileName);
-
- if (!file.exists) {
- this.setFile(file.entry, file.entry);
- this.openEditMode();
- }
- }
-
- this.updateStorePath(originalPath);
- },
-};
-
-export default RepoHelper;
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
index 72fc5a70648..b6801af7fcb 100644
--- a/app/assets/javascripts/repo/index.js
+++ b/app/assets/javascripts/repo/index.js
@@ -1,55 +1,50 @@
-import $ from 'jquery';
import Vue from 'vue';
+import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
-import Service from './services/repo_service';
-import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
+import store from './stores';
import Translate from '../vue_shared/translate';
-function initDropdowns() {
- $('.js-tree-ref-target-holder').hide();
-}
-
-function addEventsForNonVueEls() {
- window.onbeforeunload = function confirmUnload(e) {
- const hasChanged = Store.openedFiles
- .some(file => file.changed);
- if (!hasChanged) return undefined;
- const event = e || window.event;
- if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
- // For Safari
- return 'Are you sure you want to lose unsaved changes?';
- };
-}
-
-function setInitialStore(data) {
- Store.service = Service;
- Store.service.url = data.url;
- Store.service.refsUrl = data.refsUrl;
- Store.path = data.currentPath;
- Store.projectId = data.projectId;
- Store.projectName = data.projectName;
- Store.projectUrl = data.projectUrl;
- Store.canCommit = data.canCommit;
- Store.onTopOfBranch = data.onTopOfBranch;
- Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
- Store.customBranchURL = decodeURIComponent(data.blobUrl);
- Store.isRoot = convertPermissionToBoolean(data.root);
- Store.isInitialRoot = convertPermissionToBoolean(data.root);
- Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
- Store.checkIsCommitable();
- Store.setBranchHash();
-}
-
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');
},
@@ -59,15 +54,20 @@ function initRepo(el) {
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,
},
@@ -87,32 +87,20 @@ function initNewBranchForm() {
components: {
newBranchForm,
},
+ store,
render(createElement) {
- return createElement('new-branch-form', {
- props: {
- currentBranch: Store.currentBranch,
- },
- });
+ return createElement('new-branch-form');
},
});
}
-function initRepoBundle() {
- const repo = document.getElementById('repo');
- const editButton = document.querySelector('.editable-mode');
- const newDropdownHolder = document.querySelector('.js-new-dropdown');
- setInitialStore(repo.dataset);
- addEventsForNonVueEls();
- initDropdowns();
-
- Vue.use(Translate);
-
- initRepo(repo);
- initRepoEditButton(editButton);
- initNewBranchForm();
- initNewDropdown(newDropdownHolder);
-}
+const repo = document.getElementById('repo');
+const editButton = document.querySelector('.editable-mode');
+const newDropdownHolder = document.querySelector('.js-new-dropdown');
-$(initRepoBundle);
+Vue.use(Translate);
-export default initRepoBundle;
+initRepo(repo);
+initRepoEditButton(editButton);
+initNewBranchForm();
+initNewDropdown(newDropdownHolder);
diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js
deleted file mode 100644
index efeda426b96..00000000000
--- a/app/assets/javascripts/repo/mixins/repo_mixin.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Store from '../stores/repo_store';
-
-const RepoMixin = {
- computed: {
- isMini() {
- return !!Store.openedFiles.length;
- },
-
- changedFiles() {
- const changedFileList = this.openedFiles
- .filter(file => file.changed || file.tempFile);
- return changedFileList;
- },
- },
-};
-
-export default RepoMixin;
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
new file mode 100644
index 00000000000..2fb45dcb03c
--- /dev/null
+++ b/app/assets/javascripts/repo/services/index.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import Api from '../../api';
+
+Vue.use(VueResource);
+
+export default {
+ getTreeData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getFileData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getRawFileData(file) {
+ if (file.tempFile) {
+ return Promise.resolve(file.content);
+ }
+
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ .then(res => res.text());
+ },
+ getBranchData(projectId, currentBranch) {
+ return Api.branchSingle(projectId, currentBranch);
+ },
+ createBranch(projectId, payload) {
+ const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
+
+ return Vue.http.post(url, payload);
+ },
+ commit(projectId, payload) {
+ return Api.commitMultiple(projectId, payload);
+ },
+ getTreeLastCommit(endpoint) {
+ return Vue.http.get(endpoint, {
+ params: {
+ format: 'json',
+ },
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
deleted file mode 100644
index c9fa5cc8bf8..00000000000
--- a/app/assets/javascripts/repo/services/repo_service.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import axios from 'axios';
-import csrf from '../../lib/utils/csrf';
-import Store from '../stores/repo_store';
-import Api from '../../api';
-import Helper from '../helpers/repo_helper';
-
-axios.defaults.headers.common[csrf.headerKey] = csrf.token;
-
-const RepoService = {
- url: '',
- options: {
- params: {
- format: 'json',
- },
- },
- createBranchPath: '/api/:version/projects/:id/repository/branches',
- richExtensionRegExp: /md/,
-
- getRaw(file) {
- if (file.tempFile) {
- return Promise.resolve({
- data: '',
- });
- }
-
- return axios.get(file.raw_path, {
- // Stop Axios from parsing a JSON file into a JS object
- transformResponse: [res => res],
- });
- },
-
- buildParams(url = this.url) {
- // shallow clone object without reference
- const params = Object.assign({}, this.options.params);
-
- if (this.urlIsRichBlob(url)) params.viewer = 'rich';
-
- return params;
- },
-
- urlIsRichBlob(url = this.url) {
- const extension = Helper.getFileExtension(url);
-
- return this.richExtensionRegExp.test(extension);
- },
-
- getContent(url = this.url) {
- const params = this.buildParams(url);
-
- return axios.get(url, {
- params,
- });
- },
-
- getBase64Content(url = this.url) {
- const request = axios.get(url, {
- responseType: 'arraybuffer',
- });
-
- return request.then(response => this.bufferToBase64(response.data));
- },
-
- bufferToBase64(data) {
- return new Buffer(data, 'binary').toString('base64');
- },
-
- blobURLtoParentTree(url) {
- const urlArray = url.split('/');
- urlArray.pop();
- const blobIndex = urlArray.lastIndexOf('blob');
-
- if (blobIndex > -1) urlArray[blobIndex] = 'tree';
-
- return urlArray.join('/');
- },
-
- getBranch() {
- return Api.branchSingle(Store.projectId, Store.currentBranch);
- },
-
- commitFiles(payload) {
- return Api.commitMultiple(Store.projectId, payload)
- .then(this.commitFlash);
- },
-
- createBranch(payload) {
- const url = Api.buildUrl(this.createBranchPath)
- .replace(':id', Store.projectId);
- return axios.post(url, payload);
- },
-
- commitFlash(data) {
- if (data.short_id && data.stats) {
- window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
- } else {
- window.Flash(data.message);
- }
- },
-};
-
-export default RepoService;
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
new file mode 100644
index 00000000000..120ce96f44d
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions.js
@@ -0,0 +1,145 @@
+import Vue from 'vue';
+import flash from '../../flash';
+import service from '../services';
+import * as types from './mutation_types';
+
+export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
+
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
+
+export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
+
+export const discardAllChanges = ({ commit, getters, dispatch }) => {
+ const changedFiles = getters.changedFiles;
+
+ changedFiles.forEach((file) => {
+ commit(types.DISCARD_FILE_CHANGES, file);
+
+ if (file.tempFile) {
+ dispatch('closeFile', { file, force: true });
+ }
+ });
+};
+
+export const closeAllFiles = ({ state, dispatch }) => {
+ state.openFiles.forEach(file => dispatch('closeFile', { file }));
+};
+
+export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
+ const changedFiles = getters.changedFiles;
+
+ if (changedFiles.length && !force) {
+ commit(types.TOGGLE_DISCARD_POPUP, true);
+ } else {
+ commit(types.TOGGLE_EDIT_MODE);
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+ dispatch('toggleBlobView');
+
+ if (!state.editMode) {
+ dispatch('discardAllChanges');
+ }
+ }
+};
+
+export const toggleBlobView = ({ commit, state }) => {
+ if (state.editMode) {
+ commit(types.SET_EDIT_MODE);
+ } else {
+ commit(types.SET_PREVIEW_MODE);
+ }
+};
+
+export const checkCommitStatus = ({ state }) => service.getBranchData(
+ state.project.id,
+ state.currentBranch,
+)
+ .then((data) => {
+ const { id } = data.commit;
+
+ if (state.currentRef !== id) {
+ return true;
+ }
+
+ return false;
+ })
+ .catch(() => flash('Error checking branch data. Please try again.'));
+
+export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
+ service.commit(state.project.id, payload)
+ .then((data) => {
+ const { branch } = payload;
+ if (!data.short_id) {
+ flash(data.message);
+ return;
+ }
+
+ const lastCommit = {
+ commit_path: `${state.project.url}/commit/${data.id}`,
+ commit: {
+ message: data.message,
+ authored_date: data.committed_date,
+ },
+ };
+
+ flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
+
+ if (newMr) {
+ dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
+ } else {
+ commit(types.SET_COMMIT_REF, data.id);
+
+ getters.changedFiles.forEach((entry) => {
+ commit(types.SET_LAST_COMMIT_DATA, {
+ entry,
+ lastCommit,
+ });
+ });
+
+ dispatch('discardAllChanges');
+ dispatch('closeAllFiles');
+ dispatch('toggleEditMode');
+
+ window.scrollTo(0, 0);
+ }
+ })
+ .catch(() => flash('Error committing changes. Please try again.'));
+
+export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
+ if (type === 'tree') {
+ dispatch('createTempTree', name);
+ } else if (type === 'blob') {
+ dispatch('createTempFile', {
+ tree: state,
+ name,
+ base64,
+ content,
+ });
+ }
+};
+
+export const popHistoryState = ({ state, dispatch, getters }) => {
+ const treeList = getters.treeList;
+ const tree = treeList.find(file => file.url === state.previousUrl);
+
+ if (!tree) return;
+
+ if (tree.type === 'tree') {
+ dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
+ }
+};
+
+export const scrollToTab = () => {
+ Vue.nextTick(() => {
+ const tabs = document.getElementById('tabs');
+
+ if (tabs) {
+ const tabEl = tabs.querySelector('.active .repo-tab');
+
+ tabEl.focus();
+ }
+ });
+};
+
+export * from './actions/tree';
+export * from './actions/file';
+export * from './actions/branch';
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
new file mode 100644
index 00000000000..61d9a5af3e3
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/branch.js
@@ -0,0 +1,20 @@
+import service from '../../services';
+import * as types from '../mutation_types';
+import { pushState } from '../utils';
+
+// eslint-disable-next-line import/prefer-default-export
+export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
+ state.project.id,
+ {
+ branch,
+ ref: state.currentBranch,
+ },
+).then(res => res.json())
+.then((data) => {
+ const branchName = data.name;
+ const url = location.href.replace(state.currentBranch, branchName);
+
+ pushState(url);
+
+ commit(types.SET_CURRENT_BRANCH, branchName);
+});
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js
new file mode 100644
index 00000000000..5bae4fa826a
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/file.js
@@ -0,0 +1,110 @@
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import {
+ findEntry,
+ pushState,
+ setPageTitle,
+ createTemp,
+ findIndexOfFile,
+} from '../utils';
+
+export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
+ if ((file.changed || file.tempFile) && !force) return;
+
+ const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
+ const fileWasActive = file.active;
+
+ commit(types.TOGGLE_FILE_OPEN, file);
+ commit(types.SET_FILE_ACTIVE, { file, active: false });
+
+ if (state.openFiles.length > 0 && fileWasActive) {
+ const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
+ const nextFileToOpen = state.openFiles[nextIndexToOpen];
+
+ dispatch('setFileActive', nextFileToOpen);
+ } else if (!state.openFiles.length) {
+ pushState(file.parentTreeUrl);
+ }
+
+ dispatch('getLastCommitData');
+};
+
+export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
+ const currentActiveFile = getters.activeFile;
+
+ if (file.active) return;
+
+ if (currentActiveFile) {
+ commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
+ }
+
+ commit(types.SET_FILE_ACTIVE, { file, active: true });
+ dispatch('scrollToTab');
+
+ // reset hash for line highlighting
+ location.hash = '';
+};
+
+export const getFileData = ({ state, commit, dispatch }, file) => {
+ commit(types.TOGGLE_LOADING, file);
+
+ service.getFileData(file.url)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ commit(types.SET_FILE_DATA, { data, file });
+ commit(types.TOGGLE_FILE_OPEN, file);
+ dispatch('setFileActive', file);
+ commit(types.TOGGLE_LOADING, file);
+
+ pushState(file.url);
+ })
+ .catch(() => {
+ commit(types.TOGGLE_LOADING, file);
+ flash('Error loading file data. Please try again.');
+ });
+};
+
+export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
+ .then((raw) => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ })
+ .catch(() => flash('Error loading file content. Please try again.'));
+
+export const changeFileContent = ({ commit }, { file, content }) => {
+ commit(types.UPDATE_FILE_CONTENT, { file, content });
+};
+
+export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
+ const file = createTemp({
+ name: name.replace(`${state.path}/`, ''),
+ path: tree.path,
+ type: 'blob',
+ level: tree.level !== undefined ? tree.level + 1 : 0,
+ changed: true,
+ content,
+ base64,
+ });
+
+ if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
+
+ commit(types.CREATE_TMP_FILE, {
+ parent: tree,
+ file,
+ });
+ commit(types.TOGGLE_FILE_OPEN, file);
+ dispatch('setFileActive', file);
+
+ if (!state.editMode && !file.base64) {
+ dispatch('toggleEditMode', true);
+ }
+
+ return Promise.resolve(file);
+};
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
new file mode 100644
index 00000000000..aa830e946a2
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/tree.js
@@ -0,0 +1,162 @@
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import {
+ pushState,
+ setPageTitle,
+ findEntry,
+ createTemp,
+ createOrMergeEntry,
+} from '../utils';
+
+export const getTreeData = (
+ { commit, state, dispatch },
+ { endpoint = state.endpoints.rootEndpoint, tree = state } = {},
+) => {
+ commit(types.TOGGLE_LOADING, tree);
+
+ service.getTreeData(endpoint)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ const prevLastCommitPath = tree.lastCommitPath;
+ if (!state.isInitialRoot) {
+ commit(types.SET_ROOT, data.path === '/');
+ }
+
+ dispatch('updateDirectoryData', { data, tree });
+ commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
+ commit(types.TOGGLE_LOADING, tree);
+
+ if (prevLastCommitPath !== null) {
+ dispatch('getLastCommitData', tree);
+ }
+
+ pushState(endpoint);
+ })
+ .catch(() => {
+ flash('Error loading tree data. Please try again.');
+ commit(types.TOGGLE_LOADING, tree);
+ });
+};
+
+export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
+ if (tree.opened) {
+ // send empty data to clear the tree
+ const data = { trees: [], blobs: [], submodules: [] };
+
+ pushState(tree.parentTreeUrl);
+
+ commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
+ dispatch('updateDirectoryData', { data, tree });
+ } else {
+ commit(types.SET_PREVIOUS_URL, endpoint);
+ dispatch('getTreeData', { endpoint, tree });
+ }
+
+ commit(types.TOGGLE_TREE_OPEN, tree);
+};
+
+export const clickedTreeRow = ({ commit, dispatch }, row) => {
+ if (row.type === 'tree') {
+ dispatch('toggleTreeOpen', {
+ endpoint: row.url,
+ tree: row,
+ });
+ } else if (row.type === 'submodule') {
+ commit(types.TOGGLE_LOADING, row);
+
+ gl.utils.visitUrl(row.url);
+ } else if (row.type === 'blob' && row.opened) {
+ dispatch('setFileActive', row);
+ } else {
+ dispatch('getFileData', row);
+ }
+};
+
+export const createTempTree = ({ state, commit, dispatch }, name) => {
+ let tree = state;
+ const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
+
+ dirNames.forEach((dirName) => {
+ const foundEntry = findEntry(tree, 'tree', dirName);
+
+ if (!foundEntry) {
+ const tmpEntry = createTemp({
+ name: dirName,
+ path: tree.path,
+ type: 'tree',
+ level: tree.level !== undefined ? tree.level + 1 : 0,
+ });
+
+ commit(types.CREATE_TMP_TREE, {
+ parent: tree,
+ tmpEntry,
+ });
+ commit(types.TOGGLE_TREE_OPEN, tmpEntry);
+
+ tree = tmpEntry;
+ } else {
+ tree = foundEntry;
+ }
+ });
+
+ if (tree.tempFile) {
+ dispatch('createTempFile', {
+ tree,
+ name: '.gitkeep',
+ });
+ }
+};
+
+export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
+ if (tree.lastCommitPath === null || getters.isCollapsed) return;
+
+ service.getTreeLastCommit(tree.lastCommitPath)
+ .then((res) => {
+ const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
+
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
+
+ return res.json();
+ })
+ .then((data) => {
+ data.forEach((lastCommit) => {
+ const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
+
+ if (entry) {
+ commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
+ }
+ });
+
+ dispatch('getLastCommitData', tree);
+ })
+ .catch(() => flash('Error fetching log data.'));
+};
+
+export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
+ const level = tree.level !== undefined ? tree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+ const createEntry = (entry, type) => createOrMergeEntry({
+ tree,
+ entry,
+ level,
+ type,
+ parentTreeUrl,
+ });
+
+ const formattedData = [
+ ...data.trees.map(t => createEntry(t, 'tree')),
+ ...data.submodules.map(m => createEntry(m, 'submodule')),
+ ...data.blobs.map(b => createEntry(b, 'blob')),
+ ];
+
+ commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
+};
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
new file mode 100644
index 00000000000..1ed05ac6e35
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/getters.js
@@ -0,0 +1,36 @@
+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);
+};
diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/repo/stores/index.js
new file mode 100644
index 00000000000..6ac9bfd8189
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+});
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js
new file mode 100644
index 00000000000..bc3390f1506
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutation_types.js
@@ -0,0 +1,30 @@
+export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
+export const TOGGLE_LOADING = 'TOGGLE_LOADING';
+export const SET_COMMIT_REF = 'SET_COMMIT_REF';
+export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
+export const SET_ROOT = 'SET_ROOT';
+export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
+export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
+
+// Tree mutation types
+export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
+export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
+export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
+export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
+
+// File mutation types
+export const SET_FILE_DATA = 'SET_FILE_DATA';
+export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
+export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
+export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
+export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
+export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
+
+// Viewer mutation types
+export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
+export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
+
+export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js
new file mode 100644
index 00000000000..ae2ba5bedf7
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations.js
@@ -0,0 +1,61 @@
+import * as types from './mutation_types';
+import fileMutations from './mutations/file';
+import treeMutations from './mutations/tree';
+import branchMutations from './mutations/branch';
+
+export default {
+ [types.SET_INITIAL_DATA](state, data) {
+ Object.assign(state, data);
+ },
+ [types.SET_PREVIEW_MODE](state) {
+ Object.assign(state, {
+ currentBlobView: 'repo-preview',
+ });
+ },
+ [types.SET_EDIT_MODE](state) {
+ Object.assign(state, {
+ currentBlobView: 'repo-editor',
+ });
+ },
+ [types.TOGGLE_LOADING](state, entry) {
+ Object.assign(entry, {
+ loading: !entry.loading,
+ });
+ },
+ [types.TOGGLE_EDIT_MODE](state) {
+ Object.assign(state, {
+ editMode: !state.editMode,
+ });
+ },
+ [types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
+ Object.assign(state, {
+ discardPopupOpen,
+ });
+ },
+ [types.SET_COMMIT_REF](state, ref) {
+ Object.assign(state, {
+ currentRef: ref,
+ });
+ },
+ [types.SET_ROOT](state, isRoot) {
+ Object.assign(state, {
+ isRoot,
+ isInitialRoot: isRoot,
+ });
+ },
+ [types.SET_PREVIOUS_URL](state, previousUrl) {
+ Object.assign(state, {
+ previousUrl,
+ });
+ },
+ [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
+ Object.assign(entry.lastCommit, {
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ updatedAt: lastCommit.commit.authored_date,
+ });
+ },
+ ...fileMutations,
+ ...treeMutations,
+ ...branchMutations,
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js
new file mode 100644
index 00000000000..d8229e8a620
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/branch.js
@@ -0,0 +1,9 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_BRANCH](state, currentBranch) {
+ Object.assign(state, {
+ currentBranch,
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/repo/stores/mutations/file.js
new file mode 100644
index 00000000000..f9ba80b9dc2
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/file.js
@@ -0,0 +1,54 @@
+import * as types from '../mutation_types';
+import { findIndexOfFile } from '../utils';
+
+export default {
+ [types.SET_FILE_ACTIVE](state, { file, active }) {
+ Object.assign(file, {
+ active,
+ });
+ },
+ [types.TOGGLE_FILE_OPEN](state, file) {
+ Object.assign(file, {
+ opened: !file.opened,
+ });
+
+ if (file.opened) {
+ state.openFiles.push(file);
+ } else {
+ state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
+ }
+ },
+ [types.SET_FILE_DATA](state, { data, file }) {
+ Object.assign(file, {
+ blamePath: data.blame_path,
+ commitsPath: data.commits_path,
+ permalink: data.permalink,
+ rawPath: data.raw_path,
+ binary: data.binary,
+ html: data.html,
+ renderError: data.render_error,
+ });
+ },
+ [types.SET_FILE_RAW_DATA](state, { file, raw }) {
+ Object.assign(file, {
+ raw,
+ });
+ },
+ [types.UPDATE_FILE_CONTENT](state, { file, content }) {
+ const changed = content !== file.raw;
+
+ Object.assign(file, {
+ content,
+ changed,
+ });
+ },
+ [types.DISCARD_FILE_CHANGES](state, file) {
+ Object.assign(file, {
+ content: '',
+ changed: false,
+ });
+ },
+ [types.CREATE_TMP_FILE](state, { file, parent }) {
+ parent.tree.push(file);
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js
new file mode 100644
index 00000000000..130221c9fda
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/tree.js
@@ -0,0 +1,27 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.TOGGLE_TREE_OPEN](state, tree) {
+ Object.assign(tree, {
+ opened: !tree.opened,
+ });
+ },
+ [types.SET_DIRECTORY_DATA](state, { data, tree }) {
+ Object.assign(tree, {
+ tree: data,
+ });
+ },
+ [types.SET_PARENT_TREE_URL](state, url) {
+ Object.assign(state, {
+ parentTreeUrl: url,
+ });
+ },
+ [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
+ Object.assign(tree, {
+ lastCommitPath: url,
+ });
+ },
+ [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
+ parent.tree.push(tmpEntry);
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
deleted file mode 100644
index 38df1e3e0d2..00000000000
--- a/app/assets/javascripts/repo/stores/repo_store.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import Helper from '../helpers/repo_helper';
-import Service from '../services/repo_service';
-
-const RepoStore = {
- monacoLoading: false,
- service: '',
- canCommit: false,
- onTopOfBranch: false,
- editMode: false,
- isRoot: null,
- isInitialRoot: null,
- prevURL: '',
- projectId: '',
- projectName: '',
- projectUrl: '',
- branchUrl: '',
- blobRaw: '',
- currentBlobView: 'repo-preview',
- openedFiles: [],
- submitCommitsLoading: false,
- dialog: {
- open: false,
- title: '',
- status: false,
- },
- showNewBranchDialog: false,
- activeFile: Helper.getDefaultActiveFile(),
- activeFileIndex: 0,
- activeLine: -1,
- activeFileLabel: 'Raw',
- files: [],
- isCommitable: false,
- binary: false,
- currentBranch: '',
- startNewMR: false,
- currentHash: '',
- currentShortHash: '',
- customBranchURL: '',
- newMrTemplateUrl: '',
- branchChanged: false,
- commitMessage: '',
- path: '',
- loading: {
- tree: false,
- blob: false,
- },
-
- setBranchHash() {
- return Service.getBranch()
- .then((data) => {
- if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) {
- RepoStore.branchChanged = true;
- }
- RepoStore.currentHash = data.commit.id;
- RepoStore.currentShortHash = data.commit.short_id;
- });
- },
-
- // mutations
- checkIsCommitable() {
- RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
- },
-
- toggleRawPreview() {
- RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
- RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
- },
-
- setActiveFiles(file) {
- if (RepoStore.isActiveFile(file)) return;
- RepoStore.openedFiles = RepoStore.openedFiles
- .map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
-
- RepoStore.setActiveToRaw();
-
- if (file.binary) {
- RepoStore.blobRaw = file.base64;
- } else if (file.newContent || file.plain) {
- RepoStore.blobRaw = file.newContent || file.plain;
- } else {
- Service.getRaw(file)
- .then((rawResponse) => {
- RepoStore.blobRaw = rawResponse.data;
- Helper.findOpenedFileFromActive().plain = rawResponse.data;
- }).catch(Helper.loadingError);
- }
-
- if (!file.loading && !file.tempFile) {
- Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
- }
- RepoStore.binary = file.binary;
- RepoStore.setActiveLine(-1);
- },
-
- setFileActivity(file, openedFile, i) {
- const activeFile = openedFile;
- activeFile.active = file.id === activeFile.id;
-
- if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
-
- return activeFile;
- },
-
- setActiveFile(activeFile, i) {
- RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
- RepoStore.activeFileIndex = i;
- },
-
- setActiveLine(activeLine) {
- if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
- },
-
- setActiveToRaw() {
- RepoStore.activeFile.raw = false;
- // can't get vue to listen to raw for some reason so RepoStore for now.
- RepoStore.activeFileLabel = 'Display source';
- },
-
- removeFromOpenedFiles(file) {
- if (file.type === 'tree') return;
- let foundIndex;
- RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
- if (openedFile.path === file.path) foundIndex = i;
- return openedFile.path !== file.path;
- });
-
- // remove the file from the sidebar if it is a tempFile
- if (file.tempFile) {
- RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path));
- }
-
- // now activate the right tab based on what you closed.
- if (RepoStore.openedFiles.length === 0) {
- RepoStore.activeFile = {};
- return;
- }
-
- if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
- RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
- return;
- }
-
- if (foundIndex && foundIndex > 0) {
- RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
- }
- },
-
- addToOpenedFiles(file) {
- const openFile = file;
-
- const openedFilesAlreadyExists = RepoStore.openedFiles
- .some(openedFile => openedFile.path === openFile.path);
-
- if (openedFilesAlreadyExists) return;
-
- openFile.changed = false;
- openFile.active = true;
- RepoStore.openedFiles.push(openFile);
- },
-
- setActiveFileContents(contents) {
- if (!RepoStore.editMode) return;
- const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
- RepoStore.activeFile.newContent = contents;
- RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
- currentFile.changed = RepoStore.activeFile.changed;
- currentFile.newContent = contents;
- },
-
- toggleBlobView() {
- RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
- },
-
- setViewToPreview() {
- RepoStore.currentBlobView = 'repo-preview';
- },
-
- // getters
-
- isActiveFile(file) {
- return file && file.id === RepoStore.activeFile.id;
- },
-
- isPreviewView() {
- return RepoStore.currentBlobView === 'repo-preview';
- },
-};
-
-export default RepoStore;
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js
new file mode 100644
index 00000000000..0068834831e
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/state.js
@@ -0,0 +1,24 @@
+export default () => ({
+ canCommit: false,
+ currentBranch: '',
+ currentBlobView: 'repo-preview',
+ currentRef: '',
+ discardPopupOpen: false,
+ editMode: false,
+ endpoints: {},
+ isRoot: false,
+ isInitialRoot: false,
+ lastCommitPath: '',
+ loading: false,
+ onTopOfBranch: false,
+ openFiles: [],
+ path: '',
+ project: {
+ id: 0,
+ name: '',
+ url: '',
+ },
+ parentTreeUrl: '',
+ previousUrl: '',
+ tree: [],
+});
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js
new file mode 100644
index 00000000000..fae1f4439a9
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/utils.js
@@ -0,0 +1,127 @@
+export const dataStructure = () => ({
+ id: '',
+ key: '',
+ type: '',
+ name: '',
+ url: '',
+ path: '',
+ level: 0,
+ tempFile: false,
+ icon: '',
+ tree: [],
+ loading: false,
+ opened: false,
+ active: false,
+ changed: false,
+ lastCommitPath: '',
+ lastCommit: {
+ url: '',
+ message: '',
+ updatedAt: '',
+ },
+ tree_url: '',
+ blamePath: '',
+ commitsPath: '',
+ permalink: '',
+ rawPath: '',
+ binary: false,
+ html: '',
+ raw: '',
+ content: '',
+ parentTreeUrl: '',
+ renderError: false,
+ base64: false,
+});
+
+export const decorateData = (entity) => {
+ const {
+ id,
+ type,
+ url,
+ name,
+ icon,
+ tree_url,
+ path,
+ renderError,
+ content = '',
+ tempFile = false,
+ active = false,
+ opened = false,
+ changed = false,
+ parentTreeUrl = '',
+ level = 0,
+ base64 = false,
+ } = entity;
+
+ return {
+ ...dataStructure(),
+ id,
+ key: `${name}-${type}-${id}`,
+ type,
+ name,
+ url,
+ tree_url,
+ path,
+ level,
+ tempFile,
+ icon: `fa-${icon}`,
+ opened,
+ active,
+ parentTreeUrl,
+ changed,
+ renderError,
+ content,
+ base64,
+ };
+};
+
+export const findEntry = (state, type, name) => state.tree.find(
+ f => f.type === type && f.name === name,
+);
+export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
+
+export const setPageTitle = (title) => {
+ document.title = title;
+};
+
+export const pushState = (url) => {
+ history.pushState({ url }, '', url);
+};
+
+export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
+ const treePath = path ? `${path}/${name}` : name;
+
+ return decorateData({
+ id: new Date().getTime().toString(),
+ name,
+ type,
+ tempFile: true,
+ path: treePath,
+ icon: type === 'tree' ? 'folder' : 'file-text-o',
+ changed,
+ content,
+ parentTreeUrl: '',
+ level,
+ base64,
+ renderError: base64,
+ });
+};
+
+export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
+ const found = findEntry(tree, type, entry.name);
+
+ if (found) {
+ return Object.assign({}, found, {
+ id: entry.id,
+ url: entry.url,
+ tempFile: false,
+ });
+ }
+
+ return decorateData({
+ ...entry,
+ type,
+ parentTreeUrl,
+ level,
+ });
+};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index f15452ec683..9dec5d7645a 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -162,13 +162,19 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
items = [
{
header: "" + name
- }, {
+ }
+ ];
+ const issueItems = [
+ {
text: 'Issues assigned to me',
url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
url: issuesPath + "/?author_username=" + userName
- }, 'separator', {
+ }
+ ];
+ const mergeRequestItems = [
+ {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_username=" + userName
}, {
@@ -176,6 +182,11 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
url: mrPath + "/?author_username=" + userName
}
];
+ if (options.issuesDisabled) {
+ items = items.concat(mergeRequestItems);
+ } else {
+ items = items.concat(...issueItems, 'separator', ...mergeRequestItems);
+ }
if (!name) {
items.splice(0, 1);
}
@@ -408,6 +419,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
+ issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path')
};
}
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index 8635ccece6e..d34a21b37e1 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -1,34 +1,26 @@
-function expandSectionParent($section, $content) {
- $section.addClass('expanded');
- $content.off('animationend.expandSectionParent');
-}
-
function expandSection($section) {
$section.find('.js-settings-toggle').text('Collapse');
-
- const $content = $section.find('.settings-content');
- $content.addClass('expanded').off('scroll.expandSection').scrollTop(0);
-
- if ($content.hasClass('no-animate')) {
- expandSectionParent($section, $content);
- } else {
- $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content));
+ $section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
+ $section.addClass('expanded');
+ if (!$section.hasClass('no-animate')) {
+ $section.addClass('animating')
+ .one('animationend.animateSection', () => $section.removeClass('animating'));
}
}
function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand');
-
- const $content = $section.find('.settings-content');
- $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section));
-
+ $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded');
+ if (!$section.hasClass('no-animate')) {
+ $section.addClass('animating')
+ .one('animationend.animateSection', () => $section.removeClass('animating'));
+ }
}
function toggleSection($section) {
- const $content = $section.find('.settings-content');
- $content.removeClass('no-animate');
- if ($content.hasClass('expanded')) {
+ $section.removeClass('no-animate');
+ if ($section.hasClass('expanded')) {
closeSection($section);
} else {
expandSection($section);
@@ -39,10 +31,19 @@ export default function initSettingsPanels() {
$('.settings').each((i, elm) => {
const $section = $(elm);
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
- $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
+
+ if (!$section.hasClass('expanded')) {
+ $section.find('.settings-content').on('scroll.expandSection', () => {
+ $section.removeClass('no-animate');
+ expandSection($section);
+ });
+ }
});
if (location.hash) {
- expandSection($(location.hash));
+ const $target = $(location.hash);
+ if ($target.length && $target.hasClass('.settings')) {
+ expandSection($target);
+ }
}
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index fc97938e3d1..4f4f606d293 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -4,6 +4,7 @@
import _ from 'underscore';
import 'mousetrap';
import ShortcutsNavigation from './shortcuts_navigation';
+import { CopyAsGFM } from './behaviors/copy_as_gfm';
export default class ShortcutsIssuable extends ShortcutsNavigation {
constructor(isMergeRequest) {
@@ -33,8 +34,8 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
return false;
}
- const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
- const selected = window.gl.CopyAsGFM.nodeToGFM(el);
+ const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
+ const selected = CopyAsGFM.nodeToGFM(el);
if (selected.trim() === '') {
return false;
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 22a9a34dda3..6ee4d487c0b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,10 +1,12 @@
<script>
import Flash from '../../../flash';
import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
editForm,
+ Icon,
},
props: {
isConfidential: {
@@ -26,11 +28,8 @@ export default {
};
},
computed: {
- faEye() {
- const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye';
- return {
- [eye]: true,
- };
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
},
methods: {
@@ -49,7 +48,11 @@ export default {
<template>
<div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
- <i class="fa" :class="faEye" aria-hidden="true"></i>
+ <icon
+ :name="confidentialityIcon"
+ :size="16"
+ aria-hidden="true">
+ </icon>
</div>
<div class="title hide-collapsed">
Confidentiality
@@ -70,11 +73,21 @@ export default {
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value sidebar-item-value">
- <i class="fa fa-eye sidebar-item-icon"></i>
+ <icon
+ name="eye"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline">
+ </icon>
Not confidential
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
- <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
+ <icon
+ name="eye-slash"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline is-active">
+ </icon>
This issue is confidential
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index c4b2900e020..9aff53cf8af 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -2,6 +2,7 @@
/* global Flash */
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
@@ -35,11 +36,12 @@ export default {
components: {
editForm,
+ Icon,
},
computed: {
- lockIconClass() {
- return this.isLocked ? 'fa-lock' : 'fa-unlock';
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
},
isLockDialogOpen() {
@@ -66,11 +68,12 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
- <i
- class="fa"
- :class="lockIconClass"
+ <icon
+ :name="lockIcon"
+ :size="16"
aria-hidden="true"
- ></i>
+ class="sidebar-item-icon is-active">
+ </icon>
</div>
<div class="title hide-collapsed">
@@ -98,10 +101,12 @@ export default {
v-if="isLocked"
class="value sidebar-item-value"
>
- <i
+ <icon
+ name="lock"
+ :size="16"
aria-hidden="true"
- class="fa fa-lock sidebar-item-icon is-active"
- ></i>
+ class="sidebar-item-icon inline is-active">
+ </icon>
{{ __('Locked') }}
</div>
@@ -109,10 +114,12 @@ export default {
v-else
class="no-value sidebar-item-value hide-collapsed"
>
- <i
+ <icon
+ name="lock-open"
+ :size="16"
aria-hidden="true"
- class="fa fa-unlock sidebar-item-icon"
- ></i>
+ class="sidebar-item-icon inline">
+ </icon>
{{ __('Unlocked') }}
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
new file mode 100644
index 00000000000..b8510a6ce3a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -0,0 +1,125 @@
+<script>
+import { __, n__, sprintf } from '../../../locale';
+import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
+
+export default {
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ participants: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ numberOfLessParticipants: {
+ type: Number,
+ required: false,
+ default: 7,
+ },
+ },
+ data() {
+ return {
+ isShowingMoreParticipants: false,
+ };
+ },
+ components: {
+ loadingIcon,
+ userAvatarImage,
+ },
+ computed: {
+ lessParticipants() {
+ return this.participants.slice(0, this.numberOfLessParticipants);
+ },
+ visibleParticipants() {
+ return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
+ },
+ hasMoreParticipants() {
+ return this.participants.length > this.numberOfLessParticipants;
+ },
+ toggleLabel() {
+ let label = '';
+ if (this.isShowingMoreParticipants) {
+ label = __('- show less');
+ } else {
+ label = sprintf(__('+ %{moreCount} more'), {
+ moreCount: this.participants.length - this.numberOfLessParticipants,
+ });
+ }
+
+ return label;
+ },
+ participantLabel() {
+ return sprintf(
+ n__('%{count} participant', '%{count} participants', this.participants.length),
+ { count: this.loading ? '' : this.participantCount },
+ );
+ },
+ participantCount() {
+ return this.participants.length;
+ },
+ },
+ methods: {
+ toggleMoreParticipants() {
+ this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa fa-users"
+ aria-hidden="true">
+ </i>
+ <loading-icon
+ v-if="loading"
+ class="js-participants-collapsed-loading-icon" />
+ <span
+ v-else
+ class="js-participants-collapsed-count">
+ {{ participantCount }}
+ </span>
+ </div>
+ <div class="title hide-collapsed">
+ <loading-icon
+ v-if="loading"
+ :inline="true"
+ class="js-participants-expanded-loading-icon" />
+ {{ participantLabel }}
+ </div>
+ <div class="participants-list hide-collapsed">
+ <div
+ v-for="participant in visibleParticipants"
+ :key="participant.id"
+ class="participants-author js-participants-author">
+ <a
+ class="author_link"
+ :href="participant.web_url">
+ <user-avatar-image
+ :lazy="true"
+ :img-src="participant.avatar_url"
+ css-classes="avatar-inline"
+ :size="24"
+ :tooltip-text="participant.name"
+ tooltip-placement="bottom" />
+ </a>
+ </div>
+ </div>
+ <div
+ v-if="hasMoreParticipants"
+ class="participants-more hide-collapsed">
+ <button
+ type="button"
+ class="btn-transparent btn-blank js-toggle-participants-button"
+ @click="toggleMoreParticipants">
+ {{ toggleLabel }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
new file mode 100644
index 00000000000..c1296b28db7
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
@@ -0,0 +1,26 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import participants from './participants.vue';
+
+export default {
+ data() {
+ return {
+ mediator: new Mediator(),
+ store: new Store(),
+ };
+ },
+ components: {
+ participants,
+ },
+};
+</script>
+
+<template>
+ <div class="block participants">
+ <participants
+ :loading="store.isFetching.participants"
+ :participants="store.participants"
+ :number-of-less-participants="7" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
new file mode 100644
index 00000000000..25acc099699
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -0,0 +1,46 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import eventHub from '../../event_hub';
+import Flash from '../../../flash';
+import { __ } from '../../../locale';
+import subscriptions from './subscriptions.vue';
+
+export default {
+ data() {
+ return {
+ mediator: new Mediator(),
+ store: new Store(),
+ };
+ },
+
+ components: {
+ subscriptions,
+ },
+
+ methods: {
+ onToggleSubscription() {
+ this.mediator.toggleSubscription()
+ .catch(() => {
+ Flash(__('Error occurred when toggling the notification subscription'));
+ });
+ },
+ },
+
+ created() {
+ eventHub.$on('toggleSubscription', this.onToggleSubscription);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('toggleSubscription', this.onToggleSubscription);
+ },
+};
+</script>
+
+<template>
+ <div class="block subscriptions">
+ <subscriptions
+ :loading="store.isFetching.subscriptions"
+ :subscribed="store.subscribed" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
new file mode 100644
index 00000000000..940e1764f3d
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -0,0 +1,64 @@
+<script>
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
+import loadingButton from '../../../vue_shared/components/loading_button.vue';
+
+export default {
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ subscribed: {
+ type: Boolean,
+ required: false,
+ },
+ id: {
+ type: Number,
+ required: false,
+ },
+ },
+ components: {
+ loadingButton,
+ },
+ computed: {
+ buttonLabel() {
+ let label;
+ if (this.subscribed === false) {
+ label = __('Subscribe');
+ } else if (this.subscribed === true) {
+ label = __('Unsubscribe');
+ }
+
+ return label;
+ },
+ },
+ methods: {
+ toggleSubscription() {
+ eventHub.$emit('toggleSubscription', this.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa fa-rss"
+ aria-hidden="true">
+ </i>
+ </div>
+ <span class="issuable-header-text hide-collapsed pull-left">
+ {{ __('Notifications') }}
+ </span>
+ <loading-button
+ ref="loadingButton"
+ class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
+ :loading="loading"
+ :label="buttonLabel"
+ @click="toggleSubscription"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 604648407a4..37c97225bfd 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -7,6 +7,7 @@ export default class SidebarService {
constructor(endpointMap) {
if (!SidebarService.singleton) {
this.endpoint = endpointMap.endpoint;
+ this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
@@ -36,6 +37,10 @@ export default class SidebarService {
});
}
+ toggleSubscription() {
+ return Vue.http.post(this.toggleSubscriptionEndpoint);
+ }
+
moveIssue(moveToProjectId) {
return Vue.http.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId,
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 09b9d75c02d..2650bb725d4 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
+import sidebarParticipants from './components/participants/sidebar_participants.vue';
+import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
}).$mount(el);
}
+function mountParticipantsComponent() {
+ const el = document.querySelector('.js-sidebar-participants-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarParticipants,
+ },
+ render: createElement => createElement('sidebar-participants', {}),
+ });
+}
+
+function mountSubscriptionsComponent() {
+ const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarSubscriptions,
+ },
+ render: createElement => createElement('sidebar-subscriptions', {}),
+ });
+}
+
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
@@ -63,6 +95,8 @@ function domContentLoaded() {
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
+ mountParticipantsComponent();
+ mountSubscriptionsComponent();
new SidebarMoveIssue(
mediator,
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index ede3a0de144..2bda5a47791 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -8,6 +8,7 @@ export default class SidebarMediator {
this.store = new Store(options);
this.service = new Service({
endpoint: options.endpoint,
+ toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
});
@@ -39,10 +40,25 @@ export default class SidebarMediator {
.then((data) => {
this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data);
+ this.store.setParticipantsData(data);
+ this.store.setSubscriptionsData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
}
+ toggleSubscription() {
+ this.store.setFetchingState('subscriptions', true);
+ return this.service.toggleSubscription()
+ .then(() => {
+ this.store.setSubscribedState(!this.store.subscribed);
+ this.store.setFetchingState('subscriptions', false);
+ })
+ .catch((err) => {
+ this.store.setFetchingState('subscriptions', false);
+ throw err;
+ });
+ }
+
fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm)
.then(response => response.json())
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index d5d04103f3f..3150221b685 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -12,10 +12,14 @@ export default class SidebarStore {
this.assignees = [];
this.isFetching = {
assignees: true,
+ participants: true,
+ subscriptions: true,
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
+ this.participants = [];
+ this.subscribed = null;
SidebarStore.singleton = this;
}
@@ -37,6 +41,20 @@ export default class SidebarStore {
this.humanTotalTimeSpent = data.human_total_time_spent;
}
+ setParticipantsData(data) {
+ this.isFetching.participants = false;
+ this.participants = data.participants || [];
+ }
+
+ setSubscriptionsData(data) {
+ this.isFetching.subscriptions = false;
+ this.subscribed = data.subscribed || false;
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(assignee);
@@ -61,6 +79,10 @@ export default class SidebarStore {
this.autocompleteProjects = projects;
}
+ setSubscribedState(subscribed) {
+ this.subscribed = subscribed;
+ }
+
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 2bf7a3a5d61..8e931995fc6 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -3,9 +3,10 @@
* and controllable by a public API.
*/
-class SmartInterval {
+export default class SmartInterval {
/**
- * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { function } opts.callback Function that returns a promise, called on each iteration
+ * unless still in progress (required)
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
@@ -42,13 +43,16 @@ class SmartInterval {
const cfg = this.cfg;
const state = this.state;
- if (cfg.immediateExecution) {
+ if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false;
- cfg.callback();
+ this.triggerCallback();
}
state.intervalId = window.setInterval(() => {
- cfg.callback();
+ if (this.isLoading) {
+ return;
+ }
+ this.triggerCallback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
@@ -76,7 +80,7 @@ class SmartInterval {
// start a timer, using the existing interval
resume() {
- this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
+ this.stopTimer(); // stop existing timer, in case timer was not previously stopped
this.start();
}
@@ -104,6 +108,18 @@ class SmartInterval {
this.initPageUnloadHandling();
}
+ triggerCallback() {
+ this.isLoading = true;
+ this.cfg.callback()
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch((err) => {
+ this.isLoading = false;
+ throw err;
+ });
+ }
+
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
@@ -154,4 +170,3 @@ class SmartInterval {
}
}
-window.gl.SmartInterval = SmartInterval;
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index bb4d68fcd49..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,45 +0,0 @@
-class Subscription {
- constructor(containerElm) {
- this.containerElm = containerElm;
-
- const subscribeButton = containerElm.querySelector('.js-subscribe-button');
- if (subscribeButton) {
- // remove class so we don't bind twice
- subscribeButton.classList.remove('js-subscribe-button');
- subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
- }
- }
-
- toggleSubscription(event) {
- const button = event.currentTarget;
- const buttonSpan = button.querySelector('span');
- if (!buttonSpan || button.classList.contains('disabled')) {
- return;
- }
- button.classList.add('disabled');
-
- const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
- const toggleActionUrl = this.containerElm.dataset.url;
-
- $.post(toggleActionUrl, () => {
- button.classList.remove('disabled');
-
- // hack to allow this to work with the issue boards Vue object
- if (document.querySelector('html').classList.contains('issue-boards-page')) {
- gl.issueBoards.boardStoreIssueSet(
- 'subscribed',
- !gl.issueBoards.BoardsStore.detail.issue.subscribed,
- );
- } else {
- buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
- }
- });
- }
-
- static bindAll(selector) {
- [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
- }
-}
-
-window.gl = window.gl || {};
-window.gl.Subscription = Subscription;
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 37e39ce5477..1ab4c2229ca 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,33 +1,24 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
+export default function subscriptionSelect() {
+ $('.js-subscription-event').each((i, element) => {
+ const fieldName = $(element).data('field-name');
-class SubscriptionSelect {
- constructor() {
- $('.js-subscription-event').each(function(i, el) {
- var fieldName;
- fieldName = $(el).data("field-name");
- return $(el).glDropdown({
- selectable: true,
- fieldName: fieldName,
- toggleLabel: (function(_this) {
- return function(selected, el, instance) {
- var $item, label;
- label = 'Subscription';
- $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- };
- })(this),
- clicked: function(options) {
- return options.e.preventDefault();
- },
- id: function(obj, el) {
- return $(el).data("id");
+ return $(element).glDropdown({
+ selectable: true,
+ fieldName,
+ toggleLabel(selected, el, instance) {
+ let label = 'Subscription';
+ const $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
}
- });
+ return label;
+ },
+ clicked(options) {
+ return options.e.preventDefault();
+ },
+ id(obj, el) {
+ return $(el).data('id');
+ },
});
- }
+ });
}
-
-window.SubscriptionSelect = SubscriptionSelect;
diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js
index 8875590f0f2..a55a338eea8 100644
--- a/app/assets/javascripts/test_utils/index.js
+++ b/app/assets/javascripts/test_utils/index.js
@@ -1,6 +1,8 @@
import 'core-js/es6/map';
import 'core-js/es6/set';
import simulateDrag from './simulate_drag';
+import simulateInput from './simulate_input';
// Export to global space for rspec to use
window.simulateDrag = simulateDrag;
+window.simulateInput = simulateInput;
diff --git a/app/assets/javascripts/test_utils/simulate_input.js b/app/assets/javascripts/test_utils/simulate_input.js
new file mode 100644
index 00000000000..90c1b7cb57e
--- /dev/null
+++ b/app/assets/javascripts/test_utils/simulate_input.js
@@ -0,0 +1,23 @@
+function triggerEvents(input) {
+ input.dispatchEvent(new Event('keydown'));
+ input.dispatchEvent(new Event('keypress'));
+ input.dispatchEvent(new Event('input'));
+ input.dispatchEvent(new Event('keyup'));
+}
+
+export default function simulateInput(target, text) {
+ const input = document.querySelector(target);
+ if (!input || !input.matches('textarea, input')) {
+ return false;
+ }
+
+ if (text.length > 0) {
+ Array.prototype.forEach.call(text, (char) => {
+ input.value += char;
+ triggerEvents(input);
+ });
+ } else {
+ triggerEvents(input);
+ }
+ return true;
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index 219ff94924e..13e4cb5717e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -1,5 +1,5 @@
import tooltip from '../../vue_shared/directives/tooltip';
-import '../../lib/utils/text_utility';
+import { pluralize } from '../../lib/utils/text_utility';
export default {
name: 'MRWidgetHeader',
@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
- return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
+ return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
deleted file mode 100644
index c79b5c720eb..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import PipelineStage from '../../pipelines/components/stage.vue';
-import ciIcon from '../../vue_shared/components/ci_icon.vue';
-import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
-
-export default {
- name: 'MRWidgetPipeline',
- props: {
- mr: { type: Object, required: true },
- },
- components: {
- 'pipeline-stage': PipelineStage,
- ciIcon,
- },
- computed: {
- hasPipeline() {
- return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
- },
- hasCIError() {
- const { hasCI, ciStatus } = this.mr;
-
- return hasCI && !ciStatus;
- },
- svg() {
- return statusIconEntityMap.icon_status_failed;
- },
- stageText() {
- return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
- },
- status() {
- return this.mr.pipeline.details.status || {};
- },
- },
- template: `
- <div
- v-if="hasPipeline || hasCIError"
- class="mr-widget-heading">
- <div class="ci-widget media">
- <template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
- <span
- v-html="svg"
- aria-hidden="true"></span>
- </div>
- <div class="media-body">
- Could not connect to the CI server. Please check your settings and try again
- </div>
- </template>
- <template v-else-if="hasPipeline">
- <div class="ci-status-icon append-right-10">
- <a
- class="icon-link"
- :href="this.status.details_path">
- <ci-icon :status="status" />
- </a>
- </div>
- <div class="media-body">
- <span>
- Pipeline
- <a
- :href="mr.pipeline.path"
- class="pipeline-id">#{{mr.pipeline.id}}</a>
- </span>
- <span class="mr-widget-pipeline-graph">
- <span class="stage-cell">
- <div
- v-if="mr.pipeline.details.stages.length > 0"
- v-for="stage in mr.pipeline.details.stages"
- class="stage-container dropdown js-mini-pipeline-graph">
- <pipeline-stage :stage="stage" />
- </div>
- </span>
- </span>
- <span>
- {{mr.pipeline.details.status.label}} for
- <a
- :href="mr.pipeline.commit.commit_path"
- class="commit-sha js-commit-link">
- {{mr.pipeline.commit.short_id}}</a>.
- </span>
- <span
- v-if="mr.pipeline.coverage"
- class="js-mr-coverage">
- Coverage {{mr.pipeline.coverage}}%
- </span>
- </div>
- </template>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
new file mode 100644
index 00000000000..dbc65462377
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -0,0 +1,104 @@
+<script>
+ import pipelineStage from '../../pipelines/components/stage.vue';
+ import ciIcon from '../../vue_shared/components/ci_icon.vue';
+ import icon from '../../vue_shared/components/icon.vue';
+
+ export default {
+ name: 'MRWidgetPipeline',
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ // This prop needs to be camelCase, html attributes are case insensive
+ // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
+ hasCi: {
+ type: Boolean,
+ required: false,
+ },
+ ciStatus: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ pipelineStage,
+ ciIcon,
+ icon,
+ },
+ computed: {
+ hasPipeline() {
+ return this.pipeline && Object.keys(this.pipeline).length > 0;
+ },
+ hasCIError() {
+ return this.hasCi && !this.ciStatus;
+ },
+ status() {
+ return this.pipeline.details &&
+ this.pipeline.details.status ? this.pipeline.details.status : {};
+ },
+ hasStages() {
+ return this.pipeline.details &&
+ this.pipeline.details.stages &&
+ this.pipeline.details.stages.length;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ v-if="hasPipeline || hasCIError"
+ class="mr-widget-heading">
+ <div class="ci-widget media">
+ <template v-if="hasCIError">
+ <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
+ <icon name="status_failed" />
+ </div>
+ <div class="media-body">
+ Could not connect to the CI server. Please check your settings and try again
+ </div>
+ </template>
+ <template v-else-if="hasPipeline">
+ <a
+ class="append-right-10"
+ :href="this.status.details_path">
+ <ci-icon :status="status" />
+ </a>
+
+ <div class="media-body">
+ Pipeline
+ <a
+ :href="pipeline.path"
+ class="pipeline-id">
+ #{{pipeline.id}}
+ </a>
+
+ {{pipeline.details.status.label}} for
+
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link">
+ {{pipeline.commit.short_id}}</a>.
+
+ <span class="mr-widget-pipeline-graph">
+ <span class="stage-cell">
+ <div
+ v-if="hasStages"
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph">
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
+ </span>
+
+ <template v-if="pipeline.coverage">
+ Coverage {{pipeline.coverage}}%
+ </template>
+
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 49340c232c8..5bd8b99420a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -13,7 +13,7 @@ export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
-export { default as WidgetPipeline } from './components/mr_widget_pipeline';
+export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 4f497b204a3..1274db2c4c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -1,3 +1,4 @@
+import SmartInterval from '~/smart_interval';
import Flash from '../flash';
import {
WidgetHeader,
@@ -60,7 +61,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return this.mr.relatedLinks;
+ return !!this.mr.relatedLinks;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
@@ -81,7 +82,7 @@ export default {
return new MRWidgetService(endpoints);
},
checkStatus(cb) {
- this.service.checkStatus()
+ return this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.handleNotification(res);
@@ -97,7 +98,7 @@ export default {
});
},
initPolling() {
- this.pollingInterval = new gl.SmartInterval({
+ this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
startingInterval: 10000,
maxInterval: 30000,
@@ -106,7 +107,7 @@ export default {
});
},
initDeploymentsPolling() {
- this.deploymentsInterval = new gl.SmartInterval({
+ this.deploymentsInterval = new SmartInterval({
callback: this.fetchDeployments,
startingInterval: 30000,
maxInterval: 120000,
@@ -121,7 +122,7 @@ export default {
}
},
fetchDeployments() {
- this.service.fetchDeployments()
+ return this.service.fetchDeployments()
.then(res => res.json())
.then((res) => {
if (res.length) {
@@ -235,7 +236,10 @@ export default {
<mr-widget-header :mr="mr" />
<mr-widget-pipeline
v-if="shouldRenderPipelines"
- :mr="mr" />
+ :pipeline="mr.pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ />
<mr-widget-deployment
v-if="shouldRenderDeployments"
:mr="mr"
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 79c3d335679..99f5c305df5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -11,7 +11,7 @@ export default class MRWidgetService {
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
- this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
+ this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
}
diff --git a/app/assets/javascripts/vue_shared/ci_action_icons.js b/app/assets/javascripts/vue_shared/ci_action_icons.js
deleted file mode 100644
index b21f0ab49fd..00000000000
--- a/app/assets/javascripts/vue_shared/ci_action_icons.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import cancelSVG from 'icons/_icon_action_cancel.svg';
-import retrySVG from 'icons/_icon_action_retry.svg';
-import playSVG from 'icons/_icon_action_play.svg';
-import stopSVG from 'icons/_icon_action_stop.svg';
-
-/**
- * For the provided action returns the respective SVG
- *
- * @param {String} action
- * @return {SVG|String}
- */
-export default function getActionIcon(action) {
- const icons = {
- icon_action_cancel: cancelSVG,
- icon_action_play: playSVG,
- icon_action_retry: retrySVG,
- icon_action_stop: stopSVG,
- };
-
- return icons[action] || '';
-}
diff --git a/app/assets/javascripts/vue_shared/ci_status_icons.js b/app/assets/javascripts/vue_shared/ci_status_icons.js
deleted file mode 100644
index d9d0cad38e4..00000000000
--- a/app/assets/javascripts/vue_shared/ci_status_icons.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
-import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
-import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
-import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
-import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
-import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
-import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
-import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
-import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
-
-import CANCELED_SVG from 'icons/_icon_status_canceled.svg';
-import CREATED_SVG from 'icons/_icon_status_created.svg';
-import FAILED_SVG from 'icons/_icon_status_failed.svg';
-import MANUAL_SVG from 'icons/_icon_status_manual.svg';
-import PENDING_SVG from 'icons/_icon_status_pending.svg';
-import RUNNING_SVG from 'icons/_icon_status_running.svg';
-import SKIPPED_SVG from 'icons/_icon_status_skipped.svg';
-import SUCCESS_SVG from 'icons/_icon_status_success.svg';
-import WARNING_SVG from 'icons/_icon_status_warning.svg';
-
-export const borderlessStatusIconEntityMap = {
- icon_status_canceled: BORDERLESS_CANCELED_SVG,
- icon_status_created: BORDERLESS_CREATED_SVG,
- icon_status_failed: BORDERLESS_FAILED_SVG,
- icon_status_manual: BORDERLESS_MANUAL_SVG,
- icon_status_pending: BORDERLESS_PENDING_SVG,
- icon_status_running: BORDERLESS_RUNNING_SVG,
- icon_status_skipped: BORDERLESS_SKIPPED_SVG,
- icon_status_success: BORDERLESS_SUCCESS_SVG,
- icon_status_warning: BORDERLESS_WARNING_SVG,
-};
-
-export const statusIconEntityMap = {
- icon_status_canceled: CANCELED_SVG,
- icon_status_created: CREATED_SVG,
- icon_status_failed: FAILED_SVG,
- icon_status_manual: MANUAL_SVG,
- icon_status_pending: PENDING_SVG,
- icon_status_running: RUNNING_SVG,
- icon_status_skipped: SKIPPED_SVG,
- icon_status_success: SUCCESS_SVG,
- icon_status_warning: WARNING_SVG,
-};
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 5b6c6e8d0b9..fc795936abf 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -43,7 +43,6 @@
computed: {
cssClass() {
const className = this.status.group;
-
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index ec88119e16c..2a018f38366 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,5 +1,5 @@
<script>
- import { statusIconEntityMap } from '../ci_status_icons';
+ import icon from '../../vue_shared/components/icon.vue';
/**
* Renders CI icon based on API response shared between all places where it is used.
@@ -30,11 +30,11 @@
},
},
- computed: {
- statusIconSvg() {
- return statusIconEntityMap[this.status.icon];
- },
+ components: {
+ icon,
+ },
+ computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
@@ -44,7 +44,8 @@
</script>
<template>
<span
- :class="cssClass"
- v-html="statusIconSvg">
+ :class="cssClass">
+ <icon
+ :name="status.icon"/>
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
new file mode 100644
index 00000000000..8f116233e72
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -0,0 +1,51 @@
+<script>
+
+/* This is a re-usable vue component for rendering a svg sprite
+ icon
+
+ Sample configuration:
+
+ <icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />
+
+*/
+ export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+
+ size: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ computed: {
+ spriteHref() {
+ return `${gon.sprite_icons}#${this.name}`;
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
+ },
+ },
+ };
+</script>
+<template>
+ <svg
+ :class="[iconSizeClass, cssClasses]">
+ <use
+ v-bind="{'xlink:href':spriteHref}"/>
+ </svg>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index 16c0a8efcd2..564fc5029af 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -1,4 +1,6 @@
<script>
+ import Icon from '../../../vue_shared/components/icon.vue';
+
export default {
props: {
isLocked: {
@@ -14,12 +16,16 @@
},
},
+ components: {
+ Icon,
+ },
+
computed: {
- iconClass() {
- return {
- 'fa-eye-slash': this.isConfidential,
- 'fa-lock': this.isLocked,
- };
+ warningIcon() {
+ if (this.isConfidential) return 'eye-slash';
+ if (this.isLocked) return 'lock';
+
+ return '';
},
isLockedAndConfidential() {
@@ -30,12 +36,13 @@
</script>
<template>
<div class="issuable-note-warning">
- <i
- aria-hidden="true"
- class="fa icon"
- :class="iconClass"
- v-if="!isLockedAndConfidential"
- ></i>
+ <icon
+ :name="warningIcon"
+ :size="16"
+ class="icon inline"
+ aria-hidden="true"
+ v-if="!isLockedAndConfidential">
+ </icon>
<span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }}
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index 6670b554faf..247943f83e6 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -26,10 +26,20 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
label: {
type: String,
required: false,
},
+ containerClass: {
+ type: String,
+ required: false,
+ default: 'btn btn-align-content',
+ },
},
components: {
loadingIcon,
@@ -44,10 +54,10 @@ export default {
<template>
<button
- class="btn btn-align-content"
@click="onClick"
type="button"
- :disabled="loading"
+ :class="containerClass"
+ :disabled="loading || disabled"
>
<transition name="fade">
<loading-icon
diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue
index 494fe4468d9..15581d5c2a0 100644
--- a/app/assets/javascripts/vue_shared/components/loading_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue
@@ -18,12 +18,6 @@
required: false,
default: false,
},
-
- class: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
@@ -31,7 +25,7 @@
return this.inline ? 'span' : 'div';
},
cssClass() {
- return `fa-${this.size}x ${this.class}`.trim();
+ return `fa-${this.size}x`;
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 8c0d9b9cda8..ee50ce27c3d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -3,6 +3,7 @@
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
+ import icon from '../icon.vue';
export default {
props: {
@@ -24,6 +25,11 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -37,6 +43,7 @@
components: {
markdownHeader,
markdownToolbar,
+ icon,
},
computed: {
shouldShowReferencedUsers() {
@@ -45,8 +52,10 @@
},
},
methods: {
- toggleMarkdownPreview() {
- this.previewMarkdown = !this.previewMarkdown;
+ showPreviewTab() {
+ if (this.previewMarkdown) return;
+
+ this.previewMarkdown = true;
/*
Can't use `$refs` as the component is technically in the parent component
@@ -54,20 +63,22 @@
*/
const text = this.$slots.textarea[0].elm.value;
- if (!this.previewMarkdown) {
- this.markdownPreview = '';
- } else if (text) {
+ if (text) {
this.markdownPreviewLoading = true;
this.$http.post(this.markdownPreviewPath, { text })
.then(resp => resp.json())
- .then((data) => {
- this.renderMarkdown(data);
- })
+ .then(data => this.renderMarkdown(data))
.catch(() => new Flash('Error loading markdown preview'));
} else {
this.renderMarkdown();
}
},
+
+ showWriteTab() {
+ this.markdownPreview = '';
+ this.previewMarkdown = false;
+ },
+
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
this.markdownPreview = data.body || 'Nothing to preview.';
@@ -104,7 +115,8 @@
ref="gl-form">
<markdown-header
:preview-markdown="previewMarkdown"
- @toggle-markdown="toggleMarkdownPreview" />
+ @preview-markdown="showPreviewTab"
+ @write-markdown="showWriteTab" />
<div
class="md-write-holder"
v-show="!previewMarkdown">
@@ -114,14 +126,15 @@
class="zen-control zen-control-leave js-zen-leave"
href="#"
aria-label="Enter zen mode">
- <i
- class="fa fa-compress"
- aria-hidden="true">
- </i>
+ <icon
+ name="screen-normal"
+ :size="32">
+ </icon>
</a>
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :can-attach-file="canAttachFile"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 5bf2a90cc3b..6c575d8eb49 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,6 +1,7 @@
<script>
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
+ import icon from '../icon.vue';
export default {
props: {
@@ -14,25 +15,34 @@
},
components: {
toolbarButton,
+ icon,
},
methods: {
- toggleMarkdownPreview(e, form) {
- if (form && !form.find('.js-vue-markdown-field').length) {
- return;
- } else if (e.target.blur) {
- e.target.blur();
- }
+ isMarkdownForm(form) {
+ return form && !form.find('.js-vue-markdown-field').length;
+ },
+
+ previewMarkdownTab(event, form) {
+ if (event.target.blur) event.target.blur();
+ if (this.isMarkdownForm(form)) return;
+
+ this.$emit('preview-markdown');
+ },
+
+ writeMarkdownTab(event, form) {
+ if (event.target.blur) event.target.blur();
+ if (this.isMarkdownForm(form)) return;
- this.$emit('toggle-markdown');
+ this.$emit('write-markdown');
},
},
mounted() {
- $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
- $(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ $(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
+ $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
},
beforeDestroy() {
- $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
- $(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ $(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
+ $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
};
</script>
@@ -40,73 +50,74 @@
<template>
<div class="md-header">
<ul class="nav-links clearfix">
- <li :class="{ active: !previewMarkdown }">
+ <li
+ class="md-header-tab"
+ :class="{ active: !previewMarkdown }">
<a
+ class="js-write-link"
href="#md-write-holder"
tabindex="-1"
- @click.prevent="toggleMarkdownPreview($event)">
+ @click.prevent="writeMarkdownTab($event)">
Write
</a>
</li>
- <li :class="{ active: previewMarkdown }">
+ <li
+ class="md-header-tab"
+ :class="{ active: previewMarkdown }">
<a
+ class="js-preview-link"
href="#md-preview-holder"
tabindex="-1"
- @click.prevent="toggleMarkdownPreview($event)">
+ @click.prevent="previewMarkdownTab($event)">
Preview
</a>
</li>
- <li class="pull-right">
- <div class="toolbar-group">
- <toolbar-button
- tag="**"
- button-title="Add bold text"
- icon="bold" />
- <toolbar-button
- tag="*"
- button-title="Add italic text"
- icon="italic" />
- <toolbar-button
- tag="> "
- :prepend="true"
- button-title="Insert a quote"
- icon="quote-right" />
- <toolbar-button
- tag="`"
- tag-block="```"
- button-title="Insert code"
- icon="code" />
- <toolbar-button
- tag="* "
- :prepend="true"
- button-title="Add a bullet list"
- icon="list-ul" />
- <toolbar-button
- tag="1. "
- :prepend="true"
- button-title="Add a numbered list"
- icon="list-ol" />
- <toolbar-button
- tag="* [ ] "
- :prepend="true"
- button-title="Add a task list"
- icon="check-square-o" />
- </div>
- <div class="toolbar-group">
- <button
- v-tooltip
- aria-label="Go full screen"
- class="toolbar-btn js-zen-enter"
- data-container="body"
- tabindex="-1"
- title="Go full screen"
- type="button">
- <i
- aria-hidden="true"
- class="fa fa-arrows-alt fa-fw">
- </i>
- </button>
- </div>
+ <li class="md-header-toolbar">
+ <toolbar-button
+ tag="**"
+ button-title="Add bold text"
+ icon="bold" />
+ <toolbar-button
+ tag="*"
+ button-title="Add italic text"
+ icon="italic" />
+ <toolbar-button
+ tag="> "
+ :prepend="true"
+ button-title="Insert a quote"
+ icon="quote" />
+ <toolbar-button
+ tag="`"
+ tag-block="```"
+ button-title="Insert code"
+ icon="code" />
+ <toolbar-button
+ tag="* "
+ :prepend="true"
+ button-title="Add a bullet list"
+ icon="list-bulleted" />
+ <toolbar-button
+ tag="1. "
+ :prepend="true"
+ button-title="Add a numbered list"
+ icon="list-numbered" />
+ <toolbar-button
+ tag="* [ ] "
+ :prepend="true"
+ button-title="Add a task list"
+ icon="task-done" />
+ <button
+ v-tooltip
+ aria-label="Go full screen"
+ class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
+ data-container="body"
+ tabindex="-1"
+ title="Go full screen"
+ type="button">
+ <icon
+ name="screen-full">
+ </icon>
+ </button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 65fe7bbd94e..ea2509d2839 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -9,6 +9,11 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -41,7 +46,10 @@
are supported
</template>
</div>
- <span class="uploading-container">
+ <span
+ v-if="canAttachFile"
+ class="uploading-container"
+ >
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index f7da7ebfcfe..e3e41f8f0ca 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,5 +1,6 @@
<script>
import tooltip from '../../directives/tooltip';
+ import icon from '../icon.vue';
export default {
props: {
@@ -26,14 +27,12 @@
default: false,
},
},
+ components: {
+ icon,
+ },
directives: {
tooltip,
},
- computed: {
- iconClass() {
- return `fa-${this.icon}`;
- },
- },
};
</script>
@@ -41,7 +40,7 @@
<button
v-tooltip
type="button"
- class="toolbar-btn js-md hidden-xs"
+ class="toolbar-btn js-md"
tabindex="-1"
data-container="body"
:data-md-tag="tag"
@@ -49,10 +48,8 @@
:data-md-prepend="prepend"
:title="buttonTitle"
:aria-label="buttonTitle">
- <i
- aria-hidden="true"
- class="fa fa-fw"
- :class="iconClass">
- </i>
+ <icon
+ :name="icon">
+ </icon>
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
index fc6421fecb9..47efee64c6e 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -115,6 +115,7 @@ export default {
<button
type="button"
class="btn pull-right"
+ :disabled="submitDisabled"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
new file mode 100644
index 00000000000..b06493e6c66
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -0,0 +1,37 @@
+<script>
+ export default {
+ props: {
+ small: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ lines: {
+ type: Number,
+ required: false,
+ default: 6,
+ },
+ },
+ computed: {
+ lineClasses() {
+ return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="animation-container"
+ :class="{
+ 'animation-container-small': small,
+ }"
+ >
+ <div
+ v-for="(css, index) in lineClasses"
+ :key="index"
+ :class="css"
+ >
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index a0025ddb598..7a865587444 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,4 +1,5 @@
import bp from './breakpoints';
+import { slugify } from './lib/utils/text_utility';
export default class Wikis {
constructor() {
@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = gl.text.slugify(slugInput.value);
+ const slug = slugify(slugInput.value);
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 7b1ef003bb2..66212be1b8f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -34,6 +34,7 @@
@import "framework/modal";
@import "framework/pagination";
@import "framework/panels";
+@import "framework/popup";
@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
@@ -56,4 +57,4 @@
@import "framework/icons";
@import "framework/snippets";
@import "framework/memory_graph";
-@import "framework/responsive-tables";
+@import "framework/responsive_tables";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 81439c0d2fe..374988bb590 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -23,11 +23,6 @@
@include webkit-prefix(animation-duration, 2s);
}
- &.spin {
- transform-origin: center;
- animation: spin 4s linear infinite;
- }
-
&.flipOutX,
&.flipOutY,
&.bounceIn,
@@ -276,9 +271,3 @@ a {
transform: translateX(468px);
}
}
-
-@keyframes spin {
- 100% {
- transform: rotate(360deg);
- }
-}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index f1aedc227f3..26db2386879 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -42,8 +42,7 @@
&.avatar-inline {
float: none;
display: inline-block;
- margin-left: 4px;
- margin-bottom: 2px;
+ margin-left: 2px;
flex-shrink: 0;
-webkit-flex-shrink: 0;
@@ -59,7 +58,7 @@
&.avatar-tile {
border-radius: 0;
- border: none;
+ border: 0;
}
&:not([href]):hover {
@@ -96,7 +95,7 @@
.avatar {
border-radius: 0;
- border: none;
+ border: 0;
height: auto;
width: 100%;
margin: 0;
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 6bb096fc5bd..9982a5779af 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -7,29 +7,76 @@
width: 100%;
height: 100%;
padding-bottom: 25px;
- border: 1px solid $border-color;
border-radius: $border-radius-default;
}
}
-.blank-state {
- padding-top: 20px;
- padding-bottom: 20px;
+.blank-state-row {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ height: 100%;
+}
+
+.blank-state-welcome {
text-align: center;
+ padding: 20px 0 40px;
+
+ .blank-state-welcome-title {
+ font-size: 24px;
+ }
+
+ .blank-state-text {
+ margin-bottom: 0;
+ }
+}
+
+.blank-state-link {
+ display: block;
+ color: $gl-text-color;
+ flex: 0 0 100%;
+ margin-bottom: 15px;
- &.blank-state-welcome {
- .blank-state-welcome-title {
- font-size: 24px;
+ @media (min-width: $screen-sm-min) {
+ flex: 0 0 49%;
+
+ &:nth-child(odd) {
+ margin-right: 5px;
}
- .blank-state-text {
- margin-bottom: 0;
+ &:nth-child(even) {
+ margin-left: 5px;
}
}
- .blank-state-icon {
- padding-bottom: 20px;
+ &:hover {
+ background-color: $gray-light;
+ text-decoration: none;
+ color: $gl-text-color;
+ }
+}
+.blank-state-center {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+
+.blank-state {
+ padding: 20px;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ align-items: center;
+ padding: 50px 30px;
+ }
+}
+
+.blank-state,
+.blank-state-center {
+ .blank-state-icon {
svg {
display: block;
margin: auto;
@@ -38,13 +85,17 @@
.blank-state-title {
margin-top: 0;
- margin-bottom: 10px;
font-size: 18px;
}
- .blank-state-text {
- max-width: $container-text-max-width;
- margin: 0 auto $gl-padding;
- font-size: 14px;
+ .blank-state-body {
+ @media (max-width: $screen-xs-max) {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ padding-left: 20px;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index dbd990f84c1..91976ca1f56 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -39,7 +39,11 @@
}
&.top-block {
- border-top: none;
+ border-top: 0;
+
+ .container-fluid {
+ background-color: inherit;
+ }
}
&.middle-block {
@@ -59,7 +63,7 @@
&.footer-block {
margin-top: 0;
- border-bottom: none;
+ border-bottom: 0;
margin-bottom: -$gl-padding;
}
@@ -96,11 +100,7 @@
&.build-content {
background-color: $white-light;
- border-top: none;
- }
-
- &.top-block .container-fluid {
- background-color: inherit;
+ border-top: 0;
}
}
@@ -209,7 +209,6 @@
padding: 24px 0 0;
.nav-links {
- justify-content: center;
width: 100%;
float: none;
@@ -217,6 +216,14 @@
float: none;
}
}
+
+ li:first-child {
+ margin-left: auto;
+ }
+
+ li:last-child {
+ margin-right: auto;
+ }
}
.group-info {
@@ -280,12 +287,12 @@
cursor: pointer;
color: $blue-300;
z-index: 1;
- border: none;
+ border: 0;
background-color: transparent;
&:hover,
&:focus {
- border: none;
+ border: 0;
color: $blue-400;
}
}
@@ -346,3 +353,7 @@
display: -webkit-flex;
display: flex;
}
+
+.flex-right {
+ margin-left: auto;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 00a0e9cef67..b2f26cf7159 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -294,6 +294,7 @@
.btn-align-content {
display: flex;
+ justify-content: center;
align-items: center;
}
@@ -304,7 +305,7 @@
}
.btn-clipboard {
- border: none;
+ border: 0;
padding: 0 5px;
}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index e0e46dd73af..1bd94c0acba 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -12,15 +12,15 @@
border-left: 3px solid $border-color;
color: $text-color;
background: $gray-light;
-}
-.bs-callout h4 {
- margin-top: 0;
- margin-bottom: 5px;
-}
+ h4 {
+ margin-top: 0;
+ margin-bottom: 5px;
+ }
-.bs-callout p:last-child {
- margin-bottom: 0;
+ p:last-child {
+ margin-bottom: 0;
+ }
}
/* Variations */
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 1cfd7ef01a8..5e4ddf366ef 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -2,38 +2,14 @@
.cgray { color: $common-gray; }
.clgray { color: $common-gray-light; }
.cred { color: $common-red; }
+svg.cred { fill: $common-red; }
.cgreen { color: $common-green; }
+svg.cgreen { fill: $common-green; }
.cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
-/** COMMON CLASSES **/
-.prepend-top-0 { margin-top: 0; }
-.prepend-top-5 { margin-top: 5px; }
-.prepend-top-10 { margin-top: 10px; }
-.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top: 20px; }
-.prepend-left-4 { margin-left: 4px; }
-.prepend-left-5 { margin-left: 5px; }
-.prepend-left-10 { margin-left: 10px; }
-.prepend-left-default { margin-left: $gl-padding; }
-.prepend-left-20 { margin-left: 20px; }
-.append-right-5 { margin-right: 5px; }
-.append-right-8 { margin-right: 8px; }
-.append-right-10 { margin-right: 10px; }
-.append-right-default { margin-right: $gl-padding; }
-.append-right-20 { margin-right: 20px; }
-.append-bottom-0 { margin-bottom: 0; }
-.append-bottom-5 { margin-bottom: 5px; }
-.append-bottom-10 { margin-bottom: 10px; }
-.append-bottom-15 { margin-bottom: 15px; }
-.append-bottom-20 { margin-bottom: 20px; }
-.append-bottom-default { margin-bottom: $gl-padding; }
-.inline { display: inline-block; }
-.center { text-align: center; }
-.vertical-align-middle { vertical-align: middle; }
-
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; }
@@ -57,7 +33,7 @@
pre {
&.clean {
background: none;
- border: none;
+ border: 0;
margin: 0;
padding: 0;
}
@@ -82,6 +58,14 @@ hr {
.str-truncated {
@include str-truncated;
+
+ &-60 {
+ @include str-truncated(60%);
+ }
+
+ &-100 {
+ @include str-truncated(100%);
+ }
}
.block-truncated {
@@ -107,10 +91,17 @@ hr {
font-size: 14px;
}
-table a code {
- position: relative;
- top: -2px;
- margin-right: 3px;
+table {
+ a code {
+ position: relative;
+ top: -2px;
+ margin-right: 3px;
+ }
+
+ td.permission-x {
+ background: $table-permission-x-bg !important;
+ text-align: center;
+ }
}
.loading {
@@ -156,7 +147,7 @@ li.note {
img { max-width: 100%; }
.note-title {
li {
- border-bottom: none !important;
+ border-bottom: 0 !important;
}
}
}
@@ -201,7 +192,7 @@ li.note {
pre {
background: $white-light;
- border: none;
+ border: 0;
font-size: 12px;
}
}
@@ -295,13 +286,6 @@ img.emoji {
margin-bottom: 10px;
}
-table {
- td.permission-x {
- background: $table-permission-x-bg !important;
- text-align: center;
- }
-}
-
.btn-sign-in {
text-shadow: none;
@@ -367,10 +351,11 @@ table {
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
-}
-.dropzone .dz-preview .dz-progress .dz-upload {
- background: $gl-success !important;
+ .dz-upload {
+ background: $gl-success !important;
+ }
+
}
.dz-message {
@@ -406,7 +391,7 @@ table {
}
.hide-bottom-border {
- border-bottom: none !important;
+ border-bottom: 0 !important;
}
.gl-accessibility {
@@ -431,16 +416,6 @@ table {
border-radius: $border-radius-default;
}
-.str-truncated {
- &-60 {
- @include str-truncated(60%);
- }
-
- &-100 {
- @include str-truncated(100%);
- }
-}
-
.tooltip {
.tooltip-inner {
word-wrap: break-word;
@@ -451,3 +426,31 @@ table {
pointer-events: none;
opacity: .5;
}
+
+/** COMMON CLASSES **/
+.prepend-top-0 { margin-top: 0; }
+.prepend-top-5 { margin-top: 5px; }
+.prepend-top-8 { margin-top: $grid-size; }
+.prepend-top-10 { margin-top: 10px; }
+.prepend-top-15 { margin-top: 15px; }
+.prepend-top-default { margin-top: $gl-padding !important; }
+.prepend-top-20 { margin-top: 20px; }
+.prepend-left-4 { margin-left: 4px; }
+.prepend-left-5 { margin-left: 5px; }
+.prepend-left-10 { margin-left: 10px; }
+.prepend-left-default { margin-left: $gl-padding; }
+.prepend-left-20 { margin-left: 20px; }
+.append-right-5 { margin-right: 5px; }
+.append-right-8 { margin-right: 8px; }
+.append-right-10 { margin-right: 10px; }
+.append-right-default { margin-right: $gl-padding; }
+.append-right-20 { margin-right: 20px; }
+.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-5 { margin-bottom: 5px; }
+.append-bottom-10 { margin-bottom: 10px; }
+.append-bottom-15 { margin-bottom: 15px; }
+.append-bottom-20 { margin-bottom: 20px; }
+.append-bottom-default { margin-bottom: $gl-padding; }
+.inline { display: inline-block; }
+.center { text-align: center; }
+.vertical-align-middle { vertical-align: middle; }
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index fa5d3833f3e..b73932eb7e1 100644
--- a/app/assets/stylesheets/framework/contextual-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -40,12 +40,6 @@
a:hover {
background-color: $link-hover-background;
color: $gl-text-color;
-
- .settings-avatar {
- svg {
- fill: $gl-text-color;
- }
- }
}
.avatar-container {
@@ -138,18 +132,14 @@
color: $gl-text-color-secondary;
}
- svg {
- fill: $gl-text-color-secondary;
+ .nav-item-name {
+ flex: 1;
}
- }
- .nav-item-name {
- flex: 1;
- }
-
- li.active {
- > a {
- font-weight: $gl-font-weight-bold;
+ &.active {
+ > a {
+ font-weight: $gl-font-weight-bold;
+ }
}
}
@@ -224,10 +214,6 @@
&:hover {
color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
}
}
@@ -338,7 +324,6 @@
align-items: center;
svg {
- fill: $gl-text-color-secondary;
margin-right: 8px;
}
@@ -349,10 +334,6 @@
&:hover {
background-color: $border-color;
color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
}
}
@@ -484,10 +465,7 @@
height: calc(100vh - #{$header-height});
@media (min-width: $screen-sm-min) {
- height: 475px; // Needed for PhantomJS
- // scss-lint:disable DuplicateProperty
height: calc(100vh - 180px);
- // scss-lint:enable DuplicateProperty
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 63697fd38a7..579bd48fac6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -728,11 +728,11 @@
.pika-single.animate-picker.is-bound {
@include set-visible;
-}
-.pika-single.animate-picker.is-bound.is-hidden {
- @include set-invisible;
- overflow: hidden;
+ &.is-hidden {
+ @include set-invisible;
+ overflow: hidden;
+ }
}
@mixin dropdown-item-hover {
@@ -777,12 +777,15 @@
a,
button,
.menu-item {
+ margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding: 8px 16px;
text-align: left;
white-space: normal;
width: 100%;
+ font-weight: $gl-font-weight-normal;
+ line-height: normal;
&.dropdown-menu-user-link {
white-space: nowrap;
@@ -936,9 +939,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
border-right: 0;
}
}
-}
-.projects-dropdown-container {
.projects-list-frequent-container,
.projects-list-search-container, {
padding: 8px 0;
@@ -949,11 +950,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
padding: 0 15px;
- }
-
- .section-header,
- .projects-list-frequent-container li.section-empty,
- .projects-list-search-container li.section-empty {
color: $gl-text-color-secondary;
font-size: $gl-font-size;
}
diff --git a/app/assets/stylesheets/framework/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji-sprites.scss
index 925415f84b1..0174e17b660 100644
--- a/app/assets/stylesheets/framework/emoji-sprites.scss
+++ b/app/assets/stylesheets/framework/emoji-sprites.scss
@@ -765,1031 +765,1033 @@
.emoji-full_moon { background-position: -160px -540px; }
.emoji-full_moon_with_face { background-position: -180px -540px; }
.emoji-game_die { background-position: -200px -540px; }
-.emoji-gear { background-position: -220px -540px; }
-.emoji-gem { background-position: -240px -540px; }
-.emoji-gemini { background-position: -260px -540px; }
-.emoji-ghost { background-position: -280px -540px; }
-.emoji-gift { background-position: -300px -540px; }
-.emoji-gift_heart { background-position: -320px -540px; }
-.emoji-girl { background-position: -340px -540px; }
-.emoji-girl_tone1 { background-position: -360px -540px; }
-.emoji-girl_tone2 { background-position: -380px -540px; }
-.emoji-girl_tone3 { background-position: -400px -540px; }
-.emoji-girl_tone4 { background-position: -420px -540px; }
-.emoji-girl_tone5 { background-position: -440px -540px; }
-.emoji-globe_with_meridians { background-position: -460px -540px; }
-.emoji-goal { background-position: -480px -540px; }
-.emoji-goat { background-position: -500px -540px; }
-.emoji-golf { background-position: -520px -540px; }
-.emoji-golfer { background-position: -540px -540px; }
-.emoji-gorilla { background-position: -560px 0; }
-.emoji-grapes { background-position: -560px -20px; }
-.emoji-green_apple { background-position: -560px -40px; }
-.emoji-green_book { background-position: -560px -60px; }
-.emoji-green_heart { background-position: -560px -80px; }
-.emoji-grey_exclamation { background-position: -560px -100px; }
-.emoji-grey_question { background-position: -560px -120px; }
-.emoji-grimacing { background-position: -560px -140px; }
-.emoji-grin { background-position: -560px -160px; }
-.emoji-grinning { background-position: -560px -180px; }
-.emoji-guardsman { background-position: -560px -200px; }
-.emoji-guardsman_tone1 { background-position: -560px -220px; }
-.emoji-guardsman_tone2 { background-position: -560px -240px; }
-.emoji-guardsman_tone3 { background-position: -560px -260px; }
-.emoji-guardsman_tone4 { background-position: -560px -280px; }
-.emoji-guardsman_tone5 { background-position: -560px -300px; }
-.emoji-guitar { background-position: -560px -320px; }
-.emoji-gun { background-position: -560px -340px; }
-.emoji-haircut { background-position: -560px -360px; }
-.emoji-haircut_tone1 { background-position: -560px -380px; }
-.emoji-haircut_tone2 { background-position: -560px -400px; }
-.emoji-haircut_tone3 { background-position: -560px -420px; }
-.emoji-haircut_tone4 { background-position: -560px -440px; }
-.emoji-haircut_tone5 { background-position: -560px -460px; }
-.emoji-hamburger { background-position: -560px -480px; }
-.emoji-hammer { background-position: -560px -500px; }
-.emoji-hammer_pick { background-position: -560px -520px; }
-.emoji-hamster { background-position: -560px -540px; }
-.emoji-hand_splayed { background-position: 0 -560px; }
-.emoji-hand_splayed_tone1 { background-position: -20px -560px; }
-.emoji-hand_splayed_tone2 { background-position: -40px -560px; }
-.emoji-hand_splayed_tone3 { background-position: -60px -560px; }
-.emoji-hand_splayed_tone4 { background-position: -80px -560px; }
-.emoji-hand_splayed_tone5 { background-position: -100px -560px; }
-.emoji-handbag { background-position: -120px -560px; }
-.emoji-handball { background-position: -140px -560px; }
-.emoji-handball_tone1 { background-position: -160px -560px; }
-.emoji-handball_tone2 { background-position: -180px -560px; }
-.emoji-handball_tone3 { background-position: -200px -560px; }
-.emoji-handball_tone4 { background-position: -220px -560px; }
-.emoji-handball_tone5 { background-position: -240px -560px; }
-.emoji-handshake { background-position: -260px -560px; }
-.emoji-handshake_tone1 { background-position: -280px -560px; }
-.emoji-handshake_tone2 { background-position: -300px -560px; }
-.emoji-handshake_tone3 { background-position: -320px -560px; }
-.emoji-handshake_tone4 { background-position: -340px -560px; }
-.emoji-handshake_tone5 { background-position: -360px -560px; }
-.emoji-hash { background-position: -380px -560px; }
-.emoji-hatched_chick { background-position: -400px -560px; }
-.emoji-hatching_chick { background-position: -420px -560px; }
-.emoji-head_bandage { background-position: -440px -560px; }
-.emoji-headphones { background-position: -460px -560px; }
-.emoji-hear_no_evil { background-position: -480px -560px; }
-.emoji-heart { background-position: -500px -560px; }
-.emoji-heart_decoration { background-position: -520px -560px; }
-.emoji-heart_exclamation { background-position: -540px -560px; }
-.emoji-heart_eyes { background-position: -560px -560px; }
-.emoji-heart_eyes_cat { background-position: -580px 0; }
-.emoji-heartbeat { background-position: -580px -20px; }
-.emoji-heartpulse { background-position: -580px -40px; }
-.emoji-hearts { background-position: -580px -60px; }
-.emoji-heavy_check_mark { background-position: -580px -80px; }
-.emoji-heavy_division_sign { background-position: -580px -100px; }
-.emoji-heavy_dollar_sign { background-position: -580px -120px; }
-.emoji-heavy_minus_sign { background-position: -580px -140px; }
-.emoji-heavy_multiplication_x { background-position: -580px -160px; }
-.emoji-heavy_plus_sign { background-position: -580px -180px; }
-.emoji-helicopter { background-position: -580px -200px; }
-.emoji-helmet_with_cross { background-position: -580px -220px; }
-.emoji-herb { background-position: -580px -240px; }
-.emoji-hibiscus { background-position: -580px -260px; }
-.emoji-high_brightness { background-position: -580px -280px; }
-.emoji-high_heel { background-position: -580px -300px; }
-.emoji-hockey { background-position: -580px -320px; }
-.emoji-hole { background-position: -580px -340px; }
-.emoji-homes { background-position: -580px -360px; }
-.emoji-honey_pot { background-position: -580px -380px; }
-.emoji-horse { background-position: -580px -400px; }
-.emoji-horse_racing { background-position: -580px -420px; }
-.emoji-horse_racing_tone1 { background-position: -580px -440px; }
-.emoji-horse_racing_tone2 { background-position: -580px -460px; }
-.emoji-horse_racing_tone3 { background-position: -580px -480px; }
-.emoji-horse_racing_tone4 { background-position: -580px -500px; }
-.emoji-horse_racing_tone5 { background-position: -580px -520px; }
-.emoji-hospital { background-position: -580px -540px; }
-.emoji-hot_pepper { background-position: -580px -560px; }
-.emoji-hotdog { background-position: 0 -580px; }
-.emoji-hotel { background-position: -20px -580px; }
-.emoji-hotsprings { background-position: -40px -580px; }
-.emoji-hourglass { background-position: -60px -580px; }
-.emoji-hourglass_flowing_sand { background-position: -80px -580px; }
-.emoji-house { background-position: -100px -580px; }
-.emoji-house_abandoned { background-position: -120px -580px; }
-.emoji-house_with_garden { background-position: -140px -580px; }
-.emoji-hugging { background-position: -160px -580px; }
-.emoji-hushed { background-position: -180px -580px; }
-.emoji-ice_cream { background-position: -200px -580px; }
-.emoji-ice_skate { background-position: -220px -580px; }
-.emoji-icecream { background-position: -240px -580px; }
-.emoji-id { background-position: -260px -580px; }
-.emoji-ideograph_advantage { background-position: -280px -580px; }
-.emoji-imp { background-position: -300px -580px; }
-.emoji-inbox_tray { background-position: -320px -580px; }
-.emoji-incoming_envelope { background-position: -340px -580px; }
-.emoji-information_desk_person { background-position: -360px -580px; }
-.emoji-information_desk_person_tone1 { background-position: -380px -580px; }
-.emoji-information_desk_person_tone2 { background-position: -400px -580px; }
-.emoji-information_desk_person_tone3 { background-position: -420px -580px; }
-.emoji-information_desk_person_tone4 { background-position: -440px -580px; }
-.emoji-information_desk_person_tone5 { background-position: -460px -580px; }
-.emoji-information_source { background-position: -480px -580px; }
-.emoji-innocent { background-position: -500px -580px; }
-.emoji-interrobang { background-position: -520px -580px; }
-.emoji-iphone { background-position: -540px -580px; }
-.emoji-island { background-position: -560px -580px; }
-.emoji-izakaya_lantern { background-position: -580px -580px; }
-.emoji-jack_o_lantern { background-position: -600px 0; }
-.emoji-japan { background-position: -600px -20px; }
-.emoji-japanese_castle { background-position: -600px -40px; }
-.emoji-japanese_goblin { background-position: -600px -60px; }
-.emoji-japanese_ogre { background-position: -600px -80px; }
-.emoji-jeans { background-position: -600px -100px; }
-.emoji-joy { background-position: -600px -120px; }
-.emoji-joy_cat { background-position: -600px -140px; }
-.emoji-joystick { background-position: -600px -160px; }
-.emoji-juggling { background-position: -600px -180px; }
-.emoji-juggling_tone1 { background-position: -600px -200px; }
-.emoji-juggling_tone2 { background-position: -600px -220px; }
-.emoji-juggling_tone3 { background-position: -600px -240px; }
-.emoji-juggling_tone4 { background-position: -600px -260px; }
-.emoji-juggling_tone5 { background-position: -600px -280px; }
-.emoji-kaaba { background-position: -600px -300px; }
-.emoji-key { background-position: -600px -320px; }
-.emoji-key2 { background-position: -600px -340px; }
-.emoji-keyboard { background-position: -600px -360px; }
-.emoji-kimono { background-position: -600px -380px; }
-.emoji-kiss { background-position: -600px -400px; }
-.emoji-kiss_mm { background-position: -600px -420px; }
-.emoji-kiss_ww { background-position: -600px -440px; }
-.emoji-kissing { background-position: -600px -460px; }
-.emoji-kissing_cat { background-position: -600px -480px; }
-.emoji-kissing_closed_eyes { background-position: -600px -500px; }
-.emoji-kissing_heart { background-position: -600px -520px; }
-.emoji-kissing_smiling_eyes { background-position: -600px -540px; }
-.emoji-kiwi { background-position: -600px -560px; }
-.emoji-knife { background-position: -600px -580px; }
-.emoji-koala { background-position: 0 -600px; }
-.emoji-koko { background-position: -20px -600px; }
-.emoji-label { background-position: -40px -600px; }
-.emoji-large_blue_circle { background-position: -60px -600px; }
-.emoji-large_blue_diamond { background-position: -80px -600px; }
-.emoji-large_orange_diamond { background-position: -100px -600px; }
-.emoji-last_quarter_moon { background-position: -120px -600px; }
-.emoji-last_quarter_moon_with_face { background-position: -140px -600px; }
-.emoji-laughing { background-position: -160px -600px; }
-.emoji-leaves { background-position: -180px -600px; }
-.emoji-ledger { background-position: -200px -600px; }
-.emoji-left_facing_fist { background-position: -220px -600px; }
-.emoji-left_facing_fist_tone1 { background-position: -240px -600px; }
-.emoji-left_facing_fist_tone2 { background-position: -260px -600px; }
-.emoji-left_facing_fist_tone3 { background-position: -280px -600px; }
-.emoji-left_facing_fist_tone4 { background-position: -300px -600px; }
-.emoji-left_facing_fist_tone5 { background-position: -320px -600px; }
-.emoji-left_luggage { background-position: -340px -600px; }
-.emoji-left_right_arrow { background-position: -360px -600px; }
-.emoji-leftwards_arrow_with_hook { background-position: -380px -600px; }
-.emoji-lemon { background-position: -400px -600px; }
-.emoji-leo { background-position: -420px -600px; }
-.emoji-leopard { background-position: -440px -600px; }
-.emoji-level_slider { background-position: -460px -600px; }
-.emoji-levitate { background-position: -480px -600px; }
-.emoji-libra { background-position: -500px -600px; }
-.emoji-lifter { background-position: -520px -600px; }
-.emoji-lifter_tone1 { background-position: -540px -600px; }
-.emoji-lifter_tone2 { background-position: -560px -600px; }
-.emoji-lifter_tone3 { background-position: -580px -600px; }
-.emoji-lifter_tone4 { background-position: -600px -600px; }
-.emoji-lifter_tone5 { background-position: -620px 0; }
-.emoji-light_rail { background-position: -620px -20px; }
-.emoji-link { background-position: -620px -40px; }
-.emoji-lion_face { background-position: -620px -60px; }
-.emoji-lips { background-position: -620px -80px; }
-.emoji-lipstick { background-position: -620px -100px; }
-.emoji-lizard { background-position: -620px -120px; }
-.emoji-lock { background-position: -620px -140px; }
-.emoji-lock_with_ink_pen { background-position: -620px -160px; }
-.emoji-lollipop { background-position: -620px -180px; }
-.emoji-loop { background-position: -620px -200px; }
-.emoji-loud_sound { background-position: -620px -220px; }
-.emoji-loudspeaker { background-position: -620px -240px; }
-.emoji-love_hotel { background-position: -620px -260px; }
-.emoji-love_letter { background-position: -620px -280px; }
-.emoji-low_brightness { background-position: -620px -300px; }
-.emoji-lying_face { background-position: -620px -320px; }
-.emoji-m { background-position: -620px -340px; }
-.emoji-mag { background-position: -620px -360px; }
-.emoji-mag_right { background-position: -620px -380px; }
-.emoji-mahjong { background-position: -620px -400px; }
-.emoji-mailbox { background-position: -620px -420px; }
-.emoji-mailbox_closed { background-position: -620px -440px; }
-.emoji-mailbox_with_mail { background-position: -620px -460px; }
-.emoji-mailbox_with_no_mail { background-position: -620px -480px; }
-.emoji-man { background-position: -620px -500px; }
-.emoji-man_dancing { background-position: -620px -520px; }
-.emoji-man_dancing_tone1 { background-position: -620px -540px; }
-.emoji-man_dancing_tone2 { background-position: -620px -560px; }
-.emoji-man_dancing_tone3 { background-position: -620px -580px; }
-.emoji-man_dancing_tone4 { background-position: -620px -600px; }
-.emoji-man_dancing_tone5 { background-position: 0 -620px; }
-.emoji-man_in_tuxedo { background-position: -20px -620px; }
-.emoji-man_in_tuxedo_tone1 { background-position: -40px -620px; }
-.emoji-man_in_tuxedo_tone2 { background-position: -60px -620px; }
-.emoji-man_in_tuxedo_tone3 { background-position: -80px -620px; }
-.emoji-man_in_tuxedo_tone4 { background-position: -100px -620px; }
-.emoji-man_in_tuxedo_tone5 { background-position: -120px -620px; }
-.emoji-man_tone1 { background-position: -140px -620px; }
-.emoji-man_tone2 { background-position: -160px -620px; }
-.emoji-man_tone3 { background-position: -180px -620px; }
-.emoji-man_tone4 { background-position: -200px -620px; }
-.emoji-man_tone5 { background-position: -220px -620px; }
-.emoji-man_with_gua_pi_mao { background-position: -240px -620px; }
-.emoji-man_with_gua_pi_mao_tone1 { background-position: -260px -620px; }
-.emoji-man_with_gua_pi_mao_tone2 { background-position: -280px -620px; }
-.emoji-man_with_gua_pi_mao_tone3 { background-position: -300px -620px; }
-.emoji-man_with_gua_pi_mao_tone4 { background-position: -320px -620px; }
-.emoji-man_with_gua_pi_mao_tone5 { background-position: -340px -620px; }
-.emoji-man_with_turban { background-position: -360px -620px; }
-.emoji-man_with_turban_tone1 { background-position: -380px -620px; }
-.emoji-man_with_turban_tone2 { background-position: -400px -620px; }
-.emoji-man_with_turban_tone3 { background-position: -420px -620px; }
-.emoji-man_with_turban_tone4 { background-position: -440px -620px; }
-.emoji-man_with_turban_tone5 { background-position: -460px -620px; }
-.emoji-mans_shoe { background-position: -480px -620px; }
-.emoji-map { background-position: -500px -620px; }
-.emoji-maple_leaf { background-position: -520px -620px; }
-.emoji-martial_arts_uniform { background-position: -540px -620px; }
-.emoji-mask { background-position: -560px -620px; }
-.emoji-massage { background-position: -580px -620px; }
-.emoji-massage_tone1 { background-position: -600px -620px; }
-.emoji-massage_tone2 { background-position: -620px -620px; }
-.emoji-massage_tone3 { background-position: -640px 0; }
-.emoji-massage_tone4 { background-position: -640px -20px; }
-.emoji-massage_tone5 { background-position: -640px -40px; }
-.emoji-meat_on_bone { background-position: -640px -60px; }
-.emoji-medal { background-position: -640px -80px; }
-.emoji-mega { background-position: -640px -100px; }
-.emoji-melon { background-position: -640px -120px; }
-.emoji-menorah { background-position: -640px -140px; }
-.emoji-mens { background-position: -640px -160px; }
-.emoji-metal { background-position: -640px -180px; }
-.emoji-metal_tone1 { background-position: -640px -200px; }
-.emoji-metal_tone2 { background-position: -640px -220px; }
-.emoji-metal_tone3 { background-position: -640px -240px; }
-.emoji-metal_tone4 { background-position: -640px -260px; }
-.emoji-metal_tone5 { background-position: -640px -280px; }
-.emoji-metro { background-position: -640px -300px; }
-.emoji-microphone { background-position: -640px -320px; }
-.emoji-microphone2 { background-position: -640px -340px; }
-.emoji-microscope { background-position: -640px -360px; }
-.emoji-middle_finger { background-position: -640px -380px; }
-.emoji-middle_finger_tone1 { background-position: -640px -400px; }
-.emoji-middle_finger_tone2 { background-position: -640px -420px; }
-.emoji-middle_finger_tone3 { background-position: -640px -440px; }
-.emoji-middle_finger_tone4 { background-position: -640px -460px; }
-.emoji-middle_finger_tone5 { background-position: -640px -480px; }
-.emoji-military_medal { background-position: -640px -500px; }
-.emoji-milk { background-position: -640px -520px; }
-.emoji-milky_way { background-position: -640px -540px; }
-.emoji-minibus { background-position: -640px -560px; }
-.emoji-minidisc { background-position: -640px -580px; }
-.emoji-mobile_phone_off { background-position: -640px -600px; }
-.emoji-money_mouth { background-position: -640px -620px; }
-.emoji-money_with_wings { background-position: 0 -640px; }
-.emoji-moneybag { background-position: -20px -640px; }
-.emoji-monkey { background-position: -40px -640px; }
-.emoji-monkey_face { background-position: -60px -640px; }
-.emoji-monorail { background-position: -80px -640px; }
-.emoji-mortar_board { background-position: -100px -640px; }
-.emoji-mosque { background-position: -120px -640px; }
-.emoji-motor_scooter { background-position: -140px -640px; }
-.emoji-motorboat { background-position: -160px -640px; }
-.emoji-motorcycle { background-position: -180px -640px; }
-.emoji-motorway { background-position: -200px -640px; }
-.emoji-mount_fuji { background-position: -220px -640px; }
-.emoji-mountain { background-position: -240px -640px; }
-.emoji-mountain_bicyclist { background-position: -260px -640px; }
-.emoji-mountain_bicyclist_tone1 { background-position: -280px -640px; }
-.emoji-mountain_bicyclist_tone2 { background-position: -300px -640px; }
-.emoji-mountain_bicyclist_tone3 { background-position: -320px -640px; }
-.emoji-mountain_bicyclist_tone4 { background-position: -340px -640px; }
-.emoji-mountain_bicyclist_tone5 { background-position: -360px -640px; }
-.emoji-mountain_cableway { background-position: -380px -640px; }
-.emoji-mountain_railway { background-position: -400px -640px; }
-.emoji-mountain_snow { background-position: -420px -640px; }
-.emoji-mouse { background-position: -440px -640px; }
-.emoji-mouse2 { background-position: -460px -640px; }
-.emoji-mouse_three_button { background-position: -480px -640px; }
-.emoji-movie_camera { background-position: -500px -640px; }
-.emoji-moyai { background-position: -520px -640px; }
-.emoji-mrs_claus { background-position: -540px -640px; }
-.emoji-mrs_claus_tone1 { background-position: -560px -640px; }
-.emoji-mrs_claus_tone2 { background-position: -580px -640px; }
-.emoji-mrs_claus_tone3 { background-position: -600px -640px; }
-.emoji-mrs_claus_tone4 { background-position: -620px -640px; }
-.emoji-mrs_claus_tone5 { background-position: -640px -640px; }
-.emoji-muscle { background-position: -660px 0; }
-.emoji-muscle_tone1 { background-position: -660px -20px; }
-.emoji-muscle_tone2 { background-position: -660px -40px; }
-.emoji-muscle_tone3 { background-position: -660px -60px; }
-.emoji-muscle_tone4 { background-position: -660px -80px; }
-.emoji-muscle_tone5 { background-position: -660px -100px; }
-.emoji-mushroom { background-position: -660px -120px; }
-.emoji-musical_keyboard { background-position: -660px -140px; }
-.emoji-musical_note { background-position: -660px -160px; }
-.emoji-musical_score { background-position: -660px -180px; }
-.emoji-mute { background-position: -660px -200px; }
-.emoji-nail_care { background-position: -660px -220px; }
-.emoji-nail_care_tone1 { background-position: -660px -240px; }
-.emoji-nail_care_tone2 { background-position: -660px -260px; }
-.emoji-nail_care_tone3 { background-position: -660px -280px; }
-.emoji-nail_care_tone4 { background-position: -660px -300px; }
-.emoji-nail_care_tone5 { background-position: -660px -320px; }
-.emoji-name_badge { background-position: -660px -340px; }
-.emoji-nauseated_face { background-position: -660px -360px; }
-.emoji-necktie { background-position: -660px -380px; }
-.emoji-negative_squared_cross_mark { background-position: -660px -400px; }
-.emoji-nerd { background-position: -660px -420px; }
-.emoji-neutral_face { background-position: -660px -440px; }
-.emoji-new { background-position: -660px -460px; }
-.emoji-new_moon { background-position: -660px -480px; }
-.emoji-new_moon_with_face { background-position: -660px -500px; }
-.emoji-newspaper { background-position: -660px -520px; }
-.emoji-newspaper2 { background-position: -660px -540px; }
-.emoji-ng { background-position: -660px -560px; }
-.emoji-night_with_stars { background-position: -660px -580px; }
-.emoji-nine { background-position: -660px -600px; }
-.emoji-no_bell { background-position: -660px -620px; }
-.emoji-no_bicycles { background-position: -660px -640px; }
-.emoji-no_entry { background-position: 0 -660px; }
-.emoji-no_entry_sign { background-position: -20px -660px; }
-.emoji-no_good { background-position: -40px -660px; }
-.emoji-no_good_tone1 { background-position: -60px -660px; }
-.emoji-no_good_tone2 { background-position: -80px -660px; }
-.emoji-no_good_tone3 { background-position: -100px -660px; }
-.emoji-no_good_tone4 { background-position: -120px -660px; }
-.emoji-no_good_tone5 { background-position: -140px -660px; }
-.emoji-no_mobile_phones { background-position: -160px -660px; }
-.emoji-no_mouth { background-position: -180px -660px; }
-.emoji-no_pedestrians { background-position: -200px -660px; }
-.emoji-no_smoking { background-position: -220px -660px; }
-.emoji-non-potable_water { background-position: -240px -660px; }
-.emoji-nose { background-position: -260px -660px; }
-.emoji-nose_tone1 { background-position: -280px -660px; }
-.emoji-nose_tone2 { background-position: -300px -660px; }
-.emoji-nose_tone3 { background-position: -320px -660px; }
-.emoji-nose_tone4 { background-position: -340px -660px; }
-.emoji-nose_tone5 { background-position: -360px -660px; }
-.emoji-notebook { background-position: -380px -660px; }
-.emoji-notebook_with_decorative_cover { background-position: -400px -660px; }
-.emoji-notepad_spiral { background-position: -420px -660px; }
-.emoji-notes { background-position: -440px -660px; }
-.emoji-nut_and_bolt { background-position: -460px -660px; }
-.emoji-o { background-position: -480px -660px; }
-.emoji-o2 { background-position: -500px -660px; }
-.emoji-ocean { background-position: -520px -660px; }
-.emoji-octagonal_sign { background-position: -540px -660px; }
-.emoji-octopus { background-position: -560px -660px; }
-.emoji-oden { background-position: -580px -660px; }
-.emoji-office { background-position: -600px -660px; }
-.emoji-oil { background-position: -620px -660px; }
-.emoji-ok { background-position: -640px -660px; }
-.emoji-ok_hand { background-position: -660px -660px; }
-.emoji-ok_hand_tone1 { background-position: -680px 0; }
-.emoji-ok_hand_tone2 { background-position: -680px -20px; }
-.emoji-ok_hand_tone3 { background-position: -680px -40px; }
-.emoji-ok_hand_tone4 { background-position: -680px -60px; }
-.emoji-ok_hand_tone5 { background-position: -680px -80px; }
-.emoji-ok_woman { background-position: -680px -100px; }
-.emoji-ok_woman_tone1 { background-position: -680px -120px; }
-.emoji-ok_woman_tone2 { background-position: -680px -140px; }
-.emoji-ok_woman_tone3 { background-position: -680px -160px; }
-.emoji-ok_woman_tone4 { background-position: -680px -180px; }
-.emoji-ok_woman_tone5 { background-position: -680px -200px; }
-.emoji-older_man { background-position: -680px -220px; }
-.emoji-older_man_tone1 { background-position: -680px -240px; }
-.emoji-older_man_tone2 { background-position: -680px -260px; }
-.emoji-older_man_tone3 { background-position: -680px -280px; }
-.emoji-older_man_tone4 { background-position: -680px -300px; }
-.emoji-older_man_tone5 { background-position: -680px -320px; }
-.emoji-older_woman { background-position: -680px -340px; }
-.emoji-older_woman_tone1 { background-position: -680px -360px; }
-.emoji-older_woman_tone2 { background-position: -680px -380px; }
-.emoji-older_woman_tone3 { background-position: -680px -400px; }
-.emoji-older_woman_tone4 { background-position: -680px -420px; }
-.emoji-older_woman_tone5 { background-position: -680px -440px; }
-.emoji-om_symbol { background-position: -680px -460px; }
-.emoji-on { background-position: -680px -480px; }
-.emoji-oncoming_automobile { background-position: -680px -500px; }
-.emoji-oncoming_bus { background-position: -680px -520px; }
-.emoji-oncoming_police_car { background-position: -680px -540px; }
-.emoji-oncoming_taxi { background-position: -680px -560px; }
-.emoji-one { background-position: -680px -580px; }
-.emoji-open_file_folder { background-position: -680px -600px; }
-.emoji-open_hands { background-position: -680px -620px; }
-.emoji-open_hands_tone1 { background-position: -680px -640px; }
-.emoji-open_hands_tone2 { background-position: -680px -660px; }
-.emoji-open_hands_tone3 { background-position: 0 -680px; }
-.emoji-open_hands_tone4 { background-position: -20px -680px; }
-.emoji-open_hands_tone5 { background-position: -40px -680px; }
-.emoji-open_mouth { background-position: -60px -680px; }
-.emoji-ophiuchus { background-position: -80px -680px; }
-.emoji-orange_book { background-position: -100px -680px; }
-.emoji-orthodox_cross { background-position: -120px -680px; }
-.emoji-outbox_tray { background-position: -140px -680px; }
-.emoji-owl { background-position: -160px -680px; }
-.emoji-ox { background-position: -180px -680px; }
-.emoji-package { background-position: -200px -680px; }
-.emoji-page_facing_up { background-position: -220px -680px; }
-.emoji-page_with_curl { background-position: -240px -680px; }
-.emoji-pager { background-position: -260px -680px; }
-.emoji-paintbrush { background-position: -280px -680px; }
-.emoji-palm_tree { background-position: -300px -680px; }
-.emoji-pancakes { background-position: -320px -680px; }
-.emoji-panda_face { background-position: -340px -680px; }
-.emoji-paperclip { background-position: -360px -680px; }
-.emoji-paperclips { background-position: -380px -680px; }
-.emoji-park { background-position: -400px -680px; }
-.emoji-parking { background-position: -420px -680px; }
-.emoji-part_alternation_mark { background-position: -440px -680px; }
-.emoji-partly_sunny { background-position: -460px -680px; }
-.emoji-passport_control { background-position: -480px -680px; }
-.emoji-pause_button { background-position: -500px -680px; }
-.emoji-peace { background-position: -520px -680px; }
-.emoji-peach { background-position: -540px -680px; }
-.emoji-peanuts { background-position: -560px -680px; }
-.emoji-pear { background-position: -580px -680px; }
-.emoji-pen_ballpoint { background-position: -600px -680px; }
-.emoji-pen_fountain { background-position: -620px -680px; }
-.emoji-pencil { background-position: -640px -680px; }
-.emoji-pencil2 { background-position: -660px -680px; }
-.emoji-penguin { background-position: -680px -680px; }
-.emoji-pensive { background-position: -700px 0; }
-.emoji-performing_arts { background-position: -700px -20px; }
-.emoji-persevere { background-position: -700px -40px; }
-.emoji-person_frowning { background-position: -700px -60px; }
-.emoji-person_frowning_tone1 { background-position: -700px -80px; }
-.emoji-person_frowning_tone2 { background-position: -700px -100px; }
-.emoji-person_frowning_tone3 { background-position: -700px -120px; }
-.emoji-person_frowning_tone4 { background-position: -700px -140px; }
-.emoji-person_frowning_tone5 { background-position: -700px -160px; }
-.emoji-person_with_blond_hair { background-position: -700px -180px; }
-.emoji-person_with_blond_hair_tone1 { background-position: -700px -200px; }
-.emoji-person_with_blond_hair_tone2 { background-position: -700px -220px; }
-.emoji-person_with_blond_hair_tone3 { background-position: -700px -240px; }
-.emoji-person_with_blond_hair_tone4 { background-position: -700px -260px; }
-.emoji-person_with_blond_hair_tone5 { background-position: -700px -280px; }
-.emoji-person_with_pouting_face { background-position: -700px -300px; }
-.emoji-person_with_pouting_face_tone1 { background-position: -700px -320px; }
-.emoji-person_with_pouting_face_tone2 { background-position: -700px -340px; }
-.emoji-person_with_pouting_face_tone3 { background-position: -700px -360px; }
-.emoji-person_with_pouting_face_tone4 { background-position: -700px -380px; }
-.emoji-person_with_pouting_face_tone5 { background-position: -700px -400px; }
-.emoji-pick { background-position: -700px -420px; }
-.emoji-pig { background-position: -700px -440px; }
-.emoji-pig2 { background-position: -700px -460px; }
-.emoji-pig_nose { background-position: -700px -480px; }
-.emoji-pill { background-position: -700px -500px; }
-.emoji-pineapple { background-position: -700px -520px; }
-.emoji-ping_pong { background-position: -700px -540px; }
-.emoji-pisces { background-position: -700px -560px; }
-.emoji-pizza { background-position: -700px -580px; }
-.emoji-place_of_worship { background-position: -700px -600px; }
-.emoji-play_pause { background-position: -700px -620px; }
-.emoji-point_down { background-position: -700px -640px; }
-.emoji-point_down_tone1 { background-position: -700px -660px; }
-.emoji-point_down_tone2 { background-position: -700px -680px; }
-.emoji-point_down_tone3 { background-position: 0 -700px; }
-.emoji-point_down_tone4 { background-position: -20px -700px; }
-.emoji-point_down_tone5 { background-position: -40px -700px; }
-.emoji-point_left { background-position: -60px -700px; }
-.emoji-point_left_tone1 { background-position: -80px -700px; }
-.emoji-point_left_tone2 { background-position: -100px -700px; }
-.emoji-point_left_tone3 { background-position: -120px -700px; }
-.emoji-point_left_tone4 { background-position: -140px -700px; }
-.emoji-point_left_tone5 { background-position: -160px -700px; }
-.emoji-point_right { background-position: -180px -700px; }
-.emoji-point_right_tone1 { background-position: -200px -700px; }
-.emoji-point_right_tone2 { background-position: -220px -700px; }
-.emoji-point_right_tone3 { background-position: -240px -700px; }
-.emoji-point_right_tone4 { background-position: -260px -700px; }
-.emoji-point_right_tone5 { background-position: -280px -700px; }
-.emoji-point_up { background-position: -300px -700px; }
-.emoji-point_up_2 { background-position: -320px -700px; }
-.emoji-point_up_2_tone1 { background-position: -340px -700px; }
-.emoji-point_up_2_tone2 { background-position: -360px -700px; }
-.emoji-point_up_2_tone3 { background-position: -380px -700px; }
-.emoji-point_up_2_tone4 { background-position: -400px -700px; }
-.emoji-point_up_2_tone5 { background-position: -420px -700px; }
-.emoji-point_up_tone1 { background-position: -440px -700px; }
-.emoji-point_up_tone2 { background-position: -460px -700px; }
-.emoji-point_up_tone3 { background-position: -480px -700px; }
-.emoji-point_up_tone4 { background-position: -500px -700px; }
-.emoji-point_up_tone5 { background-position: -520px -700px; }
-.emoji-police_car { background-position: -540px -700px; }
-.emoji-poodle { background-position: -560px -700px; }
-.emoji-poop { background-position: -580px -700px; }
-.emoji-popcorn { background-position: -600px -700px; }
-.emoji-post_office { background-position: -620px -700px; }
-.emoji-postal_horn { background-position: -640px -700px; }
-.emoji-postbox { background-position: -660px -700px; }
-.emoji-potable_water { background-position: -680px -700px; }
-.emoji-potato { background-position: -700px -700px; }
-.emoji-pouch { background-position: -720px 0; }
-.emoji-poultry_leg { background-position: -720px -20px; }
-.emoji-pound { background-position: -720px -40px; }
-.emoji-pouting_cat { background-position: -720px -60px; }
-.emoji-pray { background-position: -720px -80px; }
-.emoji-pray_tone1 { background-position: -720px -100px; }
-.emoji-pray_tone2 { background-position: -720px -120px; }
-.emoji-pray_tone3 { background-position: -720px -140px; }
-.emoji-pray_tone4 { background-position: -720px -160px; }
-.emoji-pray_tone5 { background-position: -720px -180px; }
-.emoji-prayer_beads { background-position: -720px -200px; }
-.emoji-pregnant_woman { background-position: -720px -220px; }
-.emoji-pregnant_woman_tone1 { background-position: -720px -240px; }
-.emoji-pregnant_woman_tone2 { background-position: -720px -260px; }
-.emoji-pregnant_woman_tone3 { background-position: -720px -280px; }
-.emoji-pregnant_woman_tone4 { background-position: -720px -300px; }
-.emoji-pregnant_woman_tone5 { background-position: -720px -320px; }
-.emoji-prince { background-position: -720px -340px; }
-.emoji-prince_tone1 { background-position: -720px -360px; }
-.emoji-prince_tone2 { background-position: -720px -380px; }
-.emoji-prince_tone3 { background-position: -720px -400px; }
-.emoji-prince_tone4 { background-position: -720px -420px; }
-.emoji-prince_tone5 { background-position: -720px -440px; }
-.emoji-princess { background-position: -720px -460px; }
-.emoji-princess_tone1 { background-position: -720px -480px; }
-.emoji-princess_tone2 { background-position: -720px -500px; }
-.emoji-princess_tone3 { background-position: -720px -520px; }
-.emoji-princess_tone4 { background-position: -720px -540px; }
-.emoji-princess_tone5 { background-position: -720px -560px; }
-.emoji-printer { background-position: -720px -580px; }
-.emoji-projector { background-position: -720px -600px; }
-.emoji-punch { background-position: -720px -620px; }
-.emoji-punch_tone1 { background-position: -720px -640px; }
-.emoji-punch_tone2 { background-position: -720px -660px; }
-.emoji-punch_tone3 { background-position: -720px -680px; }
-.emoji-punch_tone4 { background-position: -720px -700px; }
-.emoji-punch_tone5 { background-position: 0 -720px; }
-.emoji-purple_heart { background-position: -20px -720px; }
-.emoji-purse { background-position: -40px -720px; }
-.emoji-pushpin { background-position: -60px -720px; }
-.emoji-put_litter_in_its_place { background-position: -80px -720px; }
-.emoji-question { background-position: -100px -720px; }
-.emoji-rabbit { background-position: -120px -720px; }
-.emoji-rabbit2 { background-position: -140px -720px; }
-.emoji-race_car { background-position: -160px -720px; }
-.emoji-racehorse { background-position: -180px -720px; }
-.emoji-radio { background-position: -200px -720px; }
-.emoji-radio_button { background-position: -220px -720px; }
-.emoji-radioactive { background-position: -240px -720px; }
-.emoji-rage { background-position: -260px -720px; }
-.emoji-railway_car { background-position: -280px -720px; }
-.emoji-railway_track { background-position: -300px -720px; }
-.emoji-rainbow { background-position: -320px -720px; }
-.emoji-raised_back_of_hand { background-position: -340px -720px; }
-.emoji-raised_back_of_hand_tone1 { background-position: -360px -720px; }
-.emoji-raised_back_of_hand_tone2 { background-position: -380px -720px; }
-.emoji-raised_back_of_hand_tone3 { background-position: -400px -720px; }
-.emoji-raised_back_of_hand_tone4 { background-position: -420px -720px; }
-.emoji-raised_back_of_hand_tone5 { background-position: -440px -720px; }
-.emoji-raised_hand { background-position: -460px -720px; }
-.emoji-raised_hand_tone1 { background-position: -480px -720px; }
-.emoji-raised_hand_tone2 { background-position: -500px -720px; }
-.emoji-raised_hand_tone3 { background-position: -520px -720px; }
-.emoji-raised_hand_tone4 { background-position: -540px -720px; }
-.emoji-raised_hand_tone5 { background-position: -560px -720px; }
-.emoji-raised_hands { background-position: -580px -720px; }
-.emoji-raised_hands_tone1 { background-position: -600px -720px; }
-.emoji-raised_hands_tone2 { background-position: -620px -720px; }
-.emoji-raised_hands_tone3 { background-position: -640px -720px; }
-.emoji-raised_hands_tone4 { background-position: -660px -720px; }
-.emoji-raised_hands_tone5 { background-position: -680px -720px; }
-.emoji-raising_hand { background-position: -700px -720px; }
-.emoji-raising_hand_tone1 { background-position: -720px -720px; }
-.emoji-raising_hand_tone2 { background-position: -740px 0; }
-.emoji-raising_hand_tone3 { background-position: -740px -20px; }
-.emoji-raising_hand_tone4 { background-position: -740px -40px; }
-.emoji-raising_hand_tone5 { background-position: -740px -60px; }
-.emoji-ram { background-position: -740px -80px; }
-.emoji-ramen { background-position: -740px -100px; }
-.emoji-rat { background-position: -740px -120px; }
-.emoji-record_button { background-position: -740px -140px; }
-.emoji-recycle { background-position: -740px -160px; }
-.emoji-red_car { background-position: -740px -180px; }
-.emoji-red_circle { background-position: -740px -200px; }
-.emoji-registered { background-position: -740px -220px; }
-.emoji-relaxed { background-position: -740px -240px; }
-.emoji-relieved { background-position: -740px -260px; }
-.emoji-reminder_ribbon { background-position: -740px -280px; }
-.emoji-repeat { background-position: -740px -300px; }
-.emoji-repeat_one { background-position: -740px -320px; }
-.emoji-restroom { background-position: -740px -340px; }
-.emoji-revolving_hearts { background-position: -740px -360px; }
-.emoji-rewind { background-position: -740px -380px; }
-.emoji-rhino { background-position: -740px -400px; }
-.emoji-ribbon { background-position: -740px -420px; }
-.emoji-rice { background-position: -740px -440px; }
-.emoji-rice_ball { background-position: -740px -460px; }
-.emoji-rice_cracker { background-position: -740px -480px; }
-.emoji-rice_scene { background-position: -740px -500px; }
-.emoji-right_facing_fist { background-position: -740px -520px; }
-.emoji-right_facing_fist_tone1 { background-position: -740px -540px; }
-.emoji-right_facing_fist_tone2 { background-position: -740px -560px; }
-.emoji-right_facing_fist_tone3 { background-position: -740px -580px; }
-.emoji-right_facing_fist_tone4 { background-position: -740px -600px; }
-.emoji-right_facing_fist_tone5 { background-position: -740px -620px; }
-.emoji-ring { background-position: -740px -640px; }
-.emoji-robot { background-position: -740px -660px; }
-.emoji-rocket { background-position: -740px -680px; }
-.emoji-rofl { background-position: -740px -700px; }
-.emoji-roller_coaster { background-position: -740px -720px; }
-.emoji-rolling_eyes { background-position: 0 -740px; }
-.emoji-rooster { background-position: -20px -740px; }
-.emoji-rose { background-position: -40px -740px; }
-.emoji-rosette { background-position: -60px -740px; }
-.emoji-rotating_light { background-position: -80px -740px; }
-.emoji-round_pushpin { background-position: -100px -740px; }
-.emoji-rowboat { background-position: -120px -740px; }
-.emoji-rowboat_tone1 { background-position: -140px -740px; }
-.emoji-rowboat_tone2 { background-position: -160px -740px; }
-.emoji-rowboat_tone3 { background-position: -180px -740px; }
-.emoji-rowboat_tone4 { background-position: -200px -740px; }
-.emoji-rowboat_tone5 { background-position: -220px -740px; }
-.emoji-rugby_football { background-position: -240px -740px; }
-.emoji-runner { background-position: -260px -740px; }
-.emoji-runner_tone1 { background-position: -280px -740px; }
-.emoji-runner_tone2 { background-position: -300px -740px; }
-.emoji-runner_tone3 { background-position: -320px -740px; }
-.emoji-runner_tone4 { background-position: -340px -740px; }
-.emoji-runner_tone5 { background-position: -360px -740px; }
-.emoji-running_shirt_with_sash { background-position: -380px -740px; }
-.emoji-sa { background-position: -400px -740px; }
-.emoji-sagittarius { background-position: -420px -740px; }
-.emoji-sailboat { background-position: -440px -740px; }
-.emoji-sake { background-position: -460px -740px; }
-.emoji-salad { background-position: -480px -740px; }
-.emoji-sandal { background-position: -500px -740px; }
-.emoji-santa { background-position: -520px -740px; }
-.emoji-santa_tone1 { background-position: -540px -740px; }
-.emoji-santa_tone2 { background-position: -560px -740px; }
-.emoji-santa_tone3 { background-position: -580px -740px; }
-.emoji-santa_tone4 { background-position: -600px -740px; }
-.emoji-santa_tone5 { background-position: -620px -740px; }
-.emoji-satellite { background-position: -640px -740px; }
-.emoji-satellite_orbital { background-position: -660px -740px; }
-.emoji-saxophone { background-position: -680px -740px; }
-.emoji-scales { background-position: -700px -740px; }
-.emoji-school { background-position: -720px -740px; }
-.emoji-school_satchel { background-position: -740px -740px; }
-.emoji-scissors { background-position: -760px 0; }
-.emoji-scooter { background-position: -760px -20px; }
-.emoji-scorpion { background-position: -760px -40px; }
-.emoji-scorpius { background-position: -760px -60px; }
-.emoji-scream { background-position: -760px -80px; }
-.emoji-scream_cat { background-position: -760px -100px; }
-.emoji-scroll { background-position: -760px -120px; }
-.emoji-seat { background-position: -760px -140px; }
-.emoji-second_place { background-position: -760px -160px; }
-.emoji-secret { background-position: -760px -180px; }
-.emoji-see_no_evil { background-position: -760px -200px; }
-.emoji-seedling { background-position: -760px -220px; }
-.emoji-selfie { background-position: -760px -240px; }
-.emoji-selfie_tone1 { background-position: -760px -260px; }
-.emoji-selfie_tone2 { background-position: -760px -280px; }
-.emoji-selfie_tone3 { background-position: -760px -300px; }
-.emoji-selfie_tone4 { background-position: -760px -320px; }
-.emoji-selfie_tone5 { background-position: -760px -340px; }
-.emoji-seven { background-position: -760px -360px; }
-.emoji-shallow_pan_of_food { background-position: -760px -380px; }
-.emoji-shamrock { background-position: -760px -400px; }
-.emoji-shark { background-position: -760px -420px; }
-.emoji-shaved_ice { background-position: -760px -440px; }
-.emoji-sheep { background-position: -760px -460px; }
-.emoji-shell { background-position: -760px -480px; }
-.emoji-shield { background-position: -760px -500px; }
-.emoji-shinto_shrine { background-position: -760px -520px; }
-.emoji-ship { background-position: -760px -540px; }
-.emoji-shirt { background-position: -760px -560px; }
-.emoji-shopping_bags { background-position: -760px -580px; }
-.emoji-shopping_cart { background-position: -760px -600px; }
-.emoji-shower { background-position: -760px -620px; }
-.emoji-shrimp { background-position: -760px -640px; }
-.emoji-shrug { background-position: -760px -660px; }
-.emoji-shrug_tone1 { background-position: -760px -680px; }
-.emoji-shrug_tone2 { background-position: -760px -700px; }
-.emoji-shrug_tone3 { background-position: -760px -720px; }
-.emoji-shrug_tone4 { background-position: -760px -740px; }
-.emoji-shrug_tone5 { background-position: 0 -760px; }
-.emoji-signal_strength { background-position: -20px -760px; }
-.emoji-six { background-position: -40px -760px; }
-.emoji-six_pointed_star { background-position: -60px -760px; }
-.emoji-ski { background-position: -80px -760px; }
-.emoji-skier { background-position: -100px -760px; }
-.emoji-skull { background-position: -120px -760px; }
-.emoji-skull_crossbones { background-position: -140px -760px; }
-.emoji-sleeping { background-position: -160px -760px; }
-.emoji-sleeping_accommodation { background-position: -180px -760px; }
-.emoji-sleepy { background-position: -200px -760px; }
-.emoji-slight_frown { background-position: -220px -760px; }
-.emoji-slight_smile { background-position: -240px -760px; }
-.emoji-slot_machine { background-position: -260px -760px; }
-.emoji-small_blue_diamond { background-position: -280px -760px; }
-.emoji-small_orange_diamond { background-position: -300px -760px; }
-.emoji-small_red_triangle { background-position: -320px -760px; }
-.emoji-small_red_triangle_down { background-position: -340px -760px; }
-.emoji-smile { background-position: -360px -760px; }
-.emoji-smile_cat { background-position: -380px -760px; }
-.emoji-smiley { background-position: -400px -760px; }
-.emoji-smiley_cat { background-position: -420px -760px; }
-.emoji-smiling_imp { background-position: -440px -760px; }
-.emoji-smirk { background-position: -460px -760px; }
-.emoji-smirk_cat { background-position: -480px -760px; }
-.emoji-smoking { background-position: -500px -760px; }
-.emoji-snail { background-position: -520px -760px; }
-.emoji-snake { background-position: -540px -760px; }
-.emoji-sneezing_face { background-position: -560px -760px; }
-.emoji-snowboarder { background-position: -580px -760px; }
-.emoji-snowflake { background-position: -600px -760px; }
-.emoji-snowman { background-position: -620px -760px; }
-.emoji-snowman2 { background-position: -640px -760px; }
-.emoji-sob { background-position: -660px -760px; }
-.emoji-soccer { background-position: -680px -760px; }
-.emoji-soon { background-position: -700px -760px; }
-.emoji-sos { background-position: -720px -760px; }
-.emoji-sound { background-position: -740px -760px; }
-.emoji-space_invader { background-position: -760px -760px; }
-.emoji-spades { background-position: -780px 0; }
-.emoji-spaghetti { background-position: -780px -20px; }
-.emoji-sparkle { background-position: -780px -40px; }
-.emoji-sparkler { background-position: -780px -60px; }
-.emoji-sparkles { background-position: -780px -80px; }
-.emoji-sparkling_heart { background-position: -780px -100px; }
-.emoji-speak_no_evil { background-position: -780px -120px; }
-.emoji-speaker { background-position: -780px -140px; }
-.emoji-speaking_head { background-position: -780px -160px; }
-.emoji-speech_balloon { background-position: -780px -180px; }
-.emoji-speedboat { background-position: -780px -200px; }
-.emoji-spider { background-position: -780px -220px; }
-.emoji-spider_web { background-position: -780px -240px; }
-.emoji-spoon { background-position: -780px -260px; }
-.emoji-spy { background-position: -780px -280px; }
-.emoji-spy_tone1 { background-position: -780px -300px; }
-.emoji-spy_tone2 { background-position: -780px -320px; }
-.emoji-spy_tone3 { background-position: -780px -340px; }
-.emoji-spy_tone4 { background-position: -780px -360px; }
-.emoji-spy_tone5 { background-position: -780px -380px; }
-.emoji-squid { background-position: -780px -400px; }
-.emoji-stadium { background-position: -780px -420px; }
-.emoji-star { background-position: -780px -440px; }
-.emoji-star2 { background-position: -780px -460px; }
-.emoji-star_and_crescent { background-position: -780px -480px; }
-.emoji-star_of_david { background-position: -780px -500px; }
-.emoji-stars { background-position: -780px -520px; }
-.emoji-station { background-position: -780px -540px; }
-.emoji-statue_of_liberty { background-position: -780px -560px; }
-.emoji-steam_locomotive { background-position: -780px -580px; }
-.emoji-stew { background-position: -780px -600px; }
-.emoji-stop_button { background-position: -780px -620px; }
-.emoji-stopwatch { background-position: -780px -640px; }
-.emoji-straight_ruler { background-position: -780px -660px; }
-.emoji-strawberry { background-position: -780px -680px; }
-.emoji-stuck_out_tongue { background-position: -780px -700px; }
-.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -720px; }
-.emoji-stuck_out_tongue_winking_eye { background-position: -780px -740px; }
-.emoji-stuffed_flatbread { background-position: -780px -760px; }
-.emoji-sun_with_face { background-position: 0 -780px; }
-.emoji-sunflower { background-position: -20px -780px; }
-.emoji-sunglasses { background-position: -40px -780px; }
-.emoji-sunny { background-position: -60px -780px; }
-.emoji-sunrise { background-position: -80px -780px; }
-.emoji-sunrise_over_mountains { background-position: -100px -780px; }
-.emoji-surfer { background-position: -120px -780px; }
-.emoji-surfer_tone1 { background-position: -140px -780px; }
-.emoji-surfer_tone2 { background-position: -160px -780px; }
-.emoji-surfer_tone3 { background-position: -180px -780px; }
-.emoji-surfer_tone4 { background-position: -200px -780px; }
-.emoji-surfer_tone5 { background-position: -220px -780px; }
-.emoji-sushi { background-position: -240px -780px; }
-.emoji-suspension_railway { background-position: -260px -780px; }
-.emoji-sweat { background-position: -280px -780px; }
-.emoji-sweat_drops { background-position: -300px -780px; }
-.emoji-sweat_smile { background-position: -320px -780px; }
-.emoji-sweet_potato { background-position: -340px -780px; }
-.emoji-swimmer { background-position: -360px -780px; }
-.emoji-swimmer_tone1 { background-position: -380px -780px; }
-.emoji-swimmer_tone2 { background-position: -400px -780px; }
-.emoji-swimmer_tone3 { background-position: -420px -780px; }
-.emoji-swimmer_tone4 { background-position: -440px -780px; }
-.emoji-swimmer_tone5 { background-position: -460px -780px; }
-.emoji-symbols { background-position: -480px -780px; }
-.emoji-synagogue { background-position: -500px -780px; }
-.emoji-syringe { background-position: -520px -780px; }
-.emoji-taco { background-position: -540px -780px; }
-.emoji-tada { background-position: -560px -780px; }
-.emoji-tanabata_tree { background-position: -580px -780px; }
-.emoji-tangerine { background-position: -600px -780px; }
-.emoji-taurus { background-position: -620px -780px; }
-.emoji-taxi { background-position: -640px -780px; }
-.emoji-tea { background-position: -660px -780px; }
-.emoji-telephone { background-position: -680px -780px; }
-.emoji-telephone_receiver { background-position: -700px -780px; }
-.emoji-telescope { background-position: -720px -780px; }
-.emoji-ten { background-position: -740px -780px; }
-.emoji-tennis { background-position: -760px -780px; }
-.emoji-tent { background-position: -780px -780px; }
-.emoji-thermometer { background-position: -800px 0; }
-.emoji-thermometer_face { background-position: -800px -20px; }
-.emoji-thinking { background-position: -800px -40px; }
-.emoji-third_place { background-position: -800px -60px; }
-.emoji-thought_balloon { background-position: -800px -80px; }
-.emoji-three { background-position: -800px -100px; }
-.emoji-thumbsdown { background-position: -800px -120px; }
-.emoji-thumbsdown_tone1 { background-position: -800px -140px; }
-.emoji-thumbsdown_tone2 { background-position: -800px -160px; }
-.emoji-thumbsdown_tone3 { background-position: -800px -180px; }
-.emoji-thumbsdown_tone4 { background-position: -800px -200px; }
-.emoji-thumbsdown_tone5 { background-position: -800px -220px; }
-.emoji-thumbsup { background-position: -800px -240px; }
-.emoji-thumbsup_tone1 { background-position: -800px -260px; }
-.emoji-thumbsup_tone2 { background-position: -800px -280px; }
-.emoji-thumbsup_tone3 { background-position: -800px -300px; }
-.emoji-thumbsup_tone4 { background-position: -800px -320px; }
-.emoji-thumbsup_tone5 { background-position: -800px -340px; }
-.emoji-thunder_cloud_rain { background-position: -800px -360px; }
-.emoji-ticket { background-position: -800px -380px; }
-.emoji-tickets { background-position: -800px -400px; }
-.emoji-tiger { background-position: -800px -420px; }
-.emoji-tiger2 { background-position: -800px -440px; }
-.emoji-timer { background-position: -800px -460px; }
-.emoji-tired_face { background-position: -800px -480px; }
-.emoji-tm { background-position: -800px -500px; }
-.emoji-toilet { background-position: -800px -520px; }
-.emoji-tokyo_tower { background-position: -800px -540px; }
-.emoji-tomato { background-position: -800px -560px; }
-.emoji-tone1 { background-position: -800px -580px; }
-.emoji-tone2 { background-position: -800px -600px; }
-.emoji-tone3 { background-position: -800px -620px; }
-.emoji-tone4 { background-position: -800px -640px; }
-.emoji-tone5 { background-position: -800px -660px; }
-.emoji-tongue { background-position: -800px -680px; }
-.emoji-tools { background-position: -800px -700px; }
-.emoji-top { background-position: -800px -720px; }
-.emoji-tophat { background-position: -800px -740px; }
-.emoji-track_next { background-position: -800px -760px; }
-.emoji-track_previous { background-position: -800px -780px; }
-.emoji-trackball { background-position: 0 -800px; }
-.emoji-tractor { background-position: -20px -800px; }
-.emoji-traffic_light { background-position: -40px -800px; }
-.emoji-train { background-position: -60px -800px; }
-.emoji-train2 { background-position: -80px -800px; }
-.emoji-tram { background-position: -100px -800px; }
-.emoji-triangular_flag_on_post { background-position: -120px -800px; }
-.emoji-triangular_ruler { background-position: -140px -800px; }
-.emoji-trident { background-position: -160px -800px; }
-.emoji-triumph { background-position: -180px -800px; }
-.emoji-trolleybus { background-position: -200px -800px; }
-.emoji-trophy { background-position: -220px -800px; }
-.emoji-tropical_drink { background-position: -240px -800px; }
-.emoji-tropical_fish { background-position: -260px -800px; }
-.emoji-truck { background-position: -280px -800px; }
-.emoji-trumpet { background-position: -300px -800px; }
-.emoji-tulip { background-position: -320px -800px; }
-.emoji-tumbler_glass { background-position: -340px -800px; }
-.emoji-turkey { background-position: -360px -800px; }
-.emoji-turtle { background-position: -380px -800px; }
-.emoji-tv { background-position: -400px -800px; }
-.emoji-twisted_rightwards_arrows { background-position: -420px -800px; }
-.emoji-two { background-position: -440px -800px; }
-.emoji-two_hearts { background-position: -460px -800px; }
-.emoji-two_men_holding_hands { background-position: -480px -800px; }
-.emoji-two_women_holding_hands { background-position: -500px -800px; }
-.emoji-u5272 { background-position: -520px -800px; }
-.emoji-u5408 { background-position: -540px -800px; }
-.emoji-u55b6 { background-position: -560px -800px; }
-.emoji-u6307 { background-position: -580px -800px; }
-.emoji-u6708 { background-position: -600px -800px; }
-.emoji-u6709 { background-position: -620px -800px; }
-.emoji-u6e80 { background-position: -640px -800px; }
-.emoji-u7121 { background-position: -660px -800px; }
-.emoji-u7533 { background-position: -680px -800px; }
-.emoji-u7981 { background-position: -700px -800px; }
-.emoji-u7a7a { background-position: -720px -800px; }
-.emoji-umbrella { background-position: -740px -800px; }
-.emoji-umbrella2 { background-position: -760px -800px; }
-.emoji-unamused { background-position: -780px -800px; }
-.emoji-underage { background-position: -800px -800px; }
-.emoji-unicorn { background-position: -820px 0; }
-.emoji-unlock { background-position: -820px -20px; }
-.emoji-up { background-position: -820px -40px; }
-.emoji-upside_down { background-position: -820px -60px; }
-.emoji-urn { background-position: -820px -80px; }
-.emoji-v { background-position: -820px -100px; }
-.emoji-v_tone1 { background-position: -820px -120px; }
-.emoji-v_tone2 { background-position: -820px -140px; }
-.emoji-v_tone3 { background-position: -820px -160px; }
-.emoji-v_tone4 { background-position: -820px -180px; }
-.emoji-v_tone5 { background-position: -820px -200px; }
-.emoji-vertical_traffic_light { background-position: -820px -220px; }
-.emoji-vhs { background-position: -820px -240px; }
-.emoji-vibration_mode { background-position: -820px -260px; }
-.emoji-video_camera { background-position: -820px -280px; }
-.emoji-video_game { background-position: -820px -300px; }
-.emoji-violin { background-position: -820px -320px; }
-.emoji-virgo { background-position: -820px -340px; }
-.emoji-volcano { background-position: -820px -360px; }
-.emoji-volleyball { background-position: -820px -380px; }
-.emoji-vs { background-position: -820px -400px; }
-.emoji-vulcan { background-position: -820px -420px; }
-.emoji-vulcan_tone1 { background-position: -820px -440px; }
-.emoji-vulcan_tone2 { background-position: -820px -460px; }
-.emoji-vulcan_tone3 { background-position: -820px -480px; }
-.emoji-vulcan_tone4 { background-position: -820px -500px; }
-.emoji-vulcan_tone5 { background-position: -820px -520px; }
-.emoji-walking { background-position: -820px -540px; }
-.emoji-walking_tone1 { background-position: -820px -560px; }
-.emoji-walking_tone2 { background-position: -820px -580px; }
-.emoji-walking_tone3 { background-position: -820px -600px; }
-.emoji-walking_tone4 { background-position: -820px -620px; }
-.emoji-walking_tone5 { background-position: -820px -640px; }
-.emoji-waning_crescent_moon { background-position: -820px -660px; }
-.emoji-waning_gibbous_moon { background-position: -820px -680px; }
-.emoji-warning { background-position: -820px -700px; }
-.emoji-wastebasket { background-position: -820px -720px; }
-.emoji-watch { background-position: -820px -740px; }
-.emoji-water_buffalo { background-position: -820px -760px; }
-.emoji-water_polo { background-position: -820px -780px; }
-.emoji-water_polo_tone1 { background-position: -820px -800px; }
-.emoji-water_polo_tone2 { background-position: 0 -820px; }
-.emoji-water_polo_tone3 { background-position: -20px -820px; }
-.emoji-water_polo_tone4 { background-position: -40px -820px; }
-.emoji-water_polo_tone5 { background-position: -60px -820px; }
-.emoji-watermelon { background-position: -80px -820px; }
-.emoji-wave { background-position: -100px -820px; }
-.emoji-wave_tone1 { background-position: -120px -820px; }
-.emoji-wave_tone2 { background-position: -140px -820px; }
-.emoji-wave_tone3 { background-position: -160px -820px; }
-.emoji-wave_tone4 { background-position: -180px -820px; }
-.emoji-wave_tone5 { background-position: -200px -820px; }
-.emoji-wavy_dash { background-position: -220px -820px; }
-.emoji-waxing_crescent_moon { background-position: -240px -820px; }
-.emoji-waxing_gibbous_moon { background-position: -260px -820px; }
-.emoji-wc { background-position: -280px -820px; }
-.emoji-weary { background-position: -300px -820px; }
-.emoji-wedding { background-position: -320px -820px; }
-.emoji-whale { background-position: -340px -820px; }
-.emoji-whale2 { background-position: -360px -820px; }
-.emoji-wheel_of_dharma { background-position: -380px -820px; }
-.emoji-wheelchair { background-position: -400px -820px; }
-.emoji-white_check_mark { background-position: -420px -820px; }
-.emoji-white_circle { background-position: -440px -820px; }
-.emoji-white_flower { background-position: -460px -820px; }
-.emoji-white_large_square { background-position: -480px -820px; }
-.emoji-white_medium_small_square { background-position: -500px -820px; }
-.emoji-white_medium_square { background-position: -520px -820px; }
-.emoji-white_small_square { background-position: -540px -820px; }
-.emoji-white_square_button { background-position: -560px -820px; }
-.emoji-white_sun_cloud { background-position: -580px -820px; }
-.emoji-white_sun_rain_cloud { background-position: -600px -820px; }
-.emoji-white_sun_small_cloud { background-position: -620px -820px; }
-.emoji-wilted_rose { background-position: -640px -820px; }
-.emoji-wind_blowing_face { background-position: -660px -820px; }
-.emoji-wind_chime { background-position: -680px -820px; }
-.emoji-wine_glass { background-position: -700px -820px; }
-.emoji-wink { background-position: -720px -820px; }
-.emoji-wolf { background-position: -740px -820px; }
-.emoji-woman { background-position: -760px -820px; }
-.emoji-woman_tone1 { background-position: -780px -820px; }
-.emoji-woman_tone2 { background-position: -800px -820px; }
-.emoji-woman_tone3 { background-position: -820px -820px; }
-.emoji-woman_tone4 { background-position: -840px 0; }
-.emoji-woman_tone5 { background-position: -840px -20px; }
-.emoji-womans_clothes { background-position: -840px -40px; }
-.emoji-womans_hat { background-position: -840px -60px; }
-.emoji-womens { background-position: -840px -80px; }
-.emoji-worried { background-position: -840px -100px; }
-.emoji-wrench { background-position: -840px -120px; }
-.emoji-wrestlers { background-position: -840px -140px; }
-.emoji-wrestlers_tone1 { background-position: -840px -160px; }
-.emoji-wrestlers_tone2 { background-position: -840px -180px; }
-.emoji-wrestlers_tone3 { background-position: -840px -200px; }
-.emoji-wrestlers_tone4 { background-position: -840px -220px; }
-.emoji-wrestlers_tone5 { background-position: -840px -240px; }
-.emoji-writing_hand { background-position: -840px -260px; }
-.emoji-writing_hand_tone1 { background-position: -840px -280px; }
-.emoji-writing_hand_tone2 { background-position: -840px -300px; }
-.emoji-writing_hand_tone3 { background-position: -840px -320px; }
-.emoji-writing_hand_tone4 { background-position: -840px -340px; }
-.emoji-writing_hand_tone5 { background-position: -840px -360px; }
-.emoji-x { background-position: -840px -380px; }
-.emoji-yellow_heart { background-position: -840px -400px; }
-.emoji-yen { background-position: -840px -420px; }
-.emoji-yin_yang { background-position: -840px -440px; }
-.emoji-yum { background-position: -840px -460px; }
-.emoji-zap { background-position: -840px -480px; }
-.emoji-zero { background-position: -840px -500px; }
-.emoji-zipper_mouth { background-position: -840px -520px; }
-.emoji-100 { background-position: -840px -540px; }
+.emoji-gay_pride_flag { background-position: -220px -540px; }
+.emoji-gear { background-position: -240px -540px; }
+.emoji-gem { background-position: -260px -540px; }
+.emoji-gemini { background-position: -280px -540px; }
+.emoji-ghost { background-position: -300px -540px; }
+.emoji-gift { background-position: -320px -540px; }
+.emoji-gift_heart { background-position: -340px -540px; }
+.emoji-girl { background-position: -360px -540px; }
+.emoji-girl_tone1 { background-position: -380px -540px; }
+.emoji-girl_tone2 { background-position: -400px -540px; }
+.emoji-girl_tone3 { background-position: -420px -540px; }
+.emoji-girl_tone4 { background-position: -440px -540px; }
+.emoji-girl_tone5 { background-position: -460px -540px; }
+.emoji-globe_with_meridians { background-position: -480px -540px; }
+.emoji-goal { background-position: -500px -540px; }
+.emoji-goat { background-position: -520px -540px; }
+.emoji-golf { background-position: -540px -540px; }
+.emoji-golfer { background-position: -560px 0; }
+.emoji-gorilla { background-position: -560px -20px; }
+.emoji-grapes { background-position: -560px -40px; }
+.emoji-green_apple { background-position: -560px -60px; }
+.emoji-green_book { background-position: -560px -80px; }
+.emoji-green_heart { background-position: -560px -100px; }
+.emoji-grey_exclamation { background-position: -560px -120px; }
+.emoji-grey_question { background-position: -560px -140px; }
+.emoji-grimacing { background-position: -560px -160px; }
+.emoji-grin { background-position: -560px -180px; }
+.emoji-grinning { background-position: -560px -200px; }
+.emoji-guardsman { background-position: -560px -220px; }
+.emoji-guardsman_tone1 { background-position: -560px -240px; }
+.emoji-guardsman_tone2 { background-position: -560px -260px; }
+.emoji-guardsman_tone3 { background-position: -560px -280px; }
+.emoji-guardsman_tone4 { background-position: -560px -300px; }
+.emoji-guardsman_tone5 { background-position: -560px -320px; }
+.emoji-guitar { background-position: -560px -340px; }
+.emoji-gun { background-position: -560px -360px; }
+.emoji-haircut { background-position: -560px -380px; }
+.emoji-haircut_tone1 { background-position: -560px -400px; }
+.emoji-haircut_tone2 { background-position: -560px -420px; }
+.emoji-haircut_tone3 { background-position: -560px -440px; }
+.emoji-haircut_tone4 { background-position: -560px -460px; }
+.emoji-haircut_tone5 { background-position: -560px -480px; }
+.emoji-hamburger { background-position: -560px -500px; }
+.emoji-hammer { background-position: -560px -520px; }
+.emoji-hammer_pick { background-position: -560px -540px; }
+.emoji-hamster { background-position: 0 -560px; }
+.emoji-hand_splayed { background-position: -20px -560px; }
+.emoji-hand_splayed_tone1 { background-position: -40px -560px; }
+.emoji-hand_splayed_tone2 { background-position: -60px -560px; }
+.emoji-hand_splayed_tone3 { background-position: -80px -560px; }
+.emoji-hand_splayed_tone4 { background-position: -100px -560px; }
+.emoji-hand_splayed_tone5 { background-position: -120px -560px; }
+.emoji-handbag { background-position: -140px -560px; }
+.emoji-handball { background-position: -160px -560px; }
+.emoji-handball_tone1 { background-position: -180px -560px; }
+.emoji-handball_tone2 { background-position: -200px -560px; }
+.emoji-handball_tone3 { background-position: -220px -560px; }
+.emoji-handball_tone4 { background-position: -240px -560px; }
+.emoji-handball_tone5 { background-position: -260px -560px; }
+.emoji-handshake { background-position: -280px -560px; }
+.emoji-handshake_tone1 { background-position: -300px -560px; }
+.emoji-handshake_tone2 { background-position: -320px -560px; }
+.emoji-handshake_tone3 { background-position: -340px -560px; }
+.emoji-handshake_tone4 { background-position: -360px -560px; }
+.emoji-handshake_tone5 { background-position: -380px -560px; }
+.emoji-hash { background-position: -400px -560px; }
+.emoji-hatched_chick { background-position: -420px -560px; }
+.emoji-hatching_chick { background-position: -440px -560px; }
+.emoji-head_bandage { background-position: -460px -560px; }
+.emoji-headphones { background-position: -480px -560px; }
+.emoji-hear_no_evil { background-position: -500px -560px; }
+.emoji-heart { background-position: -520px -560px; }
+.emoji-heart_decoration { background-position: -540px -560px; }
+.emoji-heart_exclamation { background-position: -560px -560px; }
+.emoji-heart_eyes { background-position: -580px 0; }
+.emoji-heart_eyes_cat { background-position: -580px -20px; }
+.emoji-heartbeat { background-position: -580px -40px; }
+.emoji-heartpulse { background-position: -580px -60px; }
+.emoji-hearts { background-position: -580px -80px; }
+.emoji-heavy_check_mark { background-position: -580px -100px; }
+.emoji-heavy_division_sign { background-position: -580px -120px; }
+.emoji-heavy_dollar_sign { background-position: -580px -140px; }
+.emoji-heavy_minus_sign { background-position: -580px -160px; }
+.emoji-heavy_multiplication_x { background-position: -580px -180px; }
+.emoji-heavy_plus_sign { background-position: -580px -200px; }
+.emoji-helicopter { background-position: -580px -220px; }
+.emoji-helmet_with_cross { background-position: -580px -240px; }
+.emoji-herb { background-position: -580px -260px; }
+.emoji-hibiscus { background-position: -580px -280px; }
+.emoji-high_brightness { background-position: -580px -300px; }
+.emoji-high_heel { background-position: -580px -320px; }
+.emoji-hockey { background-position: -580px -340px; }
+.emoji-hole { background-position: -580px -360px; }
+.emoji-homes { background-position: -580px -380px; }
+.emoji-honey_pot { background-position: -580px -400px; }
+.emoji-horse { background-position: -580px -420px; }
+.emoji-horse_racing { background-position: -580px -440px; }
+.emoji-horse_racing_tone1 { background-position: -580px -460px; }
+.emoji-horse_racing_tone2 { background-position: -580px -480px; }
+.emoji-horse_racing_tone3 { background-position: -580px -500px; }
+.emoji-horse_racing_tone4 { background-position: -580px -520px; }
+.emoji-horse_racing_tone5 { background-position: -580px -540px; }
+.emoji-hospital { background-position: -580px -560px; }
+.emoji-hot_pepper { background-position: 0 -580px; }
+.emoji-hotdog { background-position: -20px -580px; }
+.emoji-hotel { background-position: -40px -580px; }
+.emoji-hotsprings { background-position: -60px -580px; }
+.emoji-hourglass { background-position: -80px -580px; }
+.emoji-hourglass_flowing_sand { background-position: -100px -580px; }
+.emoji-house { background-position: -120px -580px; }
+.emoji-house_abandoned { background-position: -140px -580px; }
+.emoji-house_with_garden { background-position: -160px -580px; }
+.emoji-hugging { background-position: -180px -580px; }
+.emoji-hushed { background-position: -200px -580px; }
+.emoji-ice_cream { background-position: -220px -580px; }
+.emoji-ice_skate { background-position: -240px -580px; }
+.emoji-icecream { background-position: -260px -580px; }
+.emoji-id { background-position: -280px -580px; }
+.emoji-ideograph_advantage { background-position: -300px -580px; }
+.emoji-imp { background-position: -320px -580px; }
+.emoji-inbox_tray { background-position: -340px -580px; }
+.emoji-incoming_envelope { background-position: -360px -580px; }
+.emoji-information_desk_person { background-position: -380px -580px; }
+.emoji-information_desk_person_tone1 { background-position: -400px -580px; }
+.emoji-information_desk_person_tone2 { background-position: -420px -580px; }
+.emoji-information_desk_person_tone3 { background-position: -440px -580px; }
+.emoji-information_desk_person_tone4 { background-position: -460px -580px; }
+.emoji-information_desk_person_tone5 { background-position: -480px -580px; }
+.emoji-information_source { background-position: -500px -580px; }
+.emoji-innocent { background-position: -520px -580px; }
+.emoji-interrobang { background-position: -540px -580px; }
+.emoji-iphone { background-position: -560px -580px; }
+.emoji-island { background-position: -580px -580px; }
+.emoji-izakaya_lantern { background-position: -600px 0; }
+.emoji-jack_o_lantern { background-position: -600px -20px; }
+.emoji-japan { background-position: -600px -40px; }
+.emoji-japanese_castle { background-position: -600px -60px; }
+.emoji-japanese_goblin { background-position: -600px -80px; }
+.emoji-japanese_ogre { background-position: -600px -100px; }
+.emoji-jeans { background-position: -600px -120px; }
+.emoji-joy { background-position: -600px -140px; }
+.emoji-joy_cat { background-position: -600px -160px; }
+.emoji-joystick { background-position: -600px -180px; }
+.emoji-juggling { background-position: -600px -200px; }
+.emoji-juggling_tone1 { background-position: -600px -220px; }
+.emoji-juggling_tone2 { background-position: -600px -240px; }
+.emoji-juggling_tone3 { background-position: -600px -260px; }
+.emoji-juggling_tone4 { background-position: -600px -280px; }
+.emoji-juggling_tone5 { background-position: -600px -300px; }
+.emoji-kaaba { background-position: -600px -320px; }
+.emoji-key { background-position: -600px -340px; }
+.emoji-key2 { background-position: -600px -360px; }
+.emoji-keyboard { background-position: -600px -380px; }
+.emoji-kimono { background-position: -600px -400px; }
+.emoji-kiss { background-position: -600px -420px; }
+.emoji-kiss_mm { background-position: -600px -440px; }
+.emoji-kiss_ww { background-position: -600px -460px; }
+.emoji-kissing { background-position: -600px -480px; }
+.emoji-kissing_cat { background-position: -600px -500px; }
+.emoji-kissing_closed_eyes { background-position: -600px -520px; }
+.emoji-kissing_heart { background-position: -600px -540px; }
+.emoji-kissing_smiling_eyes { background-position: -600px -560px; }
+.emoji-kiwi { background-position: -600px -580px; }
+.emoji-knife { background-position: 0 -600px; }
+.emoji-koala { background-position: -20px -600px; }
+.emoji-koko { background-position: -40px -600px; }
+.emoji-label { background-position: -60px -600px; }
+.emoji-large_blue_circle { background-position: -80px -600px; }
+.emoji-large_blue_diamond { background-position: -100px -600px; }
+.emoji-large_orange_diamond { background-position: -120px -600px; }
+.emoji-last_quarter_moon { background-position: -140px -600px; }
+.emoji-last_quarter_moon_with_face { background-position: -160px -600px; }
+.emoji-laughing { background-position: -180px -600px; }
+.emoji-leaves { background-position: -200px -600px; }
+.emoji-ledger { background-position: -220px -600px; }
+.emoji-left_facing_fist { background-position: -240px -600px; }
+.emoji-left_facing_fist_tone1 { background-position: -260px -600px; }
+.emoji-left_facing_fist_tone2 { background-position: -280px -600px; }
+.emoji-left_facing_fist_tone3 { background-position: -300px -600px; }
+.emoji-left_facing_fist_tone4 { background-position: -320px -600px; }
+.emoji-left_facing_fist_tone5 { background-position: -340px -600px; }
+.emoji-left_luggage { background-position: -360px -600px; }
+.emoji-left_right_arrow { background-position: -380px -600px; }
+.emoji-leftwards_arrow_with_hook { background-position: -400px -600px; }
+.emoji-lemon { background-position: -420px -600px; }
+.emoji-leo { background-position: -440px -600px; }
+.emoji-leopard { background-position: -460px -600px; }
+.emoji-level_slider { background-position: -480px -600px; }
+.emoji-levitate { background-position: -500px -600px; }
+.emoji-libra { background-position: -520px -600px; }
+.emoji-lifter { background-position: -540px -600px; }
+.emoji-lifter_tone1 { background-position: -560px -600px; }
+.emoji-lifter_tone2 { background-position: -580px -600px; }
+.emoji-lifter_tone3 { background-position: -600px -600px; }
+.emoji-lifter_tone4 { background-position: -620px 0; }
+.emoji-lifter_tone5 { background-position: -620px -20px; }
+.emoji-light_rail { background-position: -620px -40px; }
+.emoji-link { background-position: -620px -60px; }
+.emoji-lion_face { background-position: -620px -80px; }
+.emoji-lips { background-position: -620px -100px; }
+.emoji-lipstick { background-position: -620px -120px; }
+.emoji-lizard { background-position: -620px -140px; }
+.emoji-lock { background-position: -620px -160px; }
+.emoji-lock_with_ink_pen { background-position: -620px -180px; }
+.emoji-lollipop { background-position: -620px -200px; }
+.emoji-loop { background-position: -620px -220px; }
+.emoji-loud_sound { background-position: -620px -240px; }
+.emoji-loudspeaker { background-position: -620px -260px; }
+.emoji-love_hotel { background-position: -620px -280px; }
+.emoji-love_letter { background-position: -620px -300px; }
+.emoji-low_brightness { background-position: -620px -320px; }
+.emoji-lying_face { background-position: -620px -340px; }
+.emoji-m { background-position: -620px -360px; }
+.emoji-mag { background-position: -620px -380px; }
+.emoji-mag_right { background-position: -620px -400px; }
+.emoji-mahjong { background-position: -620px -420px; }
+.emoji-mailbox { background-position: -620px -440px; }
+.emoji-mailbox_closed { background-position: -620px -460px; }
+.emoji-mailbox_with_mail { background-position: -620px -480px; }
+.emoji-mailbox_with_no_mail { background-position: -620px -500px; }
+.emoji-man { background-position: -620px -520px; }
+.emoji-man_dancing { background-position: -620px -540px; }
+.emoji-man_dancing_tone1 { background-position: -620px -560px; }
+.emoji-man_dancing_tone2 { background-position: -620px -580px; }
+.emoji-man_dancing_tone3 { background-position: -620px -600px; }
+.emoji-man_dancing_tone4 { background-position: 0 -620px; }
+.emoji-man_dancing_tone5 { background-position: -20px -620px; }
+.emoji-man_in_tuxedo { background-position: -40px -620px; }
+.emoji-man_in_tuxedo_tone1 { background-position: -60px -620px; }
+.emoji-man_in_tuxedo_tone2 { background-position: -80px -620px; }
+.emoji-man_in_tuxedo_tone3 { background-position: -100px -620px; }
+.emoji-man_in_tuxedo_tone4 { background-position: -120px -620px; }
+.emoji-man_in_tuxedo_tone5 { background-position: -140px -620px; }
+.emoji-man_tone1 { background-position: -160px -620px; }
+.emoji-man_tone2 { background-position: -180px -620px; }
+.emoji-man_tone3 { background-position: -200px -620px; }
+.emoji-man_tone4 { background-position: -220px -620px; }
+.emoji-man_tone5 { background-position: -240px -620px; }
+.emoji-man_with_gua_pi_mao { background-position: -260px -620px; }
+.emoji-man_with_gua_pi_mao_tone1 { background-position: -280px -620px; }
+.emoji-man_with_gua_pi_mao_tone2 { background-position: -300px -620px; }
+.emoji-man_with_gua_pi_mao_tone3 { background-position: -320px -620px; }
+.emoji-man_with_gua_pi_mao_tone4 { background-position: -340px -620px; }
+.emoji-man_with_gua_pi_mao_tone5 { background-position: -360px -620px; }
+.emoji-man_with_turban { background-position: -380px -620px; }
+.emoji-man_with_turban_tone1 { background-position: -400px -620px; }
+.emoji-man_with_turban_tone2 { background-position: -420px -620px; }
+.emoji-man_with_turban_tone3 { background-position: -440px -620px; }
+.emoji-man_with_turban_tone4 { background-position: -460px -620px; }
+.emoji-man_with_turban_tone5 { background-position: -480px -620px; }
+.emoji-mans_shoe { background-position: -500px -620px; }
+.emoji-map { background-position: -520px -620px; }
+.emoji-maple_leaf { background-position: -540px -620px; }
+.emoji-martial_arts_uniform { background-position: -560px -620px; }
+.emoji-mask { background-position: -580px -620px; }
+.emoji-massage { background-position: -600px -620px; }
+.emoji-massage_tone1 { background-position: -620px -620px; }
+.emoji-massage_tone2 { background-position: -640px 0; }
+.emoji-massage_tone3 { background-position: -640px -20px; }
+.emoji-massage_tone4 { background-position: -640px -40px; }
+.emoji-massage_tone5 { background-position: -640px -60px; }
+.emoji-meat_on_bone { background-position: -640px -80px; }
+.emoji-medal { background-position: -640px -100px; }
+.emoji-mega { background-position: -640px -120px; }
+.emoji-melon { background-position: -640px -140px; }
+.emoji-menorah { background-position: -640px -160px; }
+.emoji-mens { background-position: -640px -180px; }
+.emoji-metal { background-position: -640px -200px; }
+.emoji-metal_tone1 { background-position: -640px -220px; }
+.emoji-metal_tone2 { background-position: -640px -240px; }
+.emoji-metal_tone3 { background-position: -640px -260px; }
+.emoji-metal_tone4 { background-position: -640px -280px; }
+.emoji-metal_tone5 { background-position: -640px -300px; }
+.emoji-metro { background-position: -640px -320px; }
+.emoji-microphone { background-position: -640px -340px; }
+.emoji-microphone2 { background-position: -640px -360px; }
+.emoji-microscope { background-position: -640px -380px; }
+.emoji-middle_finger { background-position: -640px -400px; }
+.emoji-middle_finger_tone1 { background-position: -640px -420px; }
+.emoji-middle_finger_tone2 { background-position: -640px -440px; }
+.emoji-middle_finger_tone3 { background-position: -640px -460px; }
+.emoji-middle_finger_tone4 { background-position: -640px -480px; }
+.emoji-middle_finger_tone5 { background-position: -640px -500px; }
+.emoji-military_medal { background-position: -640px -520px; }
+.emoji-milk { background-position: -640px -540px; }
+.emoji-milky_way { background-position: -640px -560px; }
+.emoji-minibus { background-position: -640px -580px; }
+.emoji-minidisc { background-position: -640px -600px; }
+.emoji-mobile_phone_off { background-position: -640px -620px; }
+.emoji-money_mouth { background-position: 0 -640px; }
+.emoji-money_with_wings { background-position: -20px -640px; }
+.emoji-moneybag { background-position: -40px -640px; }
+.emoji-monkey { background-position: -60px -640px; }
+.emoji-monkey_face { background-position: -80px -640px; }
+.emoji-monorail { background-position: -100px -640px; }
+.emoji-mortar_board { background-position: -120px -640px; }
+.emoji-mosque { background-position: -140px -640px; }
+.emoji-motor_scooter { background-position: -160px -640px; }
+.emoji-motorboat { background-position: -180px -640px; }
+.emoji-motorcycle { background-position: -200px -640px; }
+.emoji-motorway { background-position: -220px -640px; }
+.emoji-mount_fuji { background-position: -240px -640px; }
+.emoji-mountain { background-position: -260px -640px; }
+.emoji-mountain_bicyclist { background-position: -280px -640px; }
+.emoji-mountain_bicyclist_tone1 { background-position: -300px -640px; }
+.emoji-mountain_bicyclist_tone2 { background-position: -320px -640px; }
+.emoji-mountain_bicyclist_tone3 { background-position: -340px -640px; }
+.emoji-mountain_bicyclist_tone4 { background-position: -360px -640px; }
+.emoji-mountain_bicyclist_tone5 { background-position: -380px -640px; }
+.emoji-mountain_cableway { background-position: -400px -640px; }
+.emoji-mountain_railway { background-position: -420px -640px; }
+.emoji-mountain_snow { background-position: -440px -640px; }
+.emoji-mouse { background-position: -460px -640px; }
+.emoji-mouse2 { background-position: -480px -640px; }
+.emoji-mouse_three_button { background-position: -500px -640px; }
+.emoji-movie_camera { background-position: -520px -640px; }
+.emoji-moyai { background-position: -540px -640px; }
+.emoji-mrs_claus { background-position: -560px -640px; }
+.emoji-mrs_claus_tone1 { background-position: -580px -640px; }
+.emoji-mrs_claus_tone2 { background-position: -600px -640px; }
+.emoji-mrs_claus_tone3 { background-position: -620px -640px; }
+.emoji-mrs_claus_tone4 { background-position: -640px -640px; }
+.emoji-mrs_claus_tone5 { background-position: -660px 0; }
+.emoji-muscle { background-position: -660px -20px; }
+.emoji-muscle_tone1 { background-position: -660px -40px; }
+.emoji-muscle_tone2 { background-position: -660px -60px; }
+.emoji-muscle_tone3 { background-position: -660px -80px; }
+.emoji-muscle_tone4 { background-position: -660px -100px; }
+.emoji-muscle_tone5 { background-position: -660px -120px; }
+.emoji-mushroom { background-position: -660px -140px; }
+.emoji-musical_keyboard { background-position: -660px -160px; }
+.emoji-musical_note { background-position: -660px -180px; }
+.emoji-musical_score { background-position: -660px -200px; }
+.emoji-mute { background-position: -660px -220px; }
+.emoji-nail_care { background-position: -660px -240px; }
+.emoji-nail_care_tone1 { background-position: -660px -260px; }
+.emoji-nail_care_tone2 { background-position: -660px -280px; }
+.emoji-nail_care_tone3 { background-position: -660px -300px; }
+.emoji-nail_care_tone4 { background-position: -660px -320px; }
+.emoji-nail_care_tone5 { background-position: -660px -340px; }
+.emoji-name_badge { background-position: -660px -360px; }
+.emoji-nauseated_face { background-position: -660px -380px; }
+.emoji-necktie { background-position: -660px -400px; }
+.emoji-negative_squared_cross_mark { background-position: -660px -420px; }
+.emoji-nerd { background-position: -660px -440px; }
+.emoji-neutral_face { background-position: -660px -460px; }
+.emoji-new { background-position: -660px -480px; }
+.emoji-new_moon { background-position: -660px -500px; }
+.emoji-new_moon_with_face { background-position: -660px -520px; }
+.emoji-newspaper { background-position: -660px -540px; }
+.emoji-newspaper2 { background-position: -660px -560px; }
+.emoji-ng { background-position: -660px -580px; }
+.emoji-night_with_stars { background-position: -660px -600px; }
+.emoji-nine { background-position: -660px -620px; }
+.emoji-no_bell { background-position: -660px -640px; }
+.emoji-no_bicycles { background-position: 0 -660px; }
+.emoji-no_entry { background-position: -20px -660px; }
+.emoji-no_entry_sign { background-position: -40px -660px; }
+.emoji-no_good { background-position: -60px -660px; }
+.emoji-no_good_tone1 { background-position: -80px -660px; }
+.emoji-no_good_tone2 { background-position: -100px -660px; }
+.emoji-no_good_tone3 { background-position: -120px -660px; }
+.emoji-no_good_tone4 { background-position: -140px -660px; }
+.emoji-no_good_tone5 { background-position: -160px -660px; }
+.emoji-no_mobile_phones { background-position: -180px -660px; }
+.emoji-no_mouth { background-position: -200px -660px; }
+.emoji-no_pedestrians { background-position: -220px -660px; }
+.emoji-no_smoking { background-position: -240px -660px; }
+.emoji-non-potable_water { background-position: -260px -660px; }
+.emoji-nose { background-position: -280px -660px; }
+.emoji-nose_tone1 { background-position: -300px -660px; }
+.emoji-nose_tone2 { background-position: -320px -660px; }
+.emoji-nose_tone3 { background-position: -340px -660px; }
+.emoji-nose_tone4 { background-position: -360px -660px; }
+.emoji-nose_tone5 { background-position: -380px -660px; }
+.emoji-notebook { background-position: -400px -660px; }
+.emoji-notebook_with_decorative_cover { background-position: -420px -660px; }
+.emoji-notepad_spiral { background-position: -440px -660px; }
+.emoji-notes { background-position: -460px -660px; }
+.emoji-nut_and_bolt { background-position: -480px -660px; }
+.emoji-o { background-position: -500px -660px; }
+.emoji-o2 { background-position: -520px -660px; }
+.emoji-ocean { background-position: -540px -660px; }
+.emoji-octagonal_sign { background-position: -560px -660px; }
+.emoji-octopus { background-position: -580px -660px; }
+.emoji-oden { background-position: -600px -660px; }
+.emoji-office { background-position: -620px -660px; }
+.emoji-oil { background-position: -640px -660px; }
+.emoji-ok { background-position: -660px -660px; }
+.emoji-ok_hand { background-position: -680px 0; }
+.emoji-ok_hand_tone1 { background-position: -680px -20px; }
+.emoji-ok_hand_tone2 { background-position: -680px -40px; }
+.emoji-ok_hand_tone3 { background-position: -680px -60px; }
+.emoji-ok_hand_tone4 { background-position: -680px -80px; }
+.emoji-ok_hand_tone5 { background-position: -680px -100px; }
+.emoji-ok_woman { background-position: -680px -120px; }
+.emoji-ok_woman_tone1 { background-position: -680px -140px; }
+.emoji-ok_woman_tone2 { background-position: -680px -160px; }
+.emoji-ok_woman_tone3 { background-position: -680px -180px; }
+.emoji-ok_woman_tone4 { background-position: -680px -200px; }
+.emoji-ok_woman_tone5 { background-position: -680px -220px; }
+.emoji-older_man { background-position: -680px -240px; }
+.emoji-older_man_tone1 { background-position: -680px -260px; }
+.emoji-older_man_tone2 { background-position: -680px -280px; }
+.emoji-older_man_tone3 { background-position: -680px -300px; }
+.emoji-older_man_tone4 { background-position: -680px -320px; }
+.emoji-older_man_tone5 { background-position: -680px -340px; }
+.emoji-older_woman { background-position: -680px -360px; }
+.emoji-older_woman_tone1 { background-position: -680px -380px; }
+.emoji-older_woman_tone2 { background-position: -680px -400px; }
+.emoji-older_woman_tone3 { background-position: -680px -420px; }
+.emoji-older_woman_tone4 { background-position: -680px -440px; }
+.emoji-older_woman_tone5 { background-position: -680px -460px; }
+.emoji-om_symbol { background-position: -680px -480px; }
+.emoji-on { background-position: -680px -500px; }
+.emoji-oncoming_automobile { background-position: -680px -520px; }
+.emoji-oncoming_bus { background-position: -680px -540px; }
+.emoji-oncoming_police_car { background-position: -680px -560px; }
+.emoji-oncoming_taxi { background-position: -680px -580px; }
+.emoji-one { background-position: -680px -600px; }
+.emoji-open_file_folder { background-position: -680px -620px; }
+.emoji-open_hands { background-position: -680px -640px; }
+.emoji-open_hands_tone1 { background-position: -680px -660px; }
+.emoji-open_hands_tone2 { background-position: 0 -680px; }
+.emoji-open_hands_tone3 { background-position: -20px -680px; }
+.emoji-open_hands_tone4 { background-position: -40px -680px; }
+.emoji-open_hands_tone5 { background-position: -60px -680px; }
+.emoji-open_mouth { background-position: -80px -680px; }
+.emoji-ophiuchus { background-position: -100px -680px; }
+.emoji-orange_book { background-position: -120px -680px; }
+.emoji-orthodox_cross { background-position: -140px -680px; }
+.emoji-outbox_tray { background-position: -160px -680px; }
+.emoji-owl { background-position: -180px -680px; }
+.emoji-ox { background-position: -200px -680px; }
+.emoji-package { background-position: -220px -680px; }
+.emoji-page_facing_up { background-position: -240px -680px; }
+.emoji-page_with_curl { background-position: -260px -680px; }
+.emoji-pager { background-position: -280px -680px; }
+.emoji-paintbrush { background-position: -300px -680px; }
+.emoji-palm_tree { background-position: -320px -680px; }
+.emoji-pancakes { background-position: -340px -680px; }
+.emoji-panda_face { background-position: -360px -680px; }
+.emoji-paperclip { background-position: -380px -680px; }
+.emoji-paperclips { background-position: -400px -680px; }
+.emoji-park { background-position: -420px -680px; }
+.emoji-parking { background-position: -440px -680px; }
+.emoji-part_alternation_mark { background-position: -460px -680px; }
+.emoji-partly_sunny { background-position: -480px -680px; }
+.emoji-passport_control { background-position: -500px -680px; }
+.emoji-pause_button { background-position: -520px -680px; }
+.emoji-peace { background-position: -540px -680px; }
+.emoji-peach { background-position: -560px -680px; }
+.emoji-peanuts { background-position: -580px -680px; }
+.emoji-pear { background-position: -600px -680px; }
+.emoji-pen_ballpoint { background-position: -620px -680px; }
+.emoji-pen_fountain { background-position: -640px -680px; }
+.emoji-pencil { background-position: -660px -680px; }
+.emoji-pencil2 { background-position: -680px -680px; }
+.emoji-penguin { background-position: -700px 0; }
+.emoji-pensive { background-position: -700px -20px; }
+.emoji-performing_arts { background-position: -700px -40px; }
+.emoji-persevere { background-position: -700px -60px; }
+.emoji-person_frowning { background-position: -700px -80px; }
+.emoji-person_frowning_tone1 { background-position: -700px -100px; }
+.emoji-person_frowning_tone2 { background-position: -700px -120px; }
+.emoji-person_frowning_tone3 { background-position: -700px -140px; }
+.emoji-person_frowning_tone4 { background-position: -700px -160px; }
+.emoji-person_frowning_tone5 { background-position: -700px -180px; }
+.emoji-person_with_blond_hair { background-position: -700px -200px; }
+.emoji-person_with_blond_hair_tone1 { background-position: -700px -220px; }
+.emoji-person_with_blond_hair_tone2 { background-position: -700px -240px; }
+.emoji-person_with_blond_hair_tone3 { background-position: -700px -260px; }
+.emoji-person_with_blond_hair_tone4 { background-position: -700px -280px; }
+.emoji-person_with_blond_hair_tone5 { background-position: -700px -300px; }
+.emoji-person_with_pouting_face { background-position: -700px -320px; }
+.emoji-person_with_pouting_face_tone1 { background-position: -700px -340px; }
+.emoji-person_with_pouting_face_tone2 { background-position: -700px -360px; }
+.emoji-person_with_pouting_face_tone3 { background-position: -700px -380px; }
+.emoji-person_with_pouting_face_tone4 { background-position: -700px -400px; }
+.emoji-person_with_pouting_face_tone5 { background-position: -700px -420px; }
+.emoji-pick { background-position: -700px -440px; }
+.emoji-pig { background-position: -700px -460px; }
+.emoji-pig2 { background-position: -700px -480px; }
+.emoji-pig_nose { background-position: -700px -500px; }
+.emoji-pill { background-position: -700px -520px; }
+.emoji-pineapple { background-position: -700px -540px; }
+.emoji-ping_pong { background-position: -700px -560px; }
+.emoji-pisces { background-position: -700px -580px; }
+.emoji-pizza { background-position: -700px -600px; }
+.emoji-place_of_worship { background-position: -700px -620px; }
+.emoji-play_pause { background-position: -700px -640px; }
+.emoji-point_down { background-position: -700px -660px; }
+.emoji-point_down_tone1 { background-position: -700px -680px; }
+.emoji-point_down_tone2 { background-position: 0 -700px; }
+.emoji-point_down_tone3 { background-position: -20px -700px; }
+.emoji-point_down_tone4 { background-position: -40px -700px; }
+.emoji-point_down_tone5 { background-position: -60px -700px; }
+.emoji-point_left { background-position: -80px -700px; }
+.emoji-point_left_tone1 { background-position: -100px -700px; }
+.emoji-point_left_tone2 { background-position: -120px -700px; }
+.emoji-point_left_tone3 { background-position: -140px -700px; }
+.emoji-point_left_tone4 { background-position: -160px -700px; }
+.emoji-point_left_tone5 { background-position: -180px -700px; }
+.emoji-point_right { background-position: -200px -700px; }
+.emoji-point_right_tone1 { background-position: -220px -700px; }
+.emoji-point_right_tone2 { background-position: -240px -700px; }
+.emoji-point_right_tone3 { background-position: -260px -700px; }
+.emoji-point_right_tone4 { background-position: -280px -700px; }
+.emoji-point_right_tone5 { background-position: -300px -700px; }
+.emoji-point_up { background-position: -320px -700px; }
+.emoji-point_up_2 { background-position: -340px -700px; }
+.emoji-point_up_2_tone1 { background-position: -360px -700px; }
+.emoji-point_up_2_tone2 { background-position: -380px -700px; }
+.emoji-point_up_2_tone3 { background-position: -400px -700px; }
+.emoji-point_up_2_tone4 { background-position: -420px -700px; }
+.emoji-point_up_2_tone5 { background-position: -440px -700px; }
+.emoji-point_up_tone1 { background-position: -460px -700px; }
+.emoji-point_up_tone2 { background-position: -480px -700px; }
+.emoji-point_up_tone3 { background-position: -500px -700px; }
+.emoji-point_up_tone4 { background-position: -520px -700px; }
+.emoji-point_up_tone5 { background-position: -540px -700px; }
+.emoji-police_car { background-position: -560px -700px; }
+.emoji-poodle { background-position: -580px -700px; }
+.emoji-poop { background-position: -600px -700px; }
+.emoji-popcorn { background-position: -620px -700px; }
+.emoji-post_office { background-position: -640px -700px; }
+.emoji-postal_horn { background-position: -660px -700px; }
+.emoji-postbox { background-position: -680px -700px; }
+.emoji-potable_water { background-position: -700px -700px; }
+.emoji-potato { background-position: -720px 0; }
+.emoji-pouch { background-position: -720px -20px; }
+.emoji-poultry_leg { background-position: -720px -40px; }
+.emoji-pound { background-position: -720px -60px; }
+.emoji-pouting_cat { background-position: -720px -80px; }
+.emoji-pray { background-position: -720px -100px; }
+.emoji-pray_tone1 { background-position: -720px -120px; }
+.emoji-pray_tone2 { background-position: -720px -140px; }
+.emoji-pray_tone3 { background-position: -720px -160px; }
+.emoji-pray_tone4 { background-position: -720px -180px; }
+.emoji-pray_tone5 { background-position: -720px -200px; }
+.emoji-prayer_beads { background-position: -720px -220px; }
+.emoji-pregnant_woman { background-position: -720px -240px; }
+.emoji-pregnant_woman_tone1 { background-position: -720px -260px; }
+.emoji-pregnant_woman_tone2 { background-position: -720px -280px; }
+.emoji-pregnant_woman_tone3 { background-position: -720px -300px; }
+.emoji-pregnant_woman_tone4 { background-position: -720px -320px; }
+.emoji-pregnant_woman_tone5 { background-position: -720px -340px; }
+.emoji-prince { background-position: -720px -360px; }
+.emoji-prince_tone1 { background-position: -720px -380px; }
+.emoji-prince_tone2 { background-position: -720px -400px; }
+.emoji-prince_tone3 { background-position: -720px -420px; }
+.emoji-prince_tone4 { background-position: -720px -440px; }
+.emoji-prince_tone5 { background-position: -720px -460px; }
+.emoji-princess { background-position: -720px -480px; }
+.emoji-princess_tone1 { background-position: -720px -500px; }
+.emoji-princess_tone2 { background-position: -720px -520px; }
+.emoji-princess_tone3 { background-position: -720px -540px; }
+.emoji-princess_tone4 { background-position: -720px -560px; }
+.emoji-princess_tone5 { background-position: -720px -580px; }
+.emoji-printer { background-position: -720px -600px; }
+.emoji-projector { background-position: -720px -620px; }
+.emoji-punch { background-position: -720px -640px; }
+.emoji-punch_tone1 { background-position: -720px -660px; }
+.emoji-punch_tone2 { background-position: -720px -680px; }
+.emoji-punch_tone3 { background-position: -720px -700px; }
+.emoji-punch_tone4 { background-position: 0 -720px; }
+.emoji-punch_tone5 { background-position: -20px -720px; }
+.emoji-purple_heart { background-position: -40px -720px; }
+.emoji-purse { background-position: -60px -720px; }
+.emoji-pushpin { background-position: -80px -720px; }
+.emoji-put_litter_in_its_place { background-position: -100px -720px; }
+.emoji-question { background-position: -120px -720px; }
+.emoji-rabbit { background-position: -140px -720px; }
+.emoji-rabbit2 { background-position: -160px -720px; }
+.emoji-race_car { background-position: -180px -720px; }
+.emoji-racehorse { background-position: -200px -720px; }
+.emoji-radio { background-position: -220px -720px; }
+.emoji-radio_button { background-position: -240px -720px; }
+.emoji-radioactive { background-position: -260px -720px; }
+.emoji-rage { background-position: -280px -720px; }
+.emoji-railway_car { background-position: -300px -720px; }
+.emoji-railway_track { background-position: -320px -720px; }
+.emoji-rainbow { background-position: -340px -720px; }
+.emoji-raised_back_of_hand { background-position: -360px -720px; }
+.emoji-raised_back_of_hand_tone1 { background-position: -380px -720px; }
+.emoji-raised_back_of_hand_tone2 { background-position: -400px -720px; }
+.emoji-raised_back_of_hand_tone3 { background-position: -420px -720px; }
+.emoji-raised_back_of_hand_tone4 { background-position: -440px -720px; }
+.emoji-raised_back_of_hand_tone5 { background-position: -460px -720px; }
+.emoji-raised_hand { background-position: -480px -720px; }
+.emoji-raised_hand_tone1 { background-position: -500px -720px; }
+.emoji-raised_hand_tone2 { background-position: -520px -720px; }
+.emoji-raised_hand_tone3 { background-position: -540px -720px; }
+.emoji-raised_hand_tone4 { background-position: -560px -720px; }
+.emoji-raised_hand_tone5 { background-position: -580px -720px; }
+.emoji-raised_hands { background-position: -600px -720px; }
+.emoji-raised_hands_tone1 { background-position: -620px -720px; }
+.emoji-raised_hands_tone2 { background-position: -640px -720px; }
+.emoji-raised_hands_tone3 { background-position: -660px -720px; }
+.emoji-raised_hands_tone4 { background-position: -680px -720px; }
+.emoji-raised_hands_tone5 { background-position: -700px -720px; }
+.emoji-raising_hand { background-position: -720px -720px; }
+.emoji-raising_hand_tone1 { background-position: -740px 0; }
+.emoji-raising_hand_tone2 { background-position: -740px -20px; }
+.emoji-raising_hand_tone3 { background-position: -740px -40px; }
+.emoji-raising_hand_tone4 { background-position: -740px -60px; }
+.emoji-raising_hand_tone5 { background-position: -740px -80px; }
+.emoji-ram { background-position: -740px -100px; }
+.emoji-ramen { background-position: -740px -120px; }
+.emoji-rat { background-position: -740px -140px; }
+.emoji-record_button { background-position: -740px -160px; }
+.emoji-recycle { background-position: -740px -180px; }
+.emoji-red_car { background-position: -740px -200px; }
+.emoji-red_circle { background-position: -740px -220px; }
+.emoji-registered { background-position: -740px -240px; }
+.emoji-relaxed { background-position: -740px -260px; }
+.emoji-relieved { background-position: -740px -280px; }
+.emoji-reminder_ribbon { background-position: -740px -300px; }
+.emoji-repeat { background-position: -740px -320px; }
+.emoji-repeat_one { background-position: -740px -340px; }
+.emoji-restroom { background-position: -740px -360px; }
+.emoji-revolving_hearts { background-position: -740px -380px; }
+.emoji-rewind { background-position: -740px -400px; }
+.emoji-rhino { background-position: -740px -420px; }
+.emoji-ribbon { background-position: -740px -440px; }
+.emoji-rice { background-position: -740px -460px; }
+.emoji-rice_ball { background-position: -740px -480px; }
+.emoji-rice_cracker { background-position: -740px -500px; }
+.emoji-rice_scene { background-position: -740px -520px; }
+.emoji-right_facing_fist { background-position: -740px -540px; }
+.emoji-right_facing_fist_tone1 { background-position: -740px -560px; }
+.emoji-right_facing_fist_tone2 { background-position: -740px -580px; }
+.emoji-right_facing_fist_tone3 { background-position: -740px -600px; }
+.emoji-right_facing_fist_tone4 { background-position: -740px -620px; }
+.emoji-right_facing_fist_tone5 { background-position: -740px -640px; }
+.emoji-ring { background-position: -740px -660px; }
+.emoji-robot { background-position: -740px -680px; }
+.emoji-rocket { background-position: -740px -700px; }
+.emoji-rofl { background-position: -740px -720px; }
+.emoji-roller_coaster { background-position: 0 -740px; }
+.emoji-rolling_eyes { background-position: -20px -740px; }
+.emoji-rooster { background-position: -40px -740px; }
+.emoji-rose { background-position: -60px -740px; }
+.emoji-rosette { background-position: -80px -740px; }
+.emoji-rotating_light { background-position: -100px -740px; }
+.emoji-round_pushpin { background-position: -120px -740px; }
+.emoji-rowboat { background-position: -140px -740px; }
+.emoji-rowboat_tone1 { background-position: -160px -740px; }
+.emoji-rowboat_tone2 { background-position: -180px -740px; }
+.emoji-rowboat_tone3 { background-position: -200px -740px; }
+.emoji-rowboat_tone4 { background-position: -220px -740px; }
+.emoji-rowboat_tone5 { background-position: -240px -740px; }
+.emoji-rugby_football { background-position: -260px -740px; }
+.emoji-runner { background-position: -280px -740px; }
+.emoji-runner_tone1 { background-position: -300px -740px; }
+.emoji-runner_tone2 { background-position: -320px -740px; }
+.emoji-runner_tone3 { background-position: -340px -740px; }
+.emoji-runner_tone4 { background-position: -360px -740px; }
+.emoji-runner_tone5 { background-position: -380px -740px; }
+.emoji-running_shirt_with_sash { background-position: -400px -740px; }
+.emoji-sa { background-position: -420px -740px; }
+.emoji-sagittarius { background-position: -440px -740px; }
+.emoji-sailboat { background-position: -460px -740px; }
+.emoji-sake { background-position: -480px -740px; }
+.emoji-salad { background-position: -500px -740px; }
+.emoji-sandal { background-position: -520px -740px; }
+.emoji-santa { background-position: -540px -740px; }
+.emoji-santa_tone1 { background-position: -560px -740px; }
+.emoji-santa_tone2 { background-position: -580px -740px; }
+.emoji-santa_tone3 { background-position: -600px -740px; }
+.emoji-santa_tone4 { background-position: -620px -740px; }
+.emoji-santa_tone5 { background-position: -640px -740px; }
+.emoji-satellite { background-position: -660px -740px; }
+.emoji-satellite_orbital { background-position: -680px -740px; }
+.emoji-saxophone { background-position: -700px -740px; }
+.emoji-scales { background-position: -720px -740px; }
+.emoji-school { background-position: -740px -740px; }
+.emoji-school_satchel { background-position: -760px 0; }
+.emoji-scissors { background-position: -760px -20px; }
+.emoji-scooter { background-position: -760px -40px; }
+.emoji-scorpion { background-position: -760px -60px; }
+.emoji-scorpius { background-position: -760px -80px; }
+.emoji-scream { background-position: -760px -100px; }
+.emoji-scream_cat { background-position: -760px -120px; }
+.emoji-scroll { background-position: -760px -140px; }
+.emoji-seat { background-position: -760px -160px; }
+.emoji-second_place { background-position: -760px -180px; }
+.emoji-secret { background-position: -760px -200px; }
+.emoji-see_no_evil { background-position: -760px -220px; }
+.emoji-seedling { background-position: -760px -240px; }
+.emoji-selfie { background-position: -760px -260px; }
+.emoji-selfie_tone1 { background-position: -760px -280px; }
+.emoji-selfie_tone2 { background-position: -760px -300px; }
+.emoji-selfie_tone3 { background-position: -760px -320px; }
+.emoji-selfie_tone4 { background-position: -760px -340px; }
+.emoji-selfie_tone5 { background-position: -760px -360px; }
+.emoji-seven { background-position: -760px -380px; }
+.emoji-shallow_pan_of_food { background-position: -760px -400px; }
+.emoji-shamrock { background-position: -760px -420px; }
+.emoji-shark { background-position: -760px -440px; }
+.emoji-shaved_ice { background-position: -760px -460px; }
+.emoji-sheep { background-position: -760px -480px; }
+.emoji-shell { background-position: -760px -500px; }
+.emoji-shield { background-position: -760px -520px; }
+.emoji-shinto_shrine { background-position: -760px -540px; }
+.emoji-ship { background-position: -760px -560px; }
+.emoji-shirt { background-position: -760px -580px; }
+.emoji-shopping_bags { background-position: -760px -600px; }
+.emoji-shopping_cart { background-position: -760px -620px; }
+.emoji-shower { background-position: -760px -640px; }
+.emoji-shrimp { background-position: -760px -660px; }
+.emoji-shrug { background-position: -760px -680px; }
+.emoji-shrug_tone1 { background-position: -760px -700px; }
+.emoji-shrug_tone2 { background-position: -760px -720px; }
+.emoji-shrug_tone3 { background-position: -760px -740px; }
+.emoji-shrug_tone4 { background-position: 0 -760px; }
+.emoji-shrug_tone5 { background-position: -20px -760px; }
+.emoji-signal_strength { background-position: -40px -760px; }
+.emoji-six { background-position: -60px -760px; }
+.emoji-six_pointed_star { background-position: -80px -760px; }
+.emoji-ski { background-position: -100px -760px; }
+.emoji-skier { background-position: -120px -760px; }
+.emoji-skull { background-position: -140px -760px; }
+.emoji-skull_crossbones { background-position: -160px -760px; }
+.emoji-sleeping { background-position: -180px -760px; }
+.emoji-sleeping_accommodation { background-position: -200px -760px; }
+.emoji-sleepy { background-position: -220px -760px; }
+.emoji-slight_frown { background-position: -240px -760px; }
+.emoji-slight_smile { background-position: -260px -760px; }
+.emoji-slot_machine { background-position: -280px -760px; }
+.emoji-small_blue_diamond { background-position: -300px -760px; }
+.emoji-small_orange_diamond { background-position: -320px -760px; }
+.emoji-small_red_triangle { background-position: -340px -760px; }
+.emoji-small_red_triangle_down { background-position: -360px -760px; }
+.emoji-smile { background-position: -380px -760px; }
+.emoji-smile_cat { background-position: -400px -760px; }
+.emoji-smiley { background-position: -420px -760px; }
+.emoji-smiley_cat { background-position: -440px -760px; }
+.emoji-smiling_imp { background-position: -460px -760px; }
+.emoji-smirk { background-position: -480px -760px; }
+.emoji-smirk_cat { background-position: -500px -760px; }
+.emoji-smoking { background-position: -520px -760px; }
+.emoji-snail { background-position: -540px -760px; }
+.emoji-snake { background-position: -560px -760px; }
+.emoji-sneezing_face { background-position: -580px -760px; }
+.emoji-snowboarder { background-position: -600px -760px; }
+.emoji-snowflake { background-position: -620px -760px; }
+.emoji-snowman { background-position: -640px -760px; }
+.emoji-snowman2 { background-position: -660px -760px; }
+.emoji-sob { background-position: -680px -760px; }
+.emoji-soccer { background-position: -700px -760px; }
+.emoji-soon { background-position: -720px -760px; }
+.emoji-sos { background-position: -740px -760px; }
+.emoji-sound { background-position: -760px -760px; }
+.emoji-space_invader { background-position: -780px 0; }
+.emoji-spades { background-position: -780px -20px; }
+.emoji-spaghetti { background-position: -780px -40px; }
+.emoji-sparkle { background-position: -780px -60px; }
+.emoji-sparkler { background-position: -780px -80px; }
+.emoji-sparkles { background-position: -780px -100px; }
+.emoji-sparkling_heart { background-position: -780px -120px; }
+.emoji-speak_no_evil { background-position: -780px -140px; }
+.emoji-speaker { background-position: -780px -160px; }
+.emoji-speaking_head { background-position: -780px -180px; }
+.emoji-speech_balloon { background-position: -780px -200px; }
+.emoji-speech_left { background-position: -780px -220px; }
+.emoji-speedboat { background-position: -780px -240px; }
+.emoji-spider { background-position: -780px -260px; }
+.emoji-spider_web { background-position: -780px -280px; }
+.emoji-spoon { background-position: -780px -300px; }
+.emoji-spy { background-position: -780px -320px; }
+.emoji-spy_tone1 { background-position: -780px -340px; }
+.emoji-spy_tone2 { background-position: -780px -360px; }
+.emoji-spy_tone3 { background-position: -780px -380px; }
+.emoji-spy_tone4 { background-position: -780px -400px; }
+.emoji-spy_tone5 { background-position: -780px -420px; }
+.emoji-squid { background-position: -780px -440px; }
+.emoji-stadium { background-position: -780px -460px; }
+.emoji-star { background-position: -780px -480px; }
+.emoji-star2 { background-position: -780px -500px; }
+.emoji-star_and_crescent { background-position: -780px -520px; }
+.emoji-star_of_david { background-position: -780px -540px; }
+.emoji-stars { background-position: -780px -560px; }
+.emoji-station { background-position: -780px -580px; }
+.emoji-statue_of_liberty { background-position: -780px -600px; }
+.emoji-steam_locomotive { background-position: -780px -620px; }
+.emoji-stew { background-position: -780px -640px; }
+.emoji-stop_button { background-position: -780px -660px; }
+.emoji-stopwatch { background-position: -780px -680px; }
+.emoji-straight_ruler { background-position: -780px -700px; }
+.emoji-strawberry { background-position: -780px -720px; }
+.emoji-stuck_out_tongue { background-position: -780px -740px; }
+.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -760px; }
+.emoji-stuck_out_tongue_winking_eye { background-position: 0 -780px; }
+.emoji-stuffed_flatbread { background-position: -20px -780px; }
+.emoji-sun_with_face { background-position: -40px -780px; }
+.emoji-sunflower { background-position: -60px -780px; }
+.emoji-sunglasses { background-position: -80px -780px; }
+.emoji-sunny { background-position: -100px -780px; }
+.emoji-sunrise { background-position: -120px -780px; }
+.emoji-sunrise_over_mountains { background-position: -140px -780px; }
+.emoji-surfer { background-position: -160px -780px; }
+.emoji-surfer_tone1 { background-position: -180px -780px; }
+.emoji-surfer_tone2 { background-position: -200px -780px; }
+.emoji-surfer_tone3 { background-position: -220px -780px; }
+.emoji-surfer_tone4 { background-position: -240px -780px; }
+.emoji-surfer_tone5 { background-position: -260px -780px; }
+.emoji-sushi { background-position: -280px -780px; }
+.emoji-suspension_railway { background-position: -300px -780px; }
+.emoji-sweat { background-position: -320px -780px; }
+.emoji-sweat_drops { background-position: -340px -780px; }
+.emoji-sweat_smile { background-position: -360px -780px; }
+.emoji-sweet_potato { background-position: -380px -780px; }
+.emoji-swimmer { background-position: -400px -780px; }
+.emoji-swimmer_tone1 { background-position: -420px -780px; }
+.emoji-swimmer_tone2 { background-position: -440px -780px; }
+.emoji-swimmer_tone3 { background-position: -460px -780px; }
+.emoji-swimmer_tone4 { background-position: -480px -780px; }
+.emoji-swimmer_tone5 { background-position: -500px -780px; }
+.emoji-symbols { background-position: -520px -780px; }
+.emoji-synagogue { background-position: -540px -780px; }
+.emoji-syringe { background-position: -560px -780px; }
+.emoji-taco { background-position: -580px -780px; }
+.emoji-tada { background-position: -600px -780px; }
+.emoji-tanabata_tree { background-position: -620px -780px; }
+.emoji-tangerine { background-position: -640px -780px; }
+.emoji-taurus { background-position: -660px -780px; }
+.emoji-taxi { background-position: -680px -780px; }
+.emoji-tea { background-position: -700px -780px; }
+.emoji-telephone { background-position: -720px -780px; }
+.emoji-telephone_receiver { background-position: -740px -780px; }
+.emoji-telescope { background-position: -760px -780px; }
+.emoji-ten { background-position: -780px -780px; }
+.emoji-tennis { background-position: -800px 0; }
+.emoji-tent { background-position: -800px -20px; }
+.emoji-thermometer { background-position: -800px -40px; }
+.emoji-thermometer_face { background-position: -800px -60px; }
+.emoji-thinking { background-position: -800px -80px; }
+.emoji-third_place { background-position: -800px -100px; }
+.emoji-thought_balloon { background-position: -800px -120px; }
+.emoji-three { background-position: -800px -140px; }
+.emoji-thumbsdown { background-position: -800px -160px; }
+.emoji-thumbsdown_tone1 { background-position: -800px -180px; }
+.emoji-thumbsdown_tone2 { background-position: -800px -200px; }
+.emoji-thumbsdown_tone3 { background-position: -800px -220px; }
+.emoji-thumbsdown_tone4 { background-position: -800px -240px; }
+.emoji-thumbsdown_tone5 { background-position: -800px -260px; }
+.emoji-thumbsup { background-position: -800px -280px; }
+.emoji-thumbsup_tone1 { background-position: -800px -300px; }
+.emoji-thumbsup_tone2 { background-position: -800px -320px; }
+.emoji-thumbsup_tone3 { background-position: -800px -340px; }
+.emoji-thumbsup_tone4 { background-position: -800px -360px; }
+.emoji-thumbsup_tone5 { background-position: -800px -380px; }
+.emoji-thunder_cloud_rain { background-position: -800px -400px; }
+.emoji-ticket { background-position: -800px -420px; }
+.emoji-tickets { background-position: -800px -440px; }
+.emoji-tiger { background-position: -800px -460px; }
+.emoji-tiger2 { background-position: -800px -480px; }
+.emoji-timer { background-position: -800px -500px; }
+.emoji-tired_face { background-position: -800px -520px; }
+.emoji-tm { background-position: -800px -540px; }
+.emoji-toilet { background-position: -800px -560px; }
+.emoji-tokyo_tower { background-position: -800px -580px; }
+.emoji-tomato { background-position: -800px -600px; }
+.emoji-tone1 { background-position: -800px -620px; }
+.emoji-tone2 { background-position: -800px -640px; }
+.emoji-tone3 { background-position: -800px -660px; }
+.emoji-tone4 { background-position: -800px -680px; }
+.emoji-tone5 { background-position: -800px -700px; }
+.emoji-tongue { background-position: -800px -720px; }
+.emoji-tools { background-position: -800px -740px; }
+.emoji-top { background-position: -800px -760px; }
+.emoji-tophat { background-position: -800px -780px; }
+.emoji-track_next { background-position: 0 -800px; }
+.emoji-track_previous { background-position: -20px -800px; }
+.emoji-trackball { background-position: -40px -800px; }
+.emoji-tractor { background-position: -60px -800px; }
+.emoji-traffic_light { background-position: -80px -800px; }
+.emoji-train { background-position: -100px -800px; }
+.emoji-train2 { background-position: -120px -800px; }
+.emoji-tram { background-position: -140px -800px; }
+.emoji-triangular_flag_on_post { background-position: -160px -800px; }
+.emoji-triangular_ruler { background-position: -180px -800px; }
+.emoji-trident { background-position: -200px -800px; }
+.emoji-triumph { background-position: -220px -800px; }
+.emoji-trolleybus { background-position: -240px -800px; }
+.emoji-trophy { background-position: -260px -800px; }
+.emoji-tropical_drink { background-position: -280px -800px; }
+.emoji-tropical_fish { background-position: -300px -800px; }
+.emoji-truck { background-position: -320px -800px; }
+.emoji-trumpet { background-position: -340px -800px; }
+.emoji-tulip { background-position: -360px -800px; }
+.emoji-tumbler_glass { background-position: -380px -800px; }
+.emoji-turkey { background-position: -400px -800px; }
+.emoji-turtle { background-position: -420px -800px; }
+.emoji-tv { background-position: -440px -800px; }
+.emoji-twisted_rightwards_arrows { background-position: -460px -800px; }
+.emoji-two { background-position: -480px -800px; }
+.emoji-two_hearts { background-position: -500px -800px; }
+.emoji-two_men_holding_hands { background-position: -520px -800px; }
+.emoji-two_women_holding_hands { background-position: -540px -800px; }
+.emoji-u5272 { background-position: -560px -800px; }
+.emoji-u5408 { background-position: -580px -800px; }
+.emoji-u55b6 { background-position: -600px -800px; }
+.emoji-u6307 { background-position: -620px -800px; }
+.emoji-u6708 { background-position: -640px -800px; }
+.emoji-u6709 { background-position: -660px -800px; }
+.emoji-u6e80 { background-position: -680px -800px; }
+.emoji-u7121 { background-position: -700px -800px; }
+.emoji-u7533 { background-position: -720px -800px; }
+.emoji-u7981 { background-position: -740px -800px; }
+.emoji-u7a7a { background-position: -760px -800px; }
+.emoji-umbrella { background-position: -780px -800px; }
+.emoji-umbrella2 { background-position: -800px -800px; }
+.emoji-unamused { background-position: -820px 0; }
+.emoji-underage { background-position: -820px -20px; }
+.emoji-unicorn { background-position: -820px -40px; }
+.emoji-unlock { background-position: -820px -60px; }
+.emoji-up { background-position: -820px -80px; }
+.emoji-upside_down { background-position: -820px -100px; }
+.emoji-urn { background-position: -820px -120px; }
+.emoji-v { background-position: -820px -140px; }
+.emoji-v_tone1 { background-position: -820px -160px; }
+.emoji-v_tone2 { background-position: -820px -180px; }
+.emoji-v_tone3 { background-position: -820px -200px; }
+.emoji-v_tone4 { background-position: -820px -220px; }
+.emoji-v_tone5 { background-position: -820px -240px; }
+.emoji-vertical_traffic_light { background-position: -820px -260px; }
+.emoji-vhs { background-position: -820px -280px; }
+.emoji-vibration_mode { background-position: -820px -300px; }
+.emoji-video_camera { background-position: -820px -320px; }
+.emoji-video_game { background-position: -820px -340px; }
+.emoji-violin { background-position: -820px -360px; }
+.emoji-virgo { background-position: -820px -380px; }
+.emoji-volcano { background-position: -820px -400px; }
+.emoji-volleyball { background-position: -820px -420px; }
+.emoji-vs { background-position: -820px -440px; }
+.emoji-vulcan { background-position: -820px -460px; }
+.emoji-vulcan_tone1 { background-position: -820px -480px; }
+.emoji-vulcan_tone2 { background-position: -820px -500px; }
+.emoji-vulcan_tone3 { background-position: -820px -520px; }
+.emoji-vulcan_tone4 { background-position: -820px -540px; }
+.emoji-vulcan_tone5 { background-position: -820px -560px; }
+.emoji-walking { background-position: -820px -580px; }
+.emoji-walking_tone1 { background-position: -820px -600px; }
+.emoji-walking_tone2 { background-position: -820px -620px; }
+.emoji-walking_tone3 { background-position: -820px -640px; }
+.emoji-walking_tone4 { background-position: -820px -660px; }
+.emoji-walking_tone5 { background-position: -820px -680px; }
+.emoji-waning_crescent_moon { background-position: -820px -700px; }
+.emoji-waning_gibbous_moon { background-position: -820px -720px; }
+.emoji-warning { background-position: -820px -740px; }
+.emoji-wastebasket { background-position: -820px -760px; }
+.emoji-watch { background-position: -820px -780px; }
+.emoji-water_buffalo { background-position: -820px -800px; }
+.emoji-water_polo { background-position: 0 -820px; }
+.emoji-water_polo_tone1 { background-position: -20px -820px; }
+.emoji-water_polo_tone2 { background-position: -40px -820px; }
+.emoji-water_polo_tone3 { background-position: -60px -820px; }
+.emoji-water_polo_tone4 { background-position: -80px -820px; }
+.emoji-water_polo_tone5 { background-position: -100px -820px; }
+.emoji-watermelon { background-position: -120px -820px; }
+.emoji-wave { background-position: -140px -820px; }
+.emoji-wave_tone1 { background-position: -160px -820px; }
+.emoji-wave_tone2 { background-position: -180px -820px; }
+.emoji-wave_tone3 { background-position: -200px -820px; }
+.emoji-wave_tone4 { background-position: -220px -820px; }
+.emoji-wave_tone5 { background-position: -240px -820px; }
+.emoji-wavy_dash { background-position: -260px -820px; }
+.emoji-waxing_crescent_moon { background-position: -280px -820px; }
+.emoji-waxing_gibbous_moon { background-position: -300px -820px; }
+.emoji-wc { background-position: -320px -820px; }
+.emoji-weary { background-position: -340px -820px; }
+.emoji-wedding { background-position: -360px -820px; }
+.emoji-whale { background-position: -380px -820px; }
+.emoji-whale2 { background-position: -400px -820px; }
+.emoji-wheel_of_dharma { background-position: -420px -820px; }
+.emoji-wheelchair { background-position: -440px -820px; }
+.emoji-white_check_mark { background-position: -460px -820px; }
+.emoji-white_circle { background-position: -480px -820px; }
+.emoji-white_flower { background-position: -500px -820px; }
+.emoji-white_large_square { background-position: -520px -820px; }
+.emoji-white_medium_small_square { background-position: -540px -820px; }
+.emoji-white_medium_square { background-position: -560px -820px; }
+.emoji-white_small_square { background-position: -580px -820px; }
+.emoji-white_square_button { background-position: -600px -820px; }
+.emoji-white_sun_cloud { background-position: -620px -820px; }
+.emoji-white_sun_rain_cloud { background-position: -640px -820px; }
+.emoji-white_sun_small_cloud { background-position: -660px -820px; }
+.emoji-wilted_rose { background-position: -680px -820px; }
+.emoji-wind_blowing_face { background-position: -700px -820px; }
+.emoji-wind_chime { background-position: -720px -820px; }
+.emoji-wine_glass { background-position: -740px -820px; }
+.emoji-wink { background-position: -760px -820px; }
+.emoji-wolf { background-position: -780px -820px; }
+.emoji-woman { background-position: -800px -820px; }
+.emoji-woman_tone1 { background-position: -820px -820px; }
+.emoji-woman_tone2 { background-position: -840px 0; }
+.emoji-woman_tone3 { background-position: -840px -20px; }
+.emoji-woman_tone4 { background-position: -840px -40px; }
+.emoji-woman_tone5 { background-position: -840px -60px; }
+.emoji-womans_clothes { background-position: -840px -80px; }
+.emoji-womans_hat { background-position: -840px -100px; }
+.emoji-womens { background-position: -840px -120px; }
+.emoji-worried { background-position: -840px -140px; }
+.emoji-wrench { background-position: -840px -160px; }
+.emoji-wrestlers { background-position: -840px -180px; }
+.emoji-wrestlers_tone1 { background-position: -840px -200px; }
+.emoji-wrestlers_tone2 { background-position: -840px -220px; }
+.emoji-wrestlers_tone3 { background-position: -840px -240px; }
+.emoji-wrestlers_tone4 { background-position: -840px -260px; }
+.emoji-wrestlers_tone5 { background-position: -840px -280px; }
+.emoji-writing_hand { background-position: -840px -300px; }
+.emoji-writing_hand_tone1 { background-position: -840px -320px; }
+.emoji-writing_hand_tone2 { background-position: -840px -340px; }
+.emoji-writing_hand_tone3 { background-position: -840px -360px; }
+.emoji-writing_hand_tone4 { background-position: -840px -380px; }
+.emoji-writing_hand_tone5 { background-position: -840px -400px; }
+.emoji-x { background-position: -840px -420px; }
+.emoji-yellow_heart { background-position: -840px -440px; }
+.emoji-yen { background-position: -840px -460px; }
+.emoji-yin_yang { background-position: -840px -480px; }
+.emoji-yum { background-position: -840px -500px; }
+.emoji-zap { background-position: -840px -520px; }
+.emoji-zero { background-position: -840px -540px; }
+.emoji-zipper_mouth { background-position: -840px -560px; }
+.emoji-100 { background-position: -840px -580px; }
.emoji-icon {
background-image: image-url('emoji.png');
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 5833ef939e9..c2a3cd16e67 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -101,13 +101,13 @@
@for $i from 0 through 5 {
.legend-box-#{$i} {
- background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
+ background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
}
}
@for $i from 1 through 4 {
.legend-box-#{$i + 5} {
- background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
+ background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
}
}
}
@@ -142,7 +142,7 @@
*/
&.blame {
table {
- border: none;
+ border: 0;
margin: 0;
}
@@ -150,65 +150,65 @@
border-bottom: 1px solid $blame-border;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
}
td {
- border-top: none;
- border-bottom: none;
+ border-top: 0;
+ border-bottom: 0;
&:first-child {
- border-left: none;
+ border-left: 0;
}
&:last-child {
- border-right: none;
+ border-right: 0;
}
- }
- td.blame-commit {
- padding: 5px 10px;
- min-width: 400px;
- max-width: 400px;
- background: $gray-light;
- border-left: 3px solid;
+ &.blame-commit {
+ padding: 5px 10px;
+ min-width: 400px;
+ max-width: 400px;
+ background: $gray-light;
+ border-left: 3px solid;
+
+ .commit-row-title {
+ display: flex;
+ }
+
+ .item-title {
+ flex: 1;
+ margin-right: 0.5em;
+ }
+ }
+
+ &.line-numbers {
+ float: none;
+ border-left: 1px solid $blame-line-numbers-border;
- .commit-row-title {
- display: flex;
+ i {
+ float: none;
+ margin-right: 0;
+ }
}
- .item-title {
- flex: 1;
- margin-right: 0.5em;
+ &.lines {
+ padding: 0;
}
}
@for $i from 0 through 5 {
td.blame-commit-age-#{$i} {
- border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
+ border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
}
}
@for $i from 1 through 4 {
td.blame-commit-age-#{$i + 5} {
- border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
- }
- }
-
- td.line-numbers {
- float: none;
- border-left: 1px solid $blame-line-numbers-border;
-
- i {
- float: none;
- margin-right: 0;
+ border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
}
}
-
- td.lines {
- padding: 0;
- }
}
&.logs {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 0d80a85d521..cf8165eab5b 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -255,7 +255,7 @@
.clear-search {
width: 35px;
background-color: $white-light;
- border: none;
+ border: 0;
outline: none;
z-index: 1;
@@ -268,12 +268,6 @@
.filtered-search-box-input-container {
flex: 1;
position: relative;
- // Fix PhantomJS not supporting `flex: 1;` properly.
- // This is important because it can change the expected `e.target` when clicking things in tests.
- // See https://gitlab.com/gitlab-org/gitlab-ce/blob/b54acba8b732688c59fe2f38510c469dc86ee499/spec/features/issues/filtered_search/visual_tokens_spec.rb#L61
- // - With `width: 100%`: `e.target` = `.tokens-container`, https://i.imgur.com/jGq7wbx.png
- // - Without `width: 100%`: `e.target` = `.filtered-search`, https://i.imgur.com/cNI2CyT.png
- width: 100%;
min-width: 0;
}
@@ -311,16 +305,11 @@
color: $gl-text-color;
border-color: $dropdown-input-focus-border;
outline: none;
-
- svg {
- fill: $gl-text-color;
- }
}
svg {
height: 14px;
width: 14px;
- fill: $gl-text-color-secondary;
vertical-align: middle;
}
@@ -424,7 +413,7 @@
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn {
- border: none;
+ border: 0;
width: 100%;
text-align: left;
padding: 8px 16px;
@@ -469,10 +458,10 @@
word-break: break-all;
}
}
-}
-.filter-dropdown-item.droplab-item-active .btn {
- @extend %filter-dropdown-item-btn-hover;
+ &.droplab-item-active .btn {
+ @extend %filter-dropdown-item-btn-hover;
+ }
}
.filter-dropdown-loading {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index dc591c06c88..db36e27fa74 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -30,10 +30,6 @@
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
-
- svg {
- fill: currentColor;
- }
}
&.line-separator {
@@ -51,10 +47,6 @@
color: $color-200;
> a {
- svg {
- fill: $color-200;
- }
-
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 62ba74ff582..f985a3aea5c 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -10,7 +10,7 @@
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
- border: none;
+ border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
@@ -129,7 +129,7 @@
margin: 5px 2px 5px -8px;
border-radius: $border-radius-default;
- svg {
+ .tanuki-logo {
@media (min-width: $screen-sm-min) {
margin-right: 8px;
}
@@ -169,7 +169,7 @@
.navbar-collapse {
flex: 0 0 auto;
- border-top: none;
+ border-top: 0;
padding: 0;
@media (max-width: $screen-xs-max) {
@@ -235,10 +235,6 @@
opacity: 1;
color: $white-light;
- svg {
- fill: currentColor;
- }
-
&.header-user-dropdown-toggle .header-user-avatar {
border-color: $white-light;
}
@@ -269,14 +265,6 @@
font-size: 20px;
}
}
-
- &.active > a,
- &.dropdown.open > a {
-
- svg {
- fill: currentColor;
- }
- }
}
}
}
@@ -289,10 +277,6 @@
text-decoration: none;
outline: 0;
color: $white-light;
-
- svg {
- fill: currentColor;
- }
}
> a {
@@ -307,10 +291,6 @@
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
-
- svg {
- fill: currentColor;
- }
}
&.line-separator {
@@ -401,10 +381,13 @@
.breadcrumbs-list {
display: -webkit-flex;
display: flex;
- flex-wrap: wrap;
margin-bottom: 0;
line-height: 16px;
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
+ }
+
> li {
display: flex;
align-items: center;
@@ -412,24 +395,35 @@
padding: 2px 0;
&:not(:last-child) {
- margin-right: 20px;
+ padding-right: 20px;
+
+ &:not(.dropdown) {
+ overflow: hidden;
+ }
}
> a {
font-size: 12px;
color: currentColor;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 0 1 auto;
}
}
}
.breadcrumb-item-text {
- @include str-truncated(128px);
text-decoration: inherit;
+
+ @media (max-width: $screen-xs-max) {
+ @include str-truncated(128px);
+ }
}
.breadcrumbs-list-angle {
position: absolute;
- right: -12px;
+ right: 7px;
top: 50%;
color: $gl-text-color-tertiary;
transform: translateY(-50%);
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index c63114f85b4..813a1711ea2 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -1,5 +1,5 @@
.file-content.code {
- border: none;
+ border: 0;
box-shadow: none;
margin: 0;
padding: 0;
@@ -7,7 +7,7 @@
pre {
padding: 10px 0;
- border: none;
+ border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index ef864e8f6a9..1ab5e6a93f9 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,63 +1,35 @@
.ci-status-icon-success,
.ci-status-icon-passed {
color: $green-500;
-
- svg {
- fill: $green-500;
- }
}
.ci-status-icon-failed {
color: $gl-danger;
-
- svg {
- fill: $gl-danger;
- }
}
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
color: $orange-500;
-
- svg {
- fill: $orange-500;
- }
}
.ci-status-icon-running {
color: $blue-400;
-
- svg {
- fill: $blue-400;
- }
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
}
.ci-status-icon-created,
.ci-status-icon-skipped {
color: $gray-darkest;
-
- svg {
- fill: $gray-darkest;
- }
}
.ci-status-icon-manual {
color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
}
.icon-link {
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 6819fd88b7f..78a8e57ddbb 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -27,6 +27,8 @@
}
svg {
+ fill: currentColor;
+
&.s8 { @include svg-size(8px); }
&.s12 { @include svg-size(12px); }
&.s16 { @include svg-size(16px); }
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 69d19ea2962..cb324ccc440 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -30,10 +30,10 @@ body {
.container {
padding-top: 0;
z-index: 5;
-}
-.container .content {
- margin: 0;
+ .content {
+ margin: 0;
+ }
}
.navless-container {
@@ -82,26 +82,26 @@ body {
transition: background-color 0.15s, border-color 0.15s;
background-color: $orange-500;
border-color: $orange-500;
- }
- .alert-warning + .alert-warning {
- background-color: $orange-600;
- border-color: $orange-600;
- }
+ &:only-of-type {
+ background-color: $orange-500;
+ border-color: $orange-500;
+ }
- .alert-warning + .alert-warning + .alert-warning {
- background-color: $orange-700;
- border-color: $orange-700;
- }
+ + .alert-warning {
+ background-color: $orange-600;
+ border-color: $orange-600;
- .alert-warning + .alert-warning + .alert-warning + .alert-warning {
- background-color: $orange-800;
- border-color: $orange-800;
- }
+ + .alert-warning {
+ background-color: $orange-700;
+ border-color: $orange-700;
- .alert-warning:only-of-type {
- background-color: $orange-500;
- border-color: $orange-500;
+ + .alert-warning {
+ background-color: $orange-800;
+ border-color: $orange-800;
+ }
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index d43f998cb82..ad3bb0e35d1 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -42,7 +42,7 @@
}
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
&.bottom {
background: $gray-light;
@@ -92,7 +92,7 @@ ul.unstyled-list {
}
ul.unstyled-list > li {
- border-bottom: none;
+ border-bottom: 0;
}
// Generic content list
@@ -178,7 +178,7 @@ ul.content-list {
// When dragging a list item
&.ui-sortable-helper {
- border-bottom: none;
+ border-bottom: 0;
}
&.list-placeholder {
@@ -295,44 +295,44 @@ ul.indent-list {
}
> .group-list-tree > .group-row.has-children:first-child {
- border-top: none;
+ border-top: 0;
}
}
-.group-list-tree .avatar-container.content-loading {
- position: relative;
+.group-list-tree {
+ .avatar-container.content-loading {
+ position: relative;
- > a,
- > a .avatar {
- height: 100%;
- border-radius: 50%;
- }
+ > a,
+ > a .avatar {
+ height: 100%;
+ border-radius: 50%;
+ }
- > a {
- padding: 2px;
- }
+ > a {
+ padding: 2px;
- > a .avatar {
- border: 2px solid $white-normal;
+ .avatar {
+ border: 2px solid $white-normal;
- &.identicon {
- line-height: 30px;
+ &.identicon {
+ line-height: 30px;
+ }
+ }
}
- }
- &::after {
- content: "";
- position: absolute;
- height: 100%;
- width: 100%;
- background-color: transparent;
- border: 2px outset $kdb-border;
- border-radius: 50%;
- animation: spin-avatar 3s infinite linear;
+ &::after {
+ content: "";
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background-color: transparent;
+ border: 2px outset $kdb-border;
+ border-radius: 50%;
+ animation: spin-avatar 3s infinite linear;
+ }
}
-}
-.group-list-tree {
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
@@ -413,7 +413,7 @@ ul.indent-list {
padding: 0;
&.has-children {
- border-top: none;
+ border-top: 0;
}
&:first-child {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index e3920b5d3d9..5389eb0a5f2 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -57,6 +57,7 @@
.md-header {
.nav-links {
a {
+ width: 100%;
padding-top: 0;
line-height: 19px;
@@ -72,6 +73,28 @@
}
}
+.md-header-tab {
+ @media(max-width: $screen-xs-max) {
+ flex: 1;
+ width: 100%;
+ border-bottom: 1px solid $border-color;
+ text-align: center;
+ }
+}
+
+.md-header-toolbar {
+ margin-left: auto;
+
+ @media(max-width: $screen-xs-max) {
+ flex: none;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding-top: $gl-padding-top;
+ padding-bottom: $gl-padding-top;
+ }
+}
+
.referenced-users {
color: $gl-text-color;
padding-top: 10px;
@@ -126,27 +149,35 @@
}
}
-.toolbar-group {
- float: left;
- margin-right: -5px;
- margin-left: $gl-padding;
-
- &:first-child {
- margin-left: 0;
- }
-}
-
.toolbar-btn {
float: left;
- padding: 0 5px;
- color: $gl-text-color-secondary;
+ padding: 0 7px;
background: transparent;
border: 0;
outline: 0;
+ svg {
+ width: 14px;
+ height: 14px;
+ margin-top: 3px;
+ fill: $gl-text-color-secondary;
+ }
+
&:hover,
&:focus {
- color: $gl-link-color;
+ svg {
+ fill: $gl-link-color;
+ }
+ }
+}
+
+.toolbar-fullscreen-btn {
+ margin-left: $gl-padding;
+ margin-right: -5px;
+
+ @media(max-width: $screen-xs-max) {
+ margin-left: 0;
+ margin-right: 0;
}
}
@@ -173,21 +204,8 @@
ul > li {
white-space: nowrap;
}
-}
-
-@media(max-width: $screen-xs-max) {
- .atwho-view-ul {
- width: 350px;
- }
-
- .atwho-view ul li {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-// TODO: fallback to global style
-.atwho-view {
+ // TODO: fallback to global style
.atwho-view-ul {
padding: 8px 1px;
@@ -220,3 +238,14 @@
}
}
}
+
+@media(max-width: $screen-xs-max) {
+ .atwho-view-ul {
+ width: 350px;
+ }
+
+ .atwho-view ul li {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 2fee2164190..e12b5aab381 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -36,7 +36,7 @@
margin: 0;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
&.active {
@@ -130,14 +130,6 @@
background-color: $color-light;
color: $color-dark;
border-color: $color-dark;
-
- svg {
- fill: $color-dark;
- }
- }
-
- svg {
- fill: $color-main;
}
}
@@ -180,3 +172,31 @@
display: none;
}
}
+
+@mixin triangle($color, $border-color, $size, $border-size) {
+ &::before,
+ &::after {
+ bottom: 100%;
+ left: 50%;
+ border: solid transparent;
+ content: '';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ }
+
+ &::before {
+ border-color: transparent;
+ border-bottom-color: $border-color;
+ border-width: ($size + $border-size);
+ margin-left: -($size + $border-size);
+ }
+
+ &::after {
+ border-color: transparent;
+ border-bottom-color: $color;
+ border-width: $size;
+ margin-left: -$size;
+ }
+}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index d218fb6d702..5c9838c1029 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -7,6 +7,7 @@
}
.modal-body {
+ background-color: $modal-body-bg;
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
@@ -47,6 +48,3 @@ body.modal-open {
display: block;
}
-.modal-body {
- background-color: $modal-body-bg;
-}
diff --git a/app/assets/stylesheets/framework/popup.scss b/app/assets/stylesheets/framework/popup.scss
new file mode 100644
index 00000000000..5c76205095f
--- /dev/null
+++ b/app/assets/stylesheets/framework/popup.scss
@@ -0,0 +1,15 @@
+.popup {
+ @include triangle(
+ $gray-lighter,
+ $gray-darker,
+ $popup-triangle-size,
+ $popup-triangle-border-size
+ );
+
+ padding: $gl-padding;
+ background-color: $gray-lighter;
+ border: 1px solid $gray-darker;
+ border-radius: $border-radius-default;
+ box-shadow: 0 5px 8px $popup-box-shadow-color;
+ position: relative;
+}
diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 8e653c443cf..7829d722560 100644
--- a/app/assets/stylesheets/framework/responsive-tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -3,57 +3,77 @@
max-width: #{$max + '%'};
}
+.gl-responsive-table-row-layout {
+ width: 100%;
+
+ @media (min-width: $screen-md-min) {
+ display: flex;
+ align-items: center;
+
+ & > &:not(:first-child) {
+ margin-top: $gl-padding;
+ }
+ }
+}
+
.gl-responsive-table-row {
+ @extend .gl-responsive-table-row-layout;
margin-top: 10px;
border: 1px solid $border-color;
@media (min-width: $screen-md-min) {
- padding: 15px 0;
margin: 0;
- display: flex;
- align-items: center;
- border: none;
- border-bottom: 1px solid $white-normal;
+ padding: $gl-padding 0;
+ border: 0;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ }
}
+}
- .table-section {
- white-space: nowrap;
+.gl-responsive-table-row-col-span {
+ flex-wrap: wrap;
+}
+
+.table-section {
+ white-space: nowrap;
- $section-widths: 10 15 20 25 30 40;
- @each $width in $section-widths {
- &.section-#{$width} {
- flex: 0 0 #{$width + '%'};
+ $section-widths: 10 15 20 25 30 40 100;
+ @each $width in $section-widths {
+ &.section-#{$width} {
+ flex: 0 0 #{$width + '%'};
- @media (min-width: $screen-md-min) {
- max-width: #{$width + '%'};
- }
+ @media (min-width: $screen-md-min) {
+ max-width: #{$width + '%'};
}
}
+ }
- &:not(.table-button-footer) {
- @media (max-width: $screen-sm-max) {
- display: flex;
- align-self: stretch;
- padding: 10px;
- align-items: center;
- min-height: 62px;
+ @media (max-width: $screen-sm-max) {
+ display: flex;
+ align-self: stretch;
+ padding: 10px;
+ align-items: center;
+ min-height: 62px;
- &:not(:first-of-type) {
- border-top: 1px solid $white-normal;
- }
- }
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
}
+ }
- &.section-wrap {
- white-space: normal;
+ &.section-wrap {
+ white-space: normal;
- @media (max-width: $screen-sm-max) {
- flex-wrap: wrap;
- }
+ @media (max-width: $screen-sm-max) {
+ flex-wrap: wrap;
}
}
-}
+ &.section-align-top {
+ align-self: flex-start;
+ }
+}
.table-button-footer {
@media (min-width: $screen-md-min) {
@@ -61,12 +81,13 @@
}
@media (max-width: $screen-sm-max) {
- background-color: $gray-normal;
+ display: block;
align-self: stretch;
+ min-height: 0;
+ background-color: $gray-normal;
border-top: 1px solid $border-color;
.table-action-buttons {
- padding: 10px 5px;
display: flex;
.btn {
@@ -77,7 +98,14 @@
> .external-url,
> .btn {
flex: 1 1 28px;
- margin: 0 5px;
+
+ &:not(:first-child) {
+ margin-left: 5px;
+ }
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
}
.dropdown-new {
diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
index 3fd2549b143..8498b37abe4 100644
--- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -63,7 +63,7 @@
.nav-links {
margin-bottom: 0;
- border-bottom: none;
+ border-bottom: 0;
float: left;
&.wide {
@@ -335,7 +335,7 @@
border-bottom: 1px solid $border-color;
.nav-links {
- border-bottom: none;
+ border-bottom: 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 621eec4f158..bb70b270299 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -17,7 +17,7 @@
.select2-arrow {
background-image: none;
background-color: transparent;
- border: none;
+ border: 0;
padding-top: 12px;
padding-right: 20px;
font-size: 10px;
@@ -73,11 +73,6 @@
margin-top: -6px;
}
-.select2-results li.select2-result-with-children > .select2-result-label {
- font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
-}
-
.select2-container-active {
.select2-choice,
.select2-choices {
@@ -144,28 +139,28 @@
.select2-drop-auto-width & {
padding: 15px 15px 5px;
}
-}
-.select2-search input {
- padding: 2px 25px 2px 5px;
- background: $white-light image-url('select2.png');
- background-repeat: no-repeat;
- background-position: right 0 bottom 6px;
- border: 1px solid $input-border;
- border-radius: $border-radius-default;
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-
- &:focus {
- border-color: $input-border-focus;
- }
-}
+ input {
+ padding: 2px 25px 2px 5px;
+ background: $white-light image-url('select2.png');
+ background-repeat: no-repeat;
+ background-position: right 0 bottom 6px;
+ border: 1px solid $input-border;
+ border-radius: $border-radius-default;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-.select2-search input.select2-active {
- background-color: $white-light;
- background-image: image-url('select2-spinner.gif') !important;
- background-repeat: no-repeat;
- background-position: right 5px center !important;
- background-size: 16px 16px !important;
+ &:focus {
+ border-color: $input-border-focus;
+ }
+
+ &.select2-active {
+ background-color: $white-light;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-repeat: no-repeat;
+ background-position: right 5px center !important;
+ background-size: 16px 16px !important;
+ }
+ }
}
.select2-results {
@@ -197,6 +192,11 @@
.select2-result {
padding: 0 1px;
}
+
+ li.select2-result-with-children > .select2-result-label {
+ font-weight: $gl-font-weight-bold;
+ color: $gl-text-color;
+ }
}
.ajax-users-select {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index ef58382ba41..1a19b7320a0 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -9,7 +9,7 @@
&.container-blank {
background: none;
padding: 0;
- border: none;
+ border: 0;
}
}
}
@@ -111,7 +111,7 @@
}
.block:last-of-type {
- border: none;
+ border: 0;
}
}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 4dd31bf28cd..5bde96caf42 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -33,7 +33,7 @@ table {
th {
background-color: $gray-light;
font-weight: $gl-font-weight-normal;
- border-bottom: none;
+ border-bottom: 0;
&.wide {
width: 55%;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index f718ec4bcad..373f35e71d8 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -21,7 +21,7 @@
}
&.text-file .diff-file {
- border-bottom: none;
+ border-bottom: 0;
}
}
@@ -66,5 +66,5 @@
.discussion .timeline-entry {
margin: 0;
- border-right: none;
+ border-right: 0;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 3c0b4c82d19..0817cce114c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -167,7 +167,7 @@
&.plain-readme {
background: none;
- border: none;
+ border: 0;
padding: 0;
margin: 0;
font-size: 14px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8ab48e4844f..cb2a237f574 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -163,7 +163,7 @@ $gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6;
-$gl-text-color-inverted: rgba(255, 255, 255, 1.0);
+$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
@@ -486,8 +486,8 @@ $callout-success-color: $green-700;
/*
* Commit Page
*/
-$commit-max-width-marker-color: rgba(0, 0, 0, 0.0);
-$commit-message-text-area-bg: rgba(0, 0, 0, 0.0);
+$commit-max-width-marker-color: rgba(0, 0, 0, 0);
+$commit-message-text-area-bg: rgba(0, 0, 0, 0);
/*
* Common
@@ -719,3 +719,10 @@ Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
+
+/*
+Popup
+*/
+$popup-triangle-size: 15px;
+$popup-triangle-border-size: 1px;
+$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 5f9756bf58a..68824ff8418 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -52,6 +52,37 @@
.label.label-gray {
background-color: $well-expand-item;
}
+
+ .branches {
+ display: inline;
+ }
+
+ .branch-link {
+ margin-bottom: 2px;
+ }
+
+ .limit-box {
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ background-color: $red-100;
+ border-radius: $border-radius-default;
+ text-align: center;
+
+ &:hover {
+ background-color: $red-200;
+ }
+
+ .limit-icon {
+ margin: 0 8px;
+ }
+
+ .limit-message {
+ line-height: 16px;
+ margin-right: 8px;
+ font-size: 12px;
+ }
+ }
}
.light-well {
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 0c226ff7598..dbd3144b9b4 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -9,7 +9,7 @@
z-index: 1031;
textarea {
- border: none;
+ border: 0;
box-shadow: none;
border-radius: 0;
color: $black;
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 65b140cd7f8..c3d8f0c61a2 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -217,13 +217,31 @@ $white-gc-bg: #eaf2f5;
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
- .gd { color: $white-gd; background-color: $white-gd-bg; }
- .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; }
+
+ .gd {
+ color: $white-gd;
+ background-color: $white-gd-bg;
+
+ .x {
+ color: $white-gd-x;
+ background-color: $white-gd-x-bg;
+ }
+ }
+
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
- .gi { color: $white-gi; background-color: $white-gi-bg; }
- .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; }
+
+ .gi {
+ color: $white-gi;
+ background-color: $white-gi-bg;
+
+ .x {
+ color: $white-gi-x;
+ background-color: $white-gi-x-bg;
+ }
+ }
+
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index fbe538ad1d7..658ac26fca9 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -158,13 +158,31 @@ span.highlight_word {
.cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $highlighted-c1; font-style: italic; }
.cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
-.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; }
-.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; }
+
+.gd {
+ color: $highlighted-gd;
+ background-color: $highlighted-gd-bg;
+
+ .x {
+ color: $highlighted-gd;
+ background-color: $highlighted-gd-x-bg;
+ }
+}
+
.ge { font-style: italic; }
.gr { color: $highlighted-gr; }
.gh { color: $highlighted-gh; }
-.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; }
-.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; }
+
+.gi {
+ color: $highlighted-gi;
+ background-color: $highlighted-gi-bg;
+
+ .x {
+ color: $highlighted-gi;
+ background-color: $highlighted-gi-x-bg;
+ }
+}
+
.go { color: $highlighted-go; }
.gp { color: $highlighted-gp; }
.gs { font-weight: $gl-font-weight-bold; }
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 91296b354a7..3683afa07de 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -72,7 +72,7 @@
}
.boards-list {
- height: calc(100vh - 152px);
+ height: calc(100vh - 105px);
width: 100%;
padding-top: 25px;
padding-bottom: 25px;
@@ -81,11 +81,12 @@
overflow-x: scroll;
white-space: nowrap;
- @media (min-width: $screen-sm-min) {
- height: 475px; // Needed for PhantomJS
- // scss-lint:disable DuplicateProperty
- height: calc(100vh - 222px);
- // scss-lint:enable DuplicateProperty
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ height: calc(100vh - 90px);
+ }
+
+ @media (min-width: $screen-md-min) {
+ height: calc(100vh - 160px);
min-height: 475px;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 50ec5110bf1..f139f4ab650 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -48,7 +48,8 @@
overflow-x: auto;
font-size: 12px;
border-radius: 0;
- border: none;
+ border: 0;
+ padding: $grid-size;
.bash {
display: block;
@@ -57,29 +58,28 @@
.top-bar {
height: 35px;
- display: flex;
- justify-content: flex-end;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
top: $header-height;
+ padding: $grid-size;
&.affix {
top: $header-height;
- }
- // with sidebar
- &.affix.sidebar-expanded {
- right: 306px;
- left: 16px;
- }
+ // with sidebar
+ &.sidebar-expanded {
+ right: 306px;
+ left: 16px;
+ }
- // without sidebar
- &.affix.sidebar-collapsed {
- right: 16px;
- left: 16px;
+ // without sidebar
+ &.sidebar-collapsed {
+ right: 16px;
+ left: 16px;
+ }
}
&.affix-top {
@@ -90,9 +90,6 @@
}
.truncated-info {
- margin: 0 auto;
- align-self: center;
-
.truncated-info-size {
margin: 0 5px;
}
@@ -118,7 +115,11 @@
.controllers-buttons {
color: $gl-text-color;
- margin: 0 10px;
+ margin: 0 $grid-size;
+
+ &:last-child {
+ margin-right: 0;
+ }
}
.btn-scroll.animate {
@@ -333,8 +334,10 @@
svg {
position: relative;
- top: 2px;
+ top: 3px;
margin-right: 3px;
+ width: 14px;
+ height: 14px;
}
}
@@ -348,9 +351,10 @@
svg {
position: relative;
- top: 2px;
+ top: 3px;
margin-right: 3px;
- height: 13px;
+ height: 14px;
+ width: 14px;
}
a {
@@ -369,7 +373,7 @@
.build-job {
position: relative;
- .fa-arrow-right {
+ .icon-arrow-right {
position: absolute;
left: 15px;
top: 20px;
@@ -379,7 +383,7 @@
&.active {
font-weight: $gl-font-weight-bold;
- .fa-arrow-right {
+ .icon-arrow-right {
display: block;
}
}
@@ -392,8 +396,7 @@
background-color: $row-hover;
}
- .fa-refresh {
- font-size: 13px;
+ .icon-retry {
margin-left: 3px;
}
}
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index bf6a48889bf..fbe1f3081a0 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -36,7 +36,7 @@
pre.commit-message {
background: none;
padding: 0;
- border: none;
+ border: 0;
margin: 20px 0;
border-radius: 0;
}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 8d6f30e3b84..e5b9e1f2de6 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -2,8 +2,9 @@
.clipboard-addon {
background-color: $white-light;
}
+}
- .alert-block {
- margin-bottom: 10px;
- }
+.cluster-applications-table {
+ // Wait for the Vue to kick-in and render the applications block
+ min-height: 302px;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index ee3ca246374..b1850be8a5f 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,6 +1,6 @@
.commit-description {
background: none;
- border: none;
+ border: 0;
padding: 0;
margin-top: 10px;
word-break: normal;
@@ -247,7 +247,7 @@
word-break: normal;
pre {
- border: none;
+ border: 0;
background: inherit;
padding: 0;
margin: 0;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 2a92673d9fa..292e0ad394b 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -22,6 +22,11 @@
}
}
}
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
}
.col-headers {
@@ -75,7 +80,7 @@
.panel {
.content-block {
padding: 24px 0;
- border-bottom: none;
+ border-bottom: 0;
position: relative;
@media (max-width: $screen-xs-max) {
@@ -155,11 +160,6 @@
}
}
- .landing svg {
- width: 136px;
- height: 136px;
- }
-
.fa-spinner {
font-size: 28px;
position: relative;
@@ -222,11 +222,11 @@
}
&:first-child {
- border-top: none;
+ border-top: 0;
}
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
.stage-nav-item-cell {
@@ -290,7 +290,7 @@
border-bottom: 1px solid $gray-darker;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 3d9eff35583..538e50ee306 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -3,6 +3,7 @@
border-bottom: 1px solid $border-color;
color: $gl-text-color;
line-height: 34px;
+ display: flex;
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 09f831dcb29..848d7f144dc 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -47,7 +47,7 @@
table {
width: 100%;
font-family: $monospace_font;
- border: none;
+ border: 0;
border-collapse: separate;
margin: 0;
padding: 0;
@@ -105,7 +105,7 @@
.new_line {
@include user-select(none);
margin: 0;
- border: none;
+ border: 0;
padding: 0 5px;
border-right: 1px solid;
text-align: right;
@@ -133,7 +133,7 @@
display: block;
margin: 0;
padding: 0 1.5em;
- border: none;
+ border: 0;
position: relative;
&.parallel {
@@ -359,7 +359,7 @@
cursor: pointer;
&:first-child {
- border-left: none;
+ border-left: 0;
}
&:hover {
@@ -380,15 +380,15 @@
}
}
}
+
+ .line_content {
+ white-space: pre-wrap;
+ }
}
.file-content .diff-file {
margin: 0;
- border: none;
-}
-
-.diff-file .line_content {
- white-space: pre-wrap;
+ border: 0;
}
.diff-wrap-lines .line_content {
@@ -400,7 +400,7 @@
}
.files-changed {
- border-bottom: none;
+ border-bottom: 0;
}
.diff-stats-summary-toggler {
@@ -628,21 +628,46 @@
}
.diff-file-changes {
- width: 450px;
+ max-width: 560px;
+ width: 100%;
z-index: 150;
@media (min-width: $screen-sm-min) {
left: $gl-padding;
}
- a {
+ .diff-changed-file {
+ display: flex;
padding-top: 8px;
padding-bottom: 8px;
+ min-width: 0;
}
- .diff-changed-file {
+ .diff-file-changed-icon {
+ margin-top: 2px;
+ }
+
+ .diff-changed-file-content {
display: flex;
- align-items: center;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .diff-changed-file-name,
+ .diff-changed-file-path {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .diff-changed-file-path {
+ direction: rtl;
+ color: $gl-text-color-tertiary;
+ }
+
+ .diff-changed-stats {
+ margin-left: auto;
+ white-space: nowrap;
}
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index edfafa79c44..c586dab4cf2 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -3,13 +3,13 @@
border-top: 1px solid $border-color;
border-right: 1px solid $border-color;
border-left: 1px solid $border-color;
- border-bottom: none;
+ border-bottom: 0;
border-radius: $border-radius-small $border-radius-small 0 0;
background: $gray-normal;
}
#editor {
- border: none;
+ border: 0;
border-radius: 0;
height: 500px;
margin: 0;
@@ -171,7 +171,7 @@
width: 100%;
margin: 5px 0;
padding: 0;
- border-left: none;
+ border-left: 0;
}
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3b5e411e2c5..b0795353ec1 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -117,7 +117,7 @@
}
.no-btn {
- border: none;
+ border: 0;
background: none;
outline: none;
width: 100%;
@@ -133,12 +133,11 @@
}
.folder-row {
- padding: 15px 0;
- border-bottom: 1px solid $white-normal;
+ border-left: 0;
+ border-right: 0;
- @media (max-width: $screen-sm-max) {
- border-top: 1px solid $white-normal;
- margin-top: 10px;
+ @media (min-width: $screen-sm-max) {
+ border-top: 0;
}
}
@@ -174,7 +173,7 @@
.prometheus-graph-overlay {
fill: none;
- opacity: 0.0;
+ opacity: 0;
pointer-events: all;
}
@@ -256,29 +255,6 @@
width: 100%;
padding: 0;
padding-bottom: 100%;
-}
-
-.prometheus-svg-container > svg {
- position: absolute;
- height: 100%;
- width: 100%;
- left: 0;
- top: 0;
-
- text {
- fill: $gl-text-color;
- stroke-width: 0;
- }
-
- .text-metric-bold {
- font-weight: $gl-font-weight-bold;
- }
-
- .label-axis-text {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 10px;
- }
.text-metric-usage,
.legend-metric-title {
@@ -287,42 +263,65 @@
font-size: 12px;
}
- .legend-axis-text {
- fill: $black;
- }
+ > svg {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
- .tick {
- > line {
- stroke: $gray-darker;
+ text {
+ fill: $gl-text-color;
+ stroke-width: 0;
}
- > text {
- font-size: 12px;
+ .text-metric-bold {
+ font-weight: $gl-font-weight-bold;
}
- }
- .text-metric-title {
- font-size: 12px;
- }
+ .label-axis-text {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 10px;
+ }
- .y-label-text,
- .x-label-text {
- fill: $gray-darkest;
- }
+ .legend-axis-text {
+ fill: $black;
+ }
- .axis-tick {
- stroke: $gray-darker;
- }
+ .tick {
+ > line {
+ stroke: $gray-darker;
+ }
- @media (max-width: $screen-sm-max) {
- .label-axis-text,
- .text-metric-usage,
- .legend-axis-text {
- font-size: 8px;
+ > text {
+ font-size: 12px;
+ }
+ }
+
+ .text-metric-title {
+ font-size: 12px;
+ }
+
+ .y-label-text,
+ .x-label-text {
+ fill: $gray-darkest;
+ }
+
+ .axis-tick {
+ stroke: $gray-darker;
}
- .tick > text {
- font-size: 8px;
+ @media (max-width: $screen-sm-max) {
+ .label-axis-text,
+ .text-metric-usage,
+ .legend-axis-text {
+ font-size: 8px;
+ }
+
+ .tick > text {
+ font-size: 8px;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 1723d716805..eea8b7dd193 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -85,7 +85,7 @@
}
pre {
- border: none;
+ border: 0;
background: $gray-light;
border-radius: 0;
color: $events-pre-color;
@@ -128,14 +128,14 @@
}
}
- &:last-child { border: none; }
+ &:last-child { border: 0; }
.event_commits {
li {
&.commit {
background: transparent;
padding: 0;
- border: none;
+ border: 0;
.commit-row-title {
font-size: $gl-font-size;
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index dae8ccdef6c..9cc9e11bcd1 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -1,23 +1,3 @@
-.documentation-index {
- h1 {
- margin: 0;
- }
-
- h2 {
- font-size: 20px;
- }
-
- li {
- line-height: 24px;
- color: $document-index-color;
-
- a {
- margin-right: 3px;
- }
- }
-}
-
-
.shortcut-mappings {
font-size: 12px;
color: $help-shortcut-mapping-color;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 48532503263..7a5dab16561 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -6,28 +6,20 @@
}
.issuable-warning-icon {
- color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default;
- padding: 5px;
margin: 0 $btn-side-margin 0 0;
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
- &:first-of-type {
- margin-right: $issuable-warning-icon-margin;
+ .icon {
+ fill: $orange-600;
+ vertical-align: text-bottom;
}
-}
-.sidebar-item-icon {
- border-radius: $border-radius-default;
- padding: 5px;
- margin: 0 3px 0 -4px;
-
- &.is-active {
- color: $orange-600;
- background-color: $orange-50;
+ &:first-of-type {
+ margin-right: $issuable-warning-icon-margin;
}
}
@@ -79,7 +71,7 @@
.title {
padding: 0;
margin-bottom: 16px;
- border-bottom: none;
+ border-bottom: 0;
}
.btn-edit {
@@ -127,6 +119,15 @@
}
.right-sidebar {
+ position: absolute;
+ top: $header-height;
+ bottom: 0;
+ right: 0;
+ transition: width $right-sidebar-transition-duration;
+ background: $gray-light;
+ z-index: 200;
+ overflow: hidden;
+
a:not(.btn-retry),
.btn-link {
color: inherit;
@@ -155,7 +156,7 @@
}
&:last-child {
- border: none;
+ border: 0;
}
span {
@@ -228,17 +229,6 @@
.btn-clipboard:hover {
color: $gl-text-color;
}
-}
-
-.right-sidebar {
- position: absolute;
- top: $header-height;
- bottom: 0;
- right: 0;
- transition: width $right-sidebar-transition-duration;
- background: $gray-light;
- z-index: 200;
- overflow: hidden;
.issuable-sidebar {
width: calc(100% + 100px);
@@ -340,7 +330,7 @@
.block {
width: $gutter_collapsed_width - 2px;
padding: 15px 0 0;
- border-bottom: none;
+ border-bottom: 0;
overflow: hidden;
}
@@ -401,7 +391,7 @@
}
.btn-clipboard {
- border: none;
+ border: 0;
color: $issuable-sidebar-color;
&:hover {
@@ -542,7 +532,9 @@
}
.participants-list {
- margin: -5px;
+ display: flex;
+ flex-wrap: wrap;
+ margin: -7px;
}
@@ -553,7 +545,7 @@
.participants-author {
display: inline-block;
- padding: 5px;
+ padding: 7px;
&:nth-of-type(7n) {
padding-right: 0;
@@ -613,6 +605,8 @@
float: none;
display: inline-block;
margin-top: 0;
+ height: auto;
+ align-self: center;
@media (max-width: $screen-xs-max) {
position: absolute;
@@ -626,6 +620,8 @@
padding-left: 45px;
padding-right: 45px;
line-height: 35px;
+ display: flex;
+ flex-grow: 1;
@media (min-width: $screen-sm-min) {
float: left;
@@ -637,11 +633,12 @@
.issuable-actions {
@include new-style-dropdown;
- padding-top: 10px;
+ align-self: center;
+ flex-shrink: 0;
+ flex: 0 0 auto;
@media (min-width: $screen-sm-min) {
float: right;
- padding-top: 0;
}
}
@@ -655,8 +652,9 @@
.issuable-meta {
display: inline-block;
- line-height: 18px;
font-size: 14px;
+ line-height: 24px;
+ align-self: center;
}
.js-issuable-selector-wrap {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index e8ca5cedaee..8bb68ad2425 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -134,11 +134,24 @@ ul.related-merge-requests > li {
}
@media (max-width: $screen-xs-max) {
- .issue-btn-group {
- width: 100%;
+ .detail-page-header,
+ .issuable-header {
+ display: block;
+
+ .issuable-meta {
+ line-height: 18px;
+ }
+ }
- .btn {
+ .issuable-actions {
+ margin-top: 10px;
+
+ .issue-btn-group {
width: 100%;
+
+ .btn {
+ width: 100%;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index cf5f933a762..b7985c4dea5 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -109,13 +109,37 @@
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
+ // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
+ // These styles prevent this from breaking the layout, and only applied when providers are configured.
+ &.custom-provider-tabs {
+ flex-wrap: wrap;
+
+ li {
+ min-width: 85px;
+ flex-basis: auto;
+
+ // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
+ // We are making somewhat of an assumption about the configuration here: that users do not have more than
+ // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
+ // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
+ // above one of the bottom row elements. If you know a better way, please implement it!
+ &:nth-child(n+5) {
+ border-top: 1px solid $border-color;
+ }
+ }
+
+ a {
+ font-size: 16px;
+ }
+ }
+
li {
flex: 1;
text-align: center;
border-left: 1px solid $border-color;
&:first-of-type {
- border-left: none;
+ border-left: 0;
border-top-left-radius: $border-radius-default;
}
@@ -141,7 +165,7 @@
border-bottom: 1px solid $border-color;
a {
- border: none;
+ border: 0;
border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black;
@@ -154,32 +178,6 @@
}
}
- // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
- // These styles prevent this from breaking the layout, and only applied when providers are configured.
-
- .new-session-tabs.custom-provider-tabs {
- flex-wrap: wrap;
-
- li {
- min-width: 85px;
- flex-basis: auto;
-
- // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
- // We are making somewhat of an assumption about the configuration here: that users do not have more than
- // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
- // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
- // above one of the bottom row elements. If you know a better way, please implement it!
- &:nth-child(n+5) {
- border-top: 1px solid $border-color;
- }
- }
-
- a {
- font-size: 16px;
- }
- }
-
-
.form-control {
&:active,
&:focus {
@@ -231,35 +229,35 @@
margin: 0;
padding: 0;
height: 100%;
-}
-// Fixes footer container to bottom of viewport
-.devise-layout-html body {
- // offset height of fixed header + 1 to avoid scroll
- height: calc(100% - 51px);
- margin: 0;
- padding: 0;
+ // Fixes footer container to bottom of viewport
+ body {
+ // offset height of fixed header + 1 to avoid scroll
+ height: calc(100% - 51px);
+ margin: 0;
+ padding: 0;
- .page-wrap {
- min-height: 100%;
- position: relative;
- }
+ .page-wrap {
+ min-height: 100%;
+ position: relative;
+ }
- .footer-container,
- hr.footer-fixed {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 40px;
- background: $white-light;
- }
+ .footer-container,
+ hr.footer-fixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 40px;
+ background: $white-light;
+ }
- .navless-container {
- padding: 65px 15px; // height of footer + bottom padding of email confirmation link
+ .navless-container {
+ padding: 65px 15px; // height of footer + bottom padding of email confirmation link
- @media (max-width: $screen-xs-max) {
- padding: 0 15px 65px;
+ @media (max-width: $screen-xs-max) {
+ padding: 0 15px 65px;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 692acf74a58..18c48405ecd 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -49,9 +49,17 @@
width: auto;
}
}
+
+ &.existing-title {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+ }
}
.member-form-control {
+ @include new-style-dropdown;
+
@media (max-width: $screen-xs-max) {
padding-bottom: 5px;
margin-left: 0;
@@ -64,12 +72,6 @@
line-height: 43px;
}
-.member.existing-title {
- @media (min-width: $screen-sm-min) {
- float: left;
- }
-}
-
.member-search-form {
@include new-style-dropdown;
@@ -281,7 +283,3 @@
}
}
}
-
-.member-form-control {
- @include new-style-dropdown;
-}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index dbf3e2b763c..04bde64c752 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -262,7 +262,7 @@ $colors: (
.editor {
pre {
height: 350px;
- border: none;
+ border: 0;
border-radius: 0;
margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index d9fb3b44d29..5832cf4637f 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -150,14 +150,6 @@
display: block;
}
- .mr-widget-body {
- @include clearfix;
-
- &.media > *:first-child {
- margin-right: 10px;
- }
- }
-
.mr-widget-pipeline-graph {
padding: 0 4px;
@@ -209,12 +201,17 @@
}
}
- .mr-widget-help {
- padding: 10px 16px 10px 48px;
- font-style: italic;
- }
-
.mr-widget-body {
+ @include clearfix;
+
+ &.media > *:first-child {
+ margin-right: 10px;
+ }
+
+ .approve-btn {
+ margin-right: 5px;
+ }
+
h4 {
float: left;
font-weight: $gl-font-weight-bold;
@@ -336,6 +333,11 @@
}
}
+ .mr-widget-help {
+ padding: 10px 16px 10px 48px;
+ font-style: italic;
+ }
+
.ci-coverage {
float: right;
}
@@ -350,12 +352,6 @@
}
}
-.mr-state-widget .mr-widget-body {
- .approve-btn {
- margin-right: 5px;
- }
-}
-
.mr-widget-body-controls {
flex-wrap: wrap;
}
@@ -469,16 +465,16 @@
padding-bottom: 0;
}
}
-}
-.mr-info-list.mr-memory-usage {
- p {
- float: left;
- }
+ &.mr-memory-usage {
+ p {
+ float: left;
+ }
- .memory-graph-container {
- float: left;
- margin-left: 5px;
+ .memory-graph-container {
+ float: left;
+ margin-left: 5px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 32039936be7..ae8fa45a2d7 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -66,6 +66,15 @@
height: 6px;
margin: 0;
}
+
+ .sidebar-collapsed-icon {
+ clear: both;
+ padding: 15px 5px 5px;
+
+ .progress {
+ margin: 5px 0;
+ }
+ }
}
.collapsed-milestone-date {
@@ -93,17 +102,6 @@
margin-right: 0;
}
- .milestone-progress {
- .sidebar-collapsed-icon {
- clear: both;
- padding: 15px 5px 5px;
-
- .progress {
- margin: 5px 0;
- }
- }
- }
-
.right-sidebar-collapsed & {
.reference {
border-top: 1px solid $border-gray-normal;
@@ -156,18 +154,16 @@
.status-box {
margin-top: 0;
- }
-
- .milestone-buttons {
- margin-left: auto;
- }
-
- .status-box {
order: 1;
}
.milestone-buttons {
+ margin-left: auto;
order: 2;
+
+ .verbose {
+ display: none;
+ }
}
.header-text-content {
@@ -175,10 +171,6 @@
width: 100%;
}
- .milestone-buttons .verbose {
- display: none;
- }
-
@media (min-width: $screen-xs-min) {
.milestone-buttons .verbose {
display: inline;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index f0cad30f4f3..1e6992cb65e 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -7,7 +7,7 @@
.diff-file .diff-content {
tr.line_holder:hover > td .line_note_link {
- opacity: 1.0;
+ opacity: 1;
filter: alpha(opacity = 100);
}
}
@@ -16,7 +16,7 @@
.discussion {
.new-note {
margin: 0;
- border: none;
+ border: 0;
}
}
@@ -106,36 +106,55 @@
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
- border-bottom: none;
+ border-bottom: 0;
padding: 3px 12px;
margin: auto;
align-items: center;
.icon {
margin-right: $issuable-warning-icon-margin;
+ vertical-align: text-bottom;
+ fill: $orange-600;
+ }
+
+ + .md-area {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ .disabled-comment {
+ border: 0;
+ border-radius: $label-border-radius;
+ padding-top: $gl-vert-padding;
+ padding-bottom: $gl-vert-padding;
+
+ .icon svg {
+ position: relative;
+ top: 2px;
+ margin-right: $btn-xs-side-margin;
+ width: $gl-font-size;
+ height: $gl-font-size;
+ fill: $orange-600;
+ }
}
}
-.disabled-comment .issuable-note-warning {
- border: none;
- border-radius: $label-border-radius;
- padding-top: $gl-vert-padding;
- padding-bottom: $gl-vert-padding;
+.sidebar-item-icon {
+ border-radius: $border-radius-default;
+ margin: 0 3px 0 -4px;
+ vertical-align: middle;
- .icon svg {
- position: relative;
- top: 2px;
- margin-right: $btn-xs-side-margin;
- width: $gl-font-size;
- height: $gl-font-size;
+ &.is-active {
fill: $orange-600;
}
}
-.sidebar-item-value {
- .fa {
- background-color: inherit;
- }
+.sidebar-collapsed-icon .sidebar-item-icon {
+ margin: 0;
+}
+
+.sidebar-item-value .sidebar-item-icon {
+ fill: $theme-gray-700;
}
.sidebar-item-warning-message {
@@ -155,11 +174,6 @@
}
}
-.issuable-note-warning + .md-area {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
-}
-
.discussion-form {
background-color: $white-light;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 312917bd13a..2461b818219 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -312,57 +312,72 @@ ul.notes {
}
}
-.diff-file .notes_holder {
- font-family: $regular_font;
+.diff-file {
+ .is-over {
+ .add-diff-note {
+ display: inline-block;
+ }
+ }
- td {
- border: 1px solid $white-normal;
- border-left: none;
+ // Merge request notes in diffs
+ // Diff is inline
+ .notes_content .note-header .note-headline-light {
+ display: inline-block;
+ position: relative;
+ }
- &.notes_line {
- vertical-align: middle;
- text-align: center;
- padding: 10px 0;
- background: $gray-light;
- color: $text-color;
- }
+ .notes_holder {
+ font-family: $regular_font;
- &.notes_line2 {
- text-align: center;
- padding: 10px 0;
- border-left: 1px solid $note-line2-border !important;
- }
+ td {
+ border: 1px solid $white-normal;
+ border-left: 0;
- &.notes_content {
- background-color: $gray-light;
- border-width: 1px 0;
- padding: 0;
- vertical-align: top;
- white-space: normal;
+ &.notes_line {
+ vertical-align: middle;
+ text-align: center;
+ padding: 10px 0;
+ background: $gray-light;
+ color: $text-color;
+ }
- &.parallel {
- border-width: 1px;
+ &.notes_line2 {
+ text-align: center;
+ padding: 10px 0;
+ border-left: 1px solid $note-line2-border !important;
}
- .discussion-notes {
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: 20px;
+ &.notes_content {
+ background-color: $gray-light;
+ border-width: 1px 0;
+ padding: 0;
+ vertical-align: top;
+ white-space: normal;
+
+ &.parallel {
+ border-width: 1px;
}
- &:not(:last-child) {
- border-bottom: 1px solid $white-normal;
- margin-bottom: 20px;
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
}
- }
- .notes {
- background-color: $white-light;
- }
+ .notes {
+ background-color: $white-light;
+ }
- a code {
- top: 0;
- margin-right: 0;
+ a code {
+ top: 0;
+ margin-right: 0;
+ }
}
}
}
@@ -457,6 +472,11 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
+ @include notes-media('max', $screen-md-max) {
+ float: none;
+ margin-left: 0;
+ }
+
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
@@ -469,8 +489,6 @@ ul.notes {
flex-shrink: 0;
display: inline-flex;
align-items: center;
- // For PhantomJS that does not support flex
- float: right;
margin-left: 10px;
color: $gray-darkest;
@@ -481,7 +499,6 @@ ul.notes {
}
.more-actions {
- float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
@@ -502,13 +519,6 @@ ul.notes {
min-width: 180px;
}
-.discussion-actions {
- @include notes-media('max', $screen-md-max) {
- float: none;
- margin-left: 0;
- }
-}
-
.note-actions-item {
margin-left: 12px;
display: flex;
@@ -537,10 +547,6 @@ ul.notes {
width: 16px;
top: 0;
vertical-align: text-top;
-
- path {
- fill: currentColor;
- }
}
.award-control-icon-positive,
@@ -560,10 +566,6 @@ ul.notes {
.link-highlight {
color: $gl-link-color;
fill: $gl-link-color;
-
- svg {
- fill: $gl-link-color;
- }
}
.award-control-icon-neutral {
@@ -660,15 +662,7 @@ ul.notes {
.timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
- border-bottom: none;
- }
- }
-}
-
-.diff-file {
- .is-over {
- .add-diff-note {
- display: inline-block;
+ border-bottom: 0;
}
}
}
@@ -681,7 +675,7 @@ ul.notes {
padding: 90px 0;
&.discussion-locked {
- border: none;
+ border: 0;
background-color: $white-light;
}
@@ -714,20 +708,20 @@ ul.notes {
svg path {
fill: $gray-darkest;
}
- }
- .btn.discussion-create-issue-btn {
- margin-left: -4px;
- border-radius: 0;
- border-right: 0;
+ &.discussion-create-issue-btn {
+ margin-left: -4px;
+ border-radius: 0;
+ border-right: 0;
- a {
- padding: 0;
- line-height: 0;
+ a {
+ padding: 0;
+ line-height: 0;
- &:hover {
- text-decoration: none;
- border: 0;
+ &:hover {
+ text-decoration: none;
+ border: 0;
+ }
}
}
}
@@ -761,7 +755,7 @@ ul.notes {
top: 0;
padding: 0;
background-color: transparent;
- border: none;
+ border: 0;
outline: 0;
color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
@@ -801,12 +795,3 @@ ul.notes {
.line-resolve-text {
vertical-align: middle;
}
-
-// Merge request notes in diffs
-.diff-file {
- // Diff is inline
- .notes_content .note-header .note-headline-light {
- display: inline-block;
- position: relative;
- }
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8fc7a5eec9b..cb24274c612 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -31,7 +31,6 @@
}
.pipeline-actions {
- padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default {
@@ -176,6 +175,25 @@
}
}
+ /**
+ * Play button with icon in dropdowns
+ */
+ .no-btn {
+ border: 0;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+
+ .icon-play {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ height: 13px;
+ width: 12px;
+ }
+ }
+
.duration,
.finished-at {
color: $gl-text-color-secondary;
@@ -270,7 +288,7 @@
.pipeline-actions {
@include new-style-dropdown;
- border-bottom: none;
+ border-bottom: 0;
}
.tab-pane {
@@ -300,7 +318,7 @@
}
.build-log {
- border: none;
+ border: 0;
line-height: initial;
}
}
@@ -368,13 +386,13 @@
// Remove right connecting horizontal line from first build in last stage
&:first-child {
&::after {
- border: none;
+ border: 0;
}
}
// Remove right curved connectors from all builds in last stage
&:not(:first-child) {
&::after {
- border: none;
+ border: 0;
}
}
// Remove opposite curve
@@ -391,7 +409,7 @@
// Remove left curved connectors from all builds in first stage
&:not(:first-child) {
&::before {
- border: none;
+ border: 0;
}
}
// Remove opposite curve
@@ -451,36 +469,46 @@
@extend .build-content:hover;
}
- // Action Icons in big pipeline-graph nodes
- .ci-action-icon-container .ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
- background: $white-light;
- border: 1px solid $border-color;
- border-radius: 100%;
- display: block;
-
- &:hover {
- background-color: $stage-hover-bg;
- border: 1px solid $dropdown-toggle-active-border-color;
- }
-
- svg {
- fill: $gl-text-color-secondary;
- position: relative;
- left: -1px;
- top: -1px;
- }
-
- &:hover svg {
- fill: $gl-text-color;
- }
- }
-
.ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
+
+ // Action Icons in big pipeline-graph nodes
+ &.ci-action-icon-wrapper {
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ svg {
+ fill: $gl-text-color-secondary;
+ position: relative;
+ left: 5px;
+ top: 2px;
+ width: 18px;
+ height: 18px;
+ }
+
+ &.play {
+ svg {
+ width: #{$ci-action-icon-size - 8};
+ height: #{$ci-action-icon-size - 8};
+ left: 8px;
+ }
+ }
+ }
}
.ci-status-icon svg {
@@ -490,7 +518,7 @@
.dropdown-menu-toggle {
background-color: transparent;
- border: none;
+ border: 0;
padding: 0;
&:focus {
@@ -721,17 +749,50 @@ button.mini-pipeline-graph-dropdown-toggle {
svg {
fill: $gl-text-color-secondary;
- width: $ci-action-icon-size;
- height: $ci-action-icon-size;
- left: -6px;
+ width: #{$ci-action-icon-size - 6};
+ height: #{$ci-action-icon-size - 6};
+ left: -3px;
position: relative;
- top: -3px;
+ top: -2px;
+
+ &.icon-action-stop,
+ &.icon-action-cancel {
+ width: 12px;
+ height: 12px;
+ top: 1px;
+ left: -1px;
+ }
+
+ &.icon-action-play {
+ width: 11px;
+ height: 11px;
+ top: 1px;
+ left: 1px;
+ }
+
+ &.icon-action-retry {
+ width: 16px;
+ height: 16px;
+ top: 0;
+ left: -3px;
+ }
}
&:hover svg,
&:focus svg {
fill: $gl-text-color;
}
+
+ &.icon-action-retry,
+ &.icon-action-play {
+ svg {
+ width: #{$ci-action-icon-size - 6};
+ height: #{$ci-action-icon-size - 6};
+ left: 8px;
+ }
+ }
+
+
}
// link to the build
@@ -762,6 +823,11 @@ button.mini-pipeline-graph-dropdown-toggle {
margin-left: 2px;
display: inline-block;
+ &::after {
+ content: '';
+ display: block;
+ }
+
@media (max-width: $screen-xs-max) {
max-width: 60%;
}
@@ -799,13 +865,10 @@ button.mini-pipeline-graph-dropdown-toggle {
left: 100%;
top: -10px;
box-shadow: 0 1px 5px $black-transparent;
-}
-
-/**
- * Top arrow in the dropdown in the big pipeline graph
- */
-.big-pipeline-graph-dropdown-menu {
+ /**
+ * Top arrow in the dropdown in the big pipeline graph
+ */
&::before,
&::after {
content: '';
@@ -867,22 +930,23 @@ button.mini-pipeline-graph-dropdown-toggle {
margin-top: 1px;
border-bottom-color: $white-light;
}
-}
-/**
- * Center dropdown menu in mini graph
- */
-.mini-pipeline-graph-dropdown-menu.dropdown-menu {
- transform: translate(-80%, 0);
- min-width: 150px;
+ /**
+ * Center dropdown menu in mini graph
+ */
+ &.dropdown-menu {
+ transform: translate(-80%, 0);
+ min-width: 150px;
- @media(min-width: $screen-md-min) {
- transform: translate(-50%, 0);
- right: auto;
- left: 50%;
- min-width: 240px;
+ @media(min-width: $screen-md-min) {
+ transform: translate(-50%, 0);
+ right: auto;
+ left: 50%;
+ min-width: 240px;
+ }
}
}
+
/**
* Terminal
*/
@@ -892,7 +956,7 @@ button.mini-pipeline-graph-dropdown-toggle {
.terminal-container {
.content-block {
- border-bottom: none;
+ border-bottom: 0;
}
#terminal {
@@ -906,25 +970,6 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
-/**
- * Play button with icon in dropdowns
- */
-.ci-table .no-btn {
- border: none;
- background: none;
- outline: none;
- width: 100%;
- text-align: left;
-
- .icon-play {
- position: relative;
- top: 2px;
- margin-right: 5px;
- height: 13px;
- width: 12px;
- }
-}
-
.ci-header-container {
min-height: 55px;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index eab39f698c3..28dc71dc641 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -113,7 +113,7 @@
li {
padding: 3px 0;
- border: none;
+ border: 0;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bd385db9692..aaad6dbba8e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -80,7 +80,7 @@
.project-feature-settings {
background: $gray-lighter;
- border-top: none;
+ border-top: 0;
margin-bottom: 16px;
}
@@ -88,7 +88,8 @@
transition: background 2s ease-out;
&:disabled {
- opacity: 0.75;
+ opacity: 0.5;
+ pointer-events: none;
}
.highlight-changes & {
@@ -127,7 +128,7 @@
.project-feature-toggle {
position: relative;
- border: none;
+ border: 0;
outline: 0;
display: block;
width: 100px;
@@ -482,7 +483,7 @@ a.deploy-project-label {
flex: 1;
padding: 0;
background: transparent;
- border: none;
+ border: 0;
line-height: 34px;
margin: 0;
@@ -778,35 +779,35 @@ a.deploy-project-label {
.nav {
padding-top: 12px;
padding-bottom: 12px;
- }
- .nav > li {
- display: inline-block;
+ > li {
+ display: inline-block;
- &:not(:last-child) {
- margin-right: $gl-padding;
- }
+ &:not(:last-child) {
+ margin-right: $gl-padding;
+ }
- &.right {
- vertical-align: top;
- margin-top: 0;
+ &.right {
+ vertical-align: top;
+ margin-top: 0;
- @media (min-width: $screen-lg-min) {
- float: right;
+ @media (min-width: $screen-lg-min) {
+ float: right;
+ }
}
- }
- }
- .nav > li > a {
- padding: 0;
- background-color: transparent;
- font-size: 14px;
- line-height: 29px;
- color: $notes-light-color;
+ > a {
+ padding: 0;
+ background-color: transparent;
+ font-size: 14px;
+ line-height: 29px;
+ color: $notes-light-color;
- &:hover,
- &:focus {
- color: $gl-text-color;
+ &:hover,
+ &:focus {
+ color: $gl-text-color;
+ }
+ }
}
}
@@ -1011,7 +1012,7 @@ pre.light-well {
margin: 0;
border-radius: 0 0 1px 1px;
padding: 20px 0;
- border: none;
+ border: 0;
}
.table-bordered {
@@ -1160,18 +1161,11 @@ pre.light-well {
}
}
-.project-repo-select {
- &.disabled {
- opacity: 0.5;
- pointer-events: none;
- }
-}
-
.variables-table {
table-layout: fixed;
&.table-responsive {
- border: none;
+ border: 0;
}
.variable-key {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index e8c7f8a8fc0..d93c51d5448 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1,10 +1,14 @@
-.monaco-loader {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: $black-transparent;
+.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,
@@ -41,6 +45,7 @@
}
.tree-content-holder {
+ display: -webkit-flex;
display: flex;
min-height: 300px;
}
@@ -50,14 +55,16 @@
}
.panel-right {
+ display: -webkit-flex;
display: flex;
+ -webkit-flex-direction: column;
flex-direction: column;
width: 80%;
height: 100%;
.monaco-editor.vs {
.current-line {
- border: none;
+ border: 0;
background: $well-light-border;
}
@@ -68,10 +75,6 @@
text-decoration: underline;
}
}
-
- .cursor {
- display: none !important;
- }
}
.blob-no-preview {
@@ -81,21 +84,12 @@
}
}
- &.edit-mode {
- .blob-viewer-container {
- overflow: hidden;
- }
-
- .monaco-editor.vs {
- .cursor {
- background: $black;
- border-color: $black;
- display: block !important;
- }
- }
+ &.blob-editor-container {
+ overflow: hidden;
}
.blob-viewer-container {
+ -webkit-flex: 1;
flex: 1;
overflow: auto;
@@ -125,6 +119,7 @@
}
#tabs {
+ position: relative;
flex-shrink: 0;
display: flex;
width: 100%;
@@ -144,7 +139,7 @@
&.active {
background: $white-light;
- border-bottom: none;
+ border-bottom: 0;
}
a {
@@ -153,6 +148,10 @@
vertical-align: middle;
text-decoration: none;
margin-right: 12px;
+
+ &:focus {
+ outline: none;
+ }
}
.close-btn {
@@ -182,7 +181,7 @@
&.tabs-divider {
width: 100%;
background-color: $white-light;
- border-right: none;
+ border-right: 0;
border-top-right-radius: 2px;
}
}
@@ -300,22 +299,6 @@
}
}
-@keyframes swipeRightAppear {
- 0% {
- transform: scaleX(0.00);
- }
-
- 100% {
- transform: scaleX(1.00);
- }
-}
-
-@keyframes swipeRightDissapear {
- 0% {
- transform: scaleX(1.00);
- }
-
- 100% {
- transform: scaleX(0.00);
- }
+.multi-file-table-col-name {
+ width: 350px;
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 6cac37a4e28..5fb97b13470 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -50,3 +50,10 @@
font-size: 11px;
}
}
+
+@media (max-width: $screen-md-max) {
+ .runners-content {
+ width: 100%;
+ overflow: auto;
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index db0a04a5eb3..fe455a04960 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -5,7 +5,7 @@
margin-bottom: $gl-padding;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
}
@@ -57,7 +57,7 @@ input[type="checkbox"]:hover {
}
.search-input {
- border: none;
+ border: 0;
font-size: 14px;
padding: 0 20px 0 0;
margin-left: 5px;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 41a6ba2023a..5d630c7d61e 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -23,15 +23,14 @@
}
.settings {
- overflow: hidden;
border-bottom: 1px solid $gray-darker;
&:first-of-type {
margin-top: 10px;
}
- &.expanded {
- overflow: visible;
+ &.animating {
+ overflow: hidden;
}
}
@@ -56,14 +55,18 @@
overflow-y: scroll;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
+ // Keep the section from expanding when we scroll over it
+ pointer-events: none;
- &.expanded {
+ .settings.expanded & {
max-height: none;
overflow-y: visible;
animation: expandMaxHeight 300ms ease-in;
+ // Reset and allow clicks again when expanded
+ pointer-events: auto;
}
- &.no-animate {
+ .settings.no-animate & {
animation: none;
}
@@ -238,11 +241,30 @@
margin-left: 5px;
background: $badge-bg;
}
- }
- /* Ensure we don't add border if there's only single li */
- li + li {
- border-top: 1px solid $border-color;
+ /* Ensure we don't add border if there's only single li */
+ + li {
+ border-top: 1px solid $border-color;
+ }
}
}
}
+
+.modal-doorkeepr-auth,
+.doorkeeper-app-form {
+ .scope-description {
+ color: $theme-gray-700;
+ }
+}
+
+.modal-doorkeepr-auth {
+ .modal-body {
+ padding: $gl-padding;
+ }
+}
+
+.doorkeeper-app-form {
+ .scope-description {
+ margin: 0 0 5px 17px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index bfe065dbbaf..2bf0bedb1f5 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -5,10 +5,10 @@ table .sherlock-code {
.sherlock-code {
pre {
word-wrap: normal;
- }
- pre code {
- white-space: pre;
+ code {
+ white-space: pre;
+ }
}
}
@@ -21,13 +21,13 @@ table .sherlock-code {
text-align: right;
padding: 0 10px !important;
}
+
+ .slow {
+ color: $red-500;
+ font-weight: $gl-font-weight-bold;
+ }
}
.sherlock-file-sample pre {
padding-top: 28px !important;
}
-
-.sherlock-line-samples-table .slow {
- color: $red-500;
- font-weight: $gl-font-weight-bold;
-}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index dfa4d033fb8..cede147d559 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -40,16 +40,16 @@
@media (max-width: $screen-xs-max) {
width: 100%;
}
- }
- .person .spark {
- display: block;
- background: $stat-graph-common-bg;
- width: 100%;
- }
+ .spark {
+ display: block;
+ background: $stat-graph-common-bg;
+ width: 100%;
+ }
- .person .area-contributor {
- fill: $stat-graph-orange-fill;
+ .area-contributor {
+ fill: $stat-graph-orange-fill;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 25c80e1f950..ade5ddd147b 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -55,10 +55,6 @@
&:not(span):hover {
background-color: rgba($gl-text-color-secondary, .07);
}
-
- svg {
- fill: $gl-text-color-secondary;
- }
}
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 6c8d87185e9..2139a029fc7 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -141,7 +141,7 @@
}
pre {
- border: none;
+ border: 0;
background: $gray-light;
border-radius: 0;
color: $todo-body-pre-color;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index e2f6e511c86..65b334662c2 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -125,7 +125,7 @@
color: $white-normal;
}
- &:hover {
+ &:hover:not(.tree-truncated-warning) {
td {
background-color: $row-hover;
border-top: 1px solid $row-hover-border;
@@ -198,6 +198,11 @@
}
}
+ .tree-truncated-warning {
+ color: $orange-600;
+ background-color: $orange-100;
+ }
+
.tree-time-ago {
min-width: 135px;
color: $gl-text-color-secondary;
@@ -252,7 +257,7 @@
margin-top: 20px;
padding: 0;
border-top: 1px solid $white-dark;
- border-bottom: none;
+ border-bottom: 0;
}
.commit-stats li {
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index b7d4e7bf582..e150f96f3fa 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -161,10 +161,10 @@ ul.wiki-pages-list.content-list {
list-style: none;
margin-left: 0;
padding-left: 15px;
- }
- ul li {
- padding: 5px 0;
+ li {
+ padding: 5px 0;
+ }
}
}
diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss
index 06733b7f1a9..e65b49c36f3 100644
--- a/app/assets/stylesheets/test.scss
+++ b/app/assets/stylesheets/test.scss
@@ -4,11 +4,6 @@
-ms-transition: none !important;
-webkit-transition: none !important;
transition: none !important;
- -o-transform: none !important;
- -moz-transform: none !important;
- -ms-transform: none !important;
- -webkit-transform: none !important;
- transform: none !important;
-webkit-animation: none !important;
-moz-animation: none !important;
-o-animation: none !important;
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index fb6d8c0bb81..5be23c76a95 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -19,10 +19,12 @@ class Admin::ApplicationsController < Admin::ApplicationController
end
def create
- @application = Doorkeeper::Application.new(application_params)
+ @application = Applications::CreateService.new(current_user, application_params).execute(request)
- if @application.save
- redirect_to_admin_page
+ if @application.persisted?
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+
+ redirect_to admin_application_url(@application)
else
render :new
end
@@ -41,13 +43,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
end
- protected
-
- def redirect_to_admin_page
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to admin_application_url(@application)
- end
-
private
def set_application
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index 07c8bf714fc..7a2c7234a1e 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth::API_SCOPES
+ @scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 391a0519195..b2ec491146f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
- before_action :authenticate_user_from_private_token!
- before_action :authenticate_user_from_rss_token!
+ before_action :authenticate_sessionless_user!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :check_password_expiration
@@ -97,31 +96,15 @@ class ApplicationController < ActionController::Base
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
+
return try(:authenticated_user)
end
- # This filter handles both private tokens and personal access tokens
- def authenticate_user_from_private_token!
- token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
-
- return unless token.present?
-
- user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
+ # This filter handles personal access tokens, and atom requests with rss tokens
+ def authenticate_sessionless_user!
+ user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
- sessionless_sign_in(user)
- end
-
- # This filter handles authentication for atom request with an rss_token
- def authenticate_user_from_rss_token!
- return unless request.format.atom?
-
- token = params[:rss_token].presence
-
- return unless token.present?
-
- user = User.find_by_rss_token(token)
-
- sessionless_sign_in(user)
+ sessionless_sign_in(user) if user
end
def log_exception(exception)
@@ -213,7 +196,11 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
- if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
+ return if session[:impersonator_id] || current_user&.ldap_user?
+
+ password_expires_at = current_user&.password_expires_at
+
+ if password_expires_at && password_expires_at < Time.now
return redirect_to new_profile_password_path
end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 10e8e54f402..cde1e284d2d 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -44,6 +44,7 @@ class AutocompleteController < ApplicationController
if @project.blank? && params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
+
group
end
end
@@ -54,6 +55,7 @@ class AutocompleteController < ApplicationController
if params[:project_id].present?
project = Project.find(params[:project_id])
return render_404 unless can?(current_user, :read_project, project)
+
project
end
end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 737656b3dcc..f8049b20b9f 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -84,6 +84,7 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
+ sidebar_endpoints: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 4079072a930..072dffaff7a 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,14 +7,59 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update
end
+ def show
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: serializer.represent(issuable, serializer: params[:serializer])
+ end
+ end
+ end
+
+ def update
+ @issuable = update_service.execute(issuable)
+
+ respond_to do |format|
+ format.html do
+ recaptcha_check_with_fallback { render :edit }
+ end
+
+ format.json do
+ render_entity_json
+ end
+ end
+
+ rescue ActiveRecord::StaleObjectError
+ render_conflict_response
+ end
+
+ def realtime_changes
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
+ response = {
+ title: view_context.markdown_field(issuable, :title),
+ title_text: issuable.title,
+ description: view_context.markdown_field(issuable, :description),
+ description_text: issuable.description,
+ task_status: issuable.task_status
+ }
+
+ if issuable.edited?
+ response[:updated_at] = issuable.updated_at
+ response[:updated_by_name] = issuable.last_edited_by.name
+ response[:updated_by_path] = user_path(issuable.last_edited_by)
+ end
+
+ render json: response
+ end
+
def destroy
issuable.destroy
- destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
- TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend
+ TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
- index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
+ index_path = polymorphic_path([parent, issuable.class])
respond_to do |format|
format.html { redirect_to index_path }
@@ -68,6 +113,10 @@ module IssuableActions
end
end
+ def authorize_update_issuable!
+ render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
+ end
+
def bulk_update_params
permitted_keys = [
:issuable_ids,
@@ -92,4 +141,24 @@ module IssuableActions
def resource_name
@resource_name ||= controller_name.singularize
end
+
+ def render_entity_json
+ if @issuable.valid?
+ render json: serializer.represent(@issuable)
+ else
+ render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
+ def serializer
+ raise NotImplementedError
+ end
+
+ def update_service
+ raise NotImplementedError
+ end
+
+ def parent
+ @project || @group
+ end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 3181f517087..2b011bc87b0 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -4,58 +4,44 @@ module IssuableCollections
include Gitlab::IssuableMetadata
included do
- helper_method :issues_finder
- helper_method :merge_requests_finder
+ helper_method :finder
end
private
- def set_issues_index
- @collection_type = "Issue"
- @issues = issues_collection
- @issues = @issues.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
- @total_pages = issues_page_count(@issues)
+ def set_issuables_index
+ @issuables = issuables_collection
+ @issuables = @issuables.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@issuables, collection_type)
+ @total_pages = issuable_page_count
- return if redirect_out_of_range(@issues, @total_pages)
+ return if redirect_out_of_range(@total_pages)
if params[:label_name].present?
- @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
+ labels_params = { project_id: @project.id, title: params[:label_name] }
+ @labels = LabelsFinder.new(current_user, labels_params).execute
end
@users = []
- end
-
- def issues_collection
- issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
- end
-
- def merge_requests_collection
- merge_requests_finder.execute.preload(
- :source_project,
- :target_project,
- :author,
- :assignee,
- :labels,
- :milestone,
- head_pipeline: :project,
- target_project: :namespace,
- merge_request_diff: :merge_request_diff_commits
- )
- end
+ if params[:assignee_id].present?
+ assignee = User.find_by_id(params[:assignee_id])
+ @users.push(assignee) if assignee
+ end
- def issues_finder
- @issues_finder ||= issuable_finder_for(IssuesFinder)
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users.push(author) if author
+ end
end
- def merge_requests_finder
- @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
+ def issuables_collection
+ finder.execute.preload(preload_for_collection)
end
- def redirect_out_of_range(relation, total_pages)
+ def redirect_out_of_range(total_pages)
return false if total_pages.zero?
- out_of_range = relation.current_page > total_pages
+ out_of_range = @issuables.current_page > total_pages
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
@@ -64,12 +50,8 @@ module IssuableCollections
out_of_range
end
- def issues_page_count(relation)
- page_count_for_relation(relation, issues_finder.row_count)
- end
-
- def merge_requests_page_count(relation)
- page_count_for_relation(relation, merge_requests_finder.row_count)
+ def issuable_page_count
+ page_count_for_relation(@issuables, finder.row_count)
end
def page_count_for_relation(relation, row_count)
@@ -145,4 +127,31 @@ module IssuableCollections
else value
end
end
+
+ def finder
+ return @finder if defined?(@finder)
+
+ @finder = issuable_finder_for(@finder_type)
+ end
+
+ def collection_type
+ @collection_type ||= case finder
+ when IssuesFinder
+ 'Issue'
+ when MergeRequestsFinder
+ 'MergeRequest'
+ end
+ end
+
+ def preload_for_collection
+ @preload_for_collection ||= case collection_type
+ when 'Issue'
+ [:project, :author, :assignees, :labels, :milestone, project: :namespace]
+ when 'MergeRequest'
+ [
+ :source_project, :target_project, :author, :assignee, :labels, :milestone,
+ head_pipeline: :project, target_project: :namespace, merge_request_diff: :merge_request_diff_commits
+ ]
+ end
+ end
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 404559c8707..ad594903331 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -3,14 +3,14 @@ module IssuesAction
include IssuableCollections
def issues
- @label = issues_finder.labels.first
+ @finder_type = IssuesFinder
+ @label = finder.labels.first
- @issues = issues_collection
+ @issues = issuables_collection
.non_archived
.page(params[:page])
- @collection_type = "Issue"
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
+ @issuable_meta_data = issuable_meta_data(@issues, collection_type)
respond_to do |format|
format.html
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 2b6afaa6233..4311f9d4db9 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -74,8 +74,9 @@ module LfsRequest
def lfs_upload_access?
return false unless project.lfs_enabled?
+ return false unless has_authentication_ability?(:push_code)
- has_authentication_ability?(:push_code) && can?(user, :push_code, project)
+ lfs_deploy_token? || can?(user, :push_code, project)
end
def lfs_deploy_token?
@@ -91,16 +92,7 @@ module LfsRequest
end
def storage_project
- @storage_project ||= begin
- result = project
-
- loop do
- break unless result.forked?
- result = result.forked_from_project
- end
-
- result
- end
+ @storage_project ||= project.lfs_storage_project
end
def objects
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index d3c8e4888bc..8b569a01afd 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -3,13 +3,12 @@ module MergeRequestsAction
include IssuableCollections
def merge_requests
- @label = merge_requests_finder.labels.first
+ @finder_type = MergeRequestsFinder
+ @label = finder.labels.first
- @merge_requests = merge_requests_collection
- .page(params[:page])
+ @merge_requests = issuables_collection.page(params[:page])
- @collection_type = "MergeRequest"
- @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
end
private
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 1126f706393..be2e1b47feb 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -4,6 +4,7 @@ module NotesActions
included do
before_action :set_polling_interval_header, only: [:index]
+ before_action :require_noteable!, only: [:index, :create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
end
@@ -38,7 +39,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
- Banzai::NoteRenderer.render([@note], @project, current_user)
+ Notes::RenderService.new(current_user).execute([@note], @project)
end
respond_to do |format|
@@ -51,7 +52,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
- Banzai::NoteRenderer.render([@note], @project, current_user)
+ Notes::RenderService.new(current_user).execute([@note], @project)
end
respond_to do |format|
@@ -89,7 +90,7 @@ module NotesActions
if note.persisted?
attrs[:valid] = true
- if noteable.nil? || noteable.discussions_rendered_on_frontend?
+ if noteable.discussions_rendered_on_frontend?
attrs.merge!(note_serializer.represent(note))
else
attrs.merge!(
@@ -108,6 +109,8 @@ module NotesActions
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
+
+ attrs[:discussion_line_code] = discussion.line_code if discussion.diff_discussion?
end
end
else
@@ -188,7 +191,11 @@ module NotesActions
end
def noteable
- @noteable ||= notes_finder.target
+ @noteable ||= notes_finder.target || @note&.noteable
+ end
+
+ def require_noteable!
+ render_404 unless noteable
end
def last_fetched_at
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index 4791bc561a4..824ad06465c 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -3,7 +3,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
- Banzai::NoteRenderer.render(notes, @project, current_user)
+ Notes::RenderService.new(current_user).execute(notes, @project)
notes
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index cd94a36a6e7..d9884a47ec4 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -57,5 +57,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 02c5857eea7..e89eaf7edda 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -76,7 +76,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def redirect_out_of_range(todos)
total_pages =
if todo_params.except(:sort, :page).empty?
- (current_user.todos_pending_count / todos.limit_value).ceil
+ (current_user.todos_pending_count.to_f / todos.limit_value).ceil
else
todos.total_pages
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 19a5db6fd17..280ed93faf8 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -32,6 +32,8 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: @event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events)
end
def set_show_full_reference
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index bc3e95f1aed..eb53a522f90 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -155,6 +155,8 @@ class GroupsController < Groups::ApplicationController
@events = EventCollection
.new(@projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def user_actions
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index ab18d86dcae..b8ba7921613 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -43,7 +43,7 @@ class Import::GithubController < Import::BaseController
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if can?(current_user, :create_projects, @target_namespace)
- @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
+ @project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
else
render 'unauthorized'
end
@@ -52,7 +52,7 @@ class Import::GithubController < Import::BaseController
private
def client
- @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
+ @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
end
def verify_import_enabled
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 510813846a4..567957ba2cb 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -4,6 +4,7 @@ class Import::GitlabProjectsController < Import::BaseController
def new
@namespace = Namespace.find(project_params[:namespace_id])
return render_404 unless current_user.can?(:create_projects, @namespace)
+
@path = project_params[:path]
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 4bceb1d67a3..7d6fe6a0232 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -30,11 +30,11 @@ class JwtController < ApplicationController
render_unauthorized
end
end
- rescue Gitlab::Auth::MissingPersonalTokenError
- render_missing_personal_token
+ rescue Gitlab::Auth::MissingPersonalAccessTokenError
+ render_missing_personal_access_token
end
- def render_missing_personal_token
+ def render_missing_personal_access_token
render json: {
errors: [
{ code: 'UNAUTHORIZED',
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 37587a52eaf..d81ad135198 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -3,10 +3,16 @@ class MetricsController < ActionController::Base
protect_from_forgery with: :exception
- before_action :validate_prometheus_metrics
-
def index
- render text: metrics_service.metrics_text, content_type: 'text/plain; version=0.0.4'
+ response = if Gitlab::Metrics.prometheus_metrics_enabled?
+ metrics_service.metrics_text
+ else
+ help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics',
+ anchor: 'gitlab-prometheus-metrics'
+ )
+ "# Metrics are disabled, see: #{help_page}\n"
+ end
+ render text: response, content_type: 'text/plain; version=0.0.4'
end
private
@@ -14,8 +20,4 @@ class MetricsController < ActionController::Base
def metrics_service
@metrics_service ||= MetricsService.new
end
-
- def validate_prometheus_metrics
- render_404 unless Gitlab::Metrics.prometheus_metrics_enabled?
- end
end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index b02e64a132b..2443f529c7b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -16,25 +16,18 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
end
def create
- @application = Doorkeeper::Application.new(application_params)
+ @application = Applications::CreateService.new(current_user, create_application_params).execute(request)
- @application.owner = current_user
+ if @application.persisted?
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- if @application.save
- redirect_to_oauth_application_page
+ redirect_to oauth_application_url(@application)
else
set_index_vars
render :index
end
end
- protected
-
- def redirect_to_oauth_application_page
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to oauth_application_url(@application)
- end
-
private
def verify_user_oauth_applications_enabled
@@ -61,4 +54,10 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
rescue_from ActiveRecord::RecordNotFound do |exception|
render "errors/not_found", layout: "errors", status: 404
end
+
+ def create_application_params
+ application_params.tap do |params|
+ params[:owner] = current_user
+ end
+ end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 9612b8d8514..56baa19f864 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -54,7 +54,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
- identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
+ identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
@@ -98,7 +98,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_omniauth
if current_user
# Add new authentication method
- current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
+ current_user.identities
+ .with_extern_uid(oauth['provider'], oauth['uid'])
+ .first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 069e6a810f2..f0e5d2aa94e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -11,10 +11,10 @@ class Profiles::KeysController < Profiles::ApplicationController
end
def create
- @key = Keys::CreateService.new(current_user, key_params).execute
+ @key = Keys::CreateService.new(current_user, key_params.merge(ip_address: request.remote_ip)).execute
if @key.persisted?
- redirect_to_profile_key_path
+ redirect_to profile_key_path(@key)
else
@keys = current_user.keys.select(&:persisted?)
render :index
@@ -50,12 +50,6 @@ class Profiles::KeysController < Profiles::ApplicationController
end
end
- protected
-
- def redirect_to_profile_key_path
- redirect_to profile_key_path(@key)
- end
-
private
def key_params
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 4146deefa89..6d9873e38df 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth.available_scopes
+ @scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 5d87037f012..dbf61a17724 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController
end
end
- def reset_private_token
- Users::UpdateService.new(current_user, user: @user).execute! do |user|
- user.reset_authentication_token!
- end
-
- flash[:notice] = "Private token was successfully reset"
-
- redirect_to profile_account_path
- end
-
def reset_incoming_email_token
Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token!
@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "Incoming email token was successfully reset"
- redirect_to profile_account_path
+ redirect_to profile_personal_access_tokens_path
end
def reset_rss_token
@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "RSS token was successfully reset"
- redirect_to profile_account_path
+ redirect_to profile_personal_access_tokens_path
end
def audit_log
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
new file mode 100644
index 00000000000..90c7fa62216
--- /dev/null
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -0,0 +1,25 @@
+class Projects::Clusters::ApplicationsController < Projects::ApplicationController
+ before_action :cluster
+ before_action :application_class, only: [:create]
+ before_action :authorize_read_cluster!
+ before_action :authorize_create_cluster!, only: [:create]
+
+ def create
+ Clusters::Applications::ScheduleInstallationService.new(project, current_user,
+ application_class: @application_class,
+ cluster: @cluster).execute
+ head :no_content
+ rescue StandardError
+ head :bad_request
+ end
+
+ private
+
+ def cluster
+ @cluster ||= project.clusters.find(params[:id]) || render_404
+ end
+
+ def application_class
+ @application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404
+ end
+end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 03019b0becc..9a56c9de858 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,8 +1,8 @@
class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:login, :index, :new, :create]
+ before_action :cluster, except: [:login, :index, :new, :new_gcp, :create]
before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:new, :create]
- before_action :authorize_google_api, only: [:new, :create]
+ before_action :authorize_create_cluster!, only: [:new, :new_gcp, :create]
+ before_action :authorize_google_api, only: [:new_gcp, :create]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
@@ -16,7 +16,7 @@ class Projects::ClustersController < Projects::ApplicationController
def login
begin
- state = generate_session_key_redirect(namespace_project_clusters_url.to_s)
+ state = generate_session_key_redirect(providers_gcp_new_namespace_project_clusters_url.to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
@@ -27,18 +27,23 @@ class Projects::ClustersController < Projects::ApplicationController
end
def new
- @cluster = project.build_cluster
+ end
+
+ def new_gcp
+ @cluster = Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
end
def create
- @cluster = Ci::CreateClusterService
+ @cluster = Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster)
else
- render :new
+ render :new_gcp
end
end
@@ -58,7 +63,7 @@ class Projects::ClustersController < Projects::ApplicationController
end
def update
- Ci::UpdateClusterService
+ Clusters::UpdateService
.new(project, current_user, update_params)
.execute(cluster)
@@ -88,19 +93,19 @@ class Projects::ClustersController < Projects::ApplicationController
def create_params
params.require(:cluster).permit(
- :gcp_project_id,
- :gcp_cluster_zone,
- :gcp_cluster_name,
- :gcp_cluster_size,
- :gcp_machine_type,
- :project_namespace,
- :enabled)
+ :enabled,
+ :name,
+ :provider_type,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ])
end
def update_params
- params.require(:cluster).permit(
- :project_namespace,
- :enabled)
+ params.require(:cluster).permit(:enabled)
end
def authorize_google_api
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index a62f05db7db..6ff96a3f295 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -16,16 +16,13 @@ class Projects::CommitController < Projects::ApplicationController
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
+ BRANCH_SEARCH_LIMIT = 1000
+
def show
apply_diff_view_cookie!
respond_to do |format|
- format.html do
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render
- end
- end
+ format.html { render }
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
end
@@ -56,8 +53,14 @@ class Projects::CommitController < Projects::ApplicationController
end
def branches
- @branches = @project.repository.branch_names_contains(commit.id)
- @tags = @project.repository.tag_names_contains(commit.id)
+ # branch_names_contains/tag_names_contains can take a long time when there are thousands of
+ # branches/tags - each `git branch --contains xxx` request can consume a cpu core.
+ # so only do the query when there are a manageable number of branches/tags
+ @branches_limit_exceeded = @project.repository.branch_count > BRANCH_SEARCH_LIMIT
+ @branches = @branches_limit_exceeded ? [] : @project.repository.branch_names_contains(commit.id)
+
+ @tags_limit_exceeded = @project.repository.tag_count > BRANCH_SEARCH_LIMIT
+ @tags = @tags_limit_exceeded ? [] : @project.repository.tag_names_contains(commit.id)
render layout: false
end
@@ -104,7 +107,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def commit
- @noteable = @commit ||= @project.commit(params[:id])
+ @noteable = @commit ||= @project.commit_by(oid: params[:id])
end
def define_commit_vars
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index d48284a4429..5f4afd2cdee 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -10,9 +10,6 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :set_commits
def show
- @note_counts = project.notes.where(commit_id: @commits.map(&:id))
- .group(:commit_id).count
-
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
@@ -60,6 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
+ @commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits)
end
end
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 47c312ffddf..1a418d0f15a 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -12,6 +12,7 @@ class Projects::DeploymentsController < Projects::ApplicationController
def metrics
return render_404 unless deployment.has_metrics?
+
@metrics = deployment.metrics
if @metrics&.any?
render json: @metrics, status: :ok
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 95d7a02e9e9..dd5e66f60e3 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401
- rescue Gitlab::Auth::MissingPersonalTokenError
- render_missing_personal_token
+ rescue Gitlab::Auth::MissingPersonalAccessTokenError
+ render_missing_personal_access_token
end
def basic_auth_provided?
@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end
- def render_missing_personal_token
+ def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index f59200d3b1f..f58ee3e9109 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -13,11 +13,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group
return render_404 unless can?(current_user, :read_group, group)
- project.project_group_links.create(
- group: group,
- group_access: params[:link_group_access],
- expires_at: params[:expires_at]
- )
+ Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
else
flash[:alert] = 'Please select a group.'
end
@@ -32,7 +28,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
end
def destroy
- project.project_group_links.find(params[:id]).destroy
+ group_link = project.project_group_links.find(params[:id])
+
+ ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
respond_to do |format|
format.html do
@@ -47,4 +45,8 @@ class Projects::GroupLinksController < Projects::ApplicationController
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
+
+ def group_link_create_params
+ params.permit(:link_group_access, :expires_at)
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index fe1334c0cfe..28fee0465d5 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,13 +10,13 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
- before_action :set_issues_index, only: [:index]
+ before_action :set_issuables_index, only: [:index]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
- before_action :authorize_update_issue!, only: [:edit, :update, :move]
+ before_action :authorize_update_issuable!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request]
@@ -24,15 +24,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- if params[:assignee_id].present?
- assignee = User.find_by_id(params[:assignee_id])
- @users.push(assignee) if assignee
- end
-
- if params[:author_id].present?
- author = User.find_by_id(params[:author_id])
- @users.push(author) if author
- end
+ @issues = @issuables
respond_to do |format|
format.html
@@ -67,18 +59,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
- def show
- @noteable = @issue
- @note = @project.notes.new(noteable: @issue)
-
- respond_to do |format|
- format.html
- format.json do
- render json: serializer.represent(@issue)
- end
- end
- end
-
def discussions
notes = @issue.notes
.inc_relations_for_view
@@ -120,25 +100,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def update
- update_params = issue_params.merge(spammable_params)
-
- @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
-
- respond_to do |format|
- format.html do
- recaptcha_check_with_fallback { render :edit }
- end
-
- format.json do
- render_issue_json
- end
- end
-
- rescue ActiveRecord::StaleObjectError
- render_conflict_response
- end
-
def move
params.require(:move_to_project_id)
@@ -196,26 +157,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def realtime_changes
- Gitlab::PollingInterval.set_header(response, interval: 3_000)
-
- response = {
- title: view_context.markdown_field(@issue, :title),
- title_text: @issue.title,
- description: view_context.markdown_field(@issue, :description),
- description_text: @issue.description,
- task_status: @issue.task_status
- }
-
- if @issue.edited?
- response[:updated_at] = @issue.updated_at
- response[:updated_by_name] = @issue.last_edited_by.name
- response[:updated_by_path] = user_path(@issue.last_edited_by)
- end
-
- render json: response
- end
-
def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
@@ -230,8 +171,10 @@ class Projects::IssuesController < Projects::ApplicationController
def issue
return @issue if defined?(@issue)
+
# The Sortable default scope causes performance issues when used with find_by
- @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue)
@@ -246,14 +189,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_issue_path(@project, @issue)
end
- def authorize_update_issue!
- render_404 unless can?(current_user, :update_issue, @issue)
- end
-
- def authorize_admin_issues!
- render_404 unless can?(current_user, :admin_issue, @project)
- end
-
def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
@@ -305,4 +240,14 @@ class Projects::IssuesController < Projects::ApplicationController
def serializer
IssueSerializer.new(current_user: current_user, project: issue.project)
end
+
+ def update_service
+ update_params = issue_params.merge(spammable_params)
+ Issues::UpdateService.new(project, current_user, update_params)
+ end
+
+ def set_issuables_index
+ @finder_type = IssuesFinder
+ super
+ end
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 1b985ea9763..1c4c09c772f 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
- except: [:index, :show, :status, :raw, :trace, :cancel_all]
+ except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
+ before_action :authorize_erase_build!, only: [:erase]
layout 'project'
@@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_build, build)
end
+ def authorize_erase_build!
+ return access_denied! unless can?(current_user, :erase_build, build)
+ end
+
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 480a2dff262..e0f4710175f 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -111,6 +111,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
+
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project),
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 32759672b6c..293869345bd 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -54,6 +54,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
name = request.headers['X-Gitlab-Lfs-Tmp']
return if name.include?('/')
return unless oid.present? && name.start_with?(oid)
+
name
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 0e71977a58a..1269759fc2b 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -2,7 +2,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
before_action :check_merge_requests_available!
before_action :merge_request
before_action :authorize_read_merge_request!
- before_action :ensure_ref_fetched
private
@@ -10,12 +9,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
@issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
- # Make sure merge requests created before 8.0
- # have head file in refs/merge-requests/
- def ensure_ref_fetched
- @merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
- end
-
def merge_request_params
params.require(:merge_request).permit(merge_request_params_attributes)
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 99dc3dda9e7..764a9c7111e 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -4,7 +4,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
include RendersCommits
skip_before_action :merge_request
- skip_before_action :ensure_ref_fetched
before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
@@ -111,9 +110,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@commits = prepare_commits_for_rendering(@merge_request.commits)
@commit = @merge_request.diff_head_commit
- @note_counts = Note.where(commit_id: @commits.map(&:id))
- .group(:commit_id).count
-
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
set_pipeline_variables
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 7d16e77ef66..d60a24d3f1d 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -10,10 +10,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
- end
+ render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
end
def diff_for_path
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c5204080333..abe4e5245b1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -7,37 +7,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections
skip_before_action :merge_request, only: [:index, :bulk_update]
- skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
- before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
+ before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
+
+ before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
def index
- @collection_type = "MergeRequest"
- @merge_requests = merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
- @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
- @total_pages = merge_requests_page_count(@merge_requests)
-
- return if redirect_out_of_range(@merge_requests, @total_pages)
-
- if params[:label_name].present?
- labels_params = { project_id: @project.id, title: params[:label_name] }
- @labels = LabelsFinder.new(current_user, labels_params).execute
- end
-
- @users = []
- if params[:assignee_id].present?
- assignee = User.find_by_id(params[:assignee_id])
- @users.push(assignee) if assignee
- end
-
- if params[:author_id].present?
- author = User.find_by_id(params[:author_id])
- @users.push(author) if author
- end
+ @merge_requests = @issuables
respond_to do |format|
format.html
@@ -52,7 +30,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show
validates_merge_request
- ensure_ref_fetched
close_merge_request_without_source_project
check_if_can_be_merged
@@ -83,7 +60,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
- render json: serializer.represent(@merge_request, basic: params[:basic])
+ render json: serializer.represent(@merge_request, serializer: params[:serializer])
end
format.patch do
@@ -103,9 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def commits
# Get commits from repository
# or from cache if already merged
- @commits = prepare_commits_for_rendering(@merge_request.commits)
- @note_counts = Note.where(commit_id: @commits.map(&:id))
- .group(:commit_id).count
+ @commits =
+ prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status)
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
@@ -256,14 +232,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def authorize_update_merge_request!
- return render_404 unless can?(current_user, :update_merge_request, @merge_request)
- end
-
- def authorize_admin_merge_request!
- return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
- end
-
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
@@ -348,4 +316,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@target_project = @merge_request.target_project
@target_branches = @merge_request.target_project.repository.branch_names
end
+
+ def set_issuables_index
+ @finder_type = MergeRequestsFinder
+ super
+ end
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index c94384d2a1a..980bbf699b6 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -2,13 +2,13 @@ class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :check_issuables_available!
- before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels]
+ before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels, :promote]
# Allow read any milestone
before_action :authorize_read_milestone!
# Allow admin milestone
- before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
+ before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels, :promote]
respond_to :html
@@ -69,6 +69,14 @@ class Projects::MilestonesController < Projects::ApplicationController
end
end
+ def promote
+ promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
+ flash[:notice] = "Milestone has been promoted to group milestone."
+ redirect_to group_milestone_path(project.group, promoted_milestone.iid)
+ rescue Milestones::PromoteService::PromoteMilestoneError => error
+ redirect_to milestone, alert: error.message
+ end
+
def destroy
return access_denied! unless can?(current_user, :admin_milestone, @project)
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index ef7d047b1ad..627cb2bd93c 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -76,6 +76,7 @@ class Projects::NotesController < Projects::ApplicationController
def authorize_create_note!
return unless noteable.lockable?
+
access_denied! unless can?(current_user, :create_note, noteable)
end
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 2fd015df688..2376f469213 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -56,9 +56,12 @@ class Projects::RefsController < Projects::ApplicationController
contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
+ commit_path = project_commit_path(@project, last_commit) if last_commit
{
file_name: content.name,
- commit: last_commit
+ commit: last_commit,
+ type: content.type,
+ commit_path: commit_path
}
end
end
@@ -70,6 +73,11 @@ class Projects::RefsController < Projects::ApplicationController
respond_to do |format|
format.html { render_404 }
+ format.json do
+ response.headers["More-Logs-Url"] = @more_log_url
+
+ render json: @logs
+ end
format.js
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index f7a9c98629d..292e4158f8b 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -28,6 +28,7 @@ class Projects::WikisController < Projects::ApplicationController
)
else
return render('empty') unless can?(current_user, :create_wiki, @project)
+
@page = WikiPage.new(@project_wiki)
@page.title = params[:id]
@@ -74,7 +75,11 @@ class Projects::WikisController < Projects::ApplicationController
def history
@page = @project_wiki.find_page(params[:id])
- unless @page
+ if @page
+ @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
+ total_count: @page.count_versions)
+ .page(params[:page])
+ else
redirect_to(
project_wiki_path(@project, :home),
notice: "Page not found"
@@ -101,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
- @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
+ @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index db543d688a0..a784c6f402a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -269,13 +269,15 @@ class ProjectsController < Projects::ApplicationController
def render_landing_page
if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
+
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
- @issues = issues_collection.page(params[:page])
+ @finder_type = IssuesFinder
+ @issues = issuables_collection.page(params[:page])
@collection_type = 'Issue'
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
end
@@ -300,6 +302,8 @@ class ProjectsController < Projects::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def project_params
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index f9496787b15..c8b4682e6dc 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -20,6 +20,7 @@ class Snippets::NotesController < ApplicationController
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
+ alias_method :noteable, :snippet
def note_params
super.merge(noteable_id: params[:snippet_id])
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 4ee855806ab..5fca31b4956 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -108,6 +108,8 @@ class UsersController < ApplicationController
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def load_projects
diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb
index b8f52e31926..c3f5358b577 100644
--- a/app/finders/autocomplete_users_finder.rb
+++ b/app/finders/autocomplete_users_finder.rb
@@ -45,7 +45,7 @@ class AutocompleteUsersFinder
def find_users
return users_from_project if project
- return group.users if group
+ return group.users_with_parents if group
return User.all if current_user
User.none
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 0c4c4b10fb6..0282b378d88 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -15,6 +15,8 @@
# Anonymous users will never return any `owned` groups. They will return all
# public groups instead, even if `all_available` is set to false.
class GroupsFinder < UnionFinder
+ include CustomAttributesFilter
+
def initialize(current_user = nil, params = {})
@current_user = current_user
@params = params
@@ -22,8 +24,12 @@ class GroupsFinder < UnionFinder
def execute
items = all_groups.map do |item|
- by_parent(item)
+ item = by_parent(item)
+ item = by_custom_attributes(item)
+
+ item
end
+
find_union(items, Group).with_route.order_id_desc
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 24c07f3dc70..b46ec5e5350 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -36,6 +36,7 @@ class IssuableFinder
iids
label_name
milestone_title
+ my_reaction_emoji
non_archived
project_id
scope
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 760166b453f..d975f354a88 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -18,6 +18,7 @@ class PersonalAccessTokensFinder
def by_user(tokens)
return tokens unless @params[:user]
+
tokens.where(user: @params[:user])
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index eac6095d8dc..005612ededc 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -18,6 +18,8 @@
# non_archived: boolean
#
class ProjectsFinder < UnionFinder
+ include CustomAttributesFilter
+
attr_accessor :params
attr_reader :current_user, :project_ids_relation
@@ -44,6 +46,7 @@ class ProjectsFinder < UnionFinder
collection = by_tags(collection)
collection = by_search(collection)
collection = by_archived(collection)
+ collection = by_custom_attributes(collection)
sort(collection)
end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 8ad94d3f723..df590cf47c8 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -30,4 +30,11 @@ module AppearancesHelper
render 'shared/logo.svg'
end
end
+
+ # Skip the 'GitLab' type logo when custom brand logo is set
+ def brand_header_logo_type
+ unless brand_item && brand_item.header_logo?
+ render 'shared/logo_type.svg'
+ end
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index cd1ecaadb85..e5d2693b01e 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -231,6 +231,15 @@ module ApplicationSettingsHelper
:sign_in_text,
:signup_enabled,
:terminal_max_session_time,
+ :throttle_unauthenticated_enabled,
+ :throttle_unauthenticated_requests_per_period,
+ :throttle_unauthenticated_period_in_seconds,
+ :throttle_authenticated_web_enabled,
+ :throttle_authenticated_web_requests_per_period,
+ :throttle_authenticated_web_period_in_seconds,
+ :throttle_authenticated_api_enabled,
+ :throttle_authenticated_api_requests_per_period,
+ :throttle_authenticated_api_period_in_seconds,
:two_factor_grace_period,
:unique_ips_limit_enabled,
:unique_ips_limit_per_user,
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 8022547a6ad..636316da80a 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -6,11 +6,6 @@
# See 'detailed_status?` method and `Gitlab::Ci::Status` module.
#
module CiStatusHelper
- def ci_status_path(pipeline)
- project = pipeline.project
- project_pipeline_path(project, pipeline)
- end
-
def ci_label_for_status(status)
if detailed_status?(status)
return status.label
@@ -63,34 +58,34 @@ module CiStatusHelper
def ci_icon_for_status(status)
if detailed_status?(status)
- return custom_icon(status.icon)
+ return sprite_icon(status.icon)
end
icon_name =
case status
when 'success'
- 'icon_status_success'
+ 'status_success'
when 'success_with_warnings'
- 'icon_status_warning'
+ 'status_warning'
when 'failed'
- 'icon_status_failed'
+ 'status_failed'
when 'pending'
- 'icon_status_pending'
+ 'status_pending'
when 'running'
- 'icon_status_running'
+ 'status_running'
when 'play'
- 'icon_play'
+ 'play'
when 'created'
- 'icon_status_created'
+ 'status_created'
when 'skipped'
- 'icon_status_skipped'
+ 'status_skipped'
when 'manual'
- 'icon_status_manual'
+ 'status_manual'
else
- 'icon_status_canceled'
+ 'status_canceled'
end
- custom_icon(icon_name)
+ sprite_icon(icon_name, size: 16)
end
def pipeline_status_cache_key(pipeline_status)
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index ef22cafc2e2..f9a666fa1e6 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -60,23 +60,33 @@ module CommitsHelper
branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop
end
+ # Returns a link formatted as a commit branch link
+ def commit_branch_link(url, text)
+ link_to(url, class: 'label label-gray ref-name branch-link') do
+ icon('code-fork') + " #{text}"
+ end
+ end
+
# Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches)
branches.sort.map do |branch|
- link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do
- icon('code-fork') + " #{branch}"
- end
- end.join(" ").html_safe
+ commit_branch_link(project_ref_path(project, branch), branch)
+ end.join(' ').html_safe
+ end
+
+ # Returns a link formatted as a commit tag link
+ def commit_tag_link(url, text)
+ link_to(url, class: 'label label-gray ref-name') do
+ icon('tag') + " #{text}"
+ end
end
# Returns the sorted links to tags, separated by a comma
def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags)
sorted.map do |tag|
- link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do
- icon('tag') + " #{tag}"
- end
- end.join(" ").html_safe
+ commit_tag_link(project_ref_path(project, tag), tag)
+ end.join(' ').html_safe
end
def link_to_browse_code(project, commit)
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 4e4a66e8a02..e82136f0177 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -111,6 +111,7 @@ module DiffHelper
def diff_file_old_blob_raw_path(diff_file)
sha = diff_file.old_content_sha
return unless sha
+
project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
@@ -152,11 +153,11 @@ module DiffHelper
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file? || diff_file.renamed_file?
- "minus"
+ "file-deletion"
elsif diff_file.new_file?
- "plus"
+ "file-addition"
else
- "adjust"
+ "file-modified"
end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 5f11fe62030..878bc9b5c9c 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -24,6 +24,7 @@ module EmailsHelper
def action_title(url)
return unless url
+
%w(merge_requests issues commit).each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index fd88e0d794a..079b3cd3aa0 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -172,16 +172,6 @@ module EventsHelper
end
end
- def event_note(text, options = {})
- text = first_line_in_markdown(text, 150, options)
-
- sanitize(
- text,
- tags: %w(a img gl-emoji b pre code p span),
- attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
- )
- end
-
def event_commit_title(message)
message ||= ''
(message.split("\n").first || "").truncate(70)
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index d4a91e533c1..a77aa0ad2cc 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -71,11 +71,13 @@ module GitlabRoutingHelper
project_commit_url(entity.project, entity.sha, *args)
end
- def preview_markdown_path(project, *args)
+ def preview_markdown_path(parent, *args)
+ return group_preview_markdown_path(parent) if parent.is_a?(Group)
+
if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path
else
- preview_markdown_project_path(project, *args)
+ preview_markdown_project_path(parent, *args)
end
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index ec779c1c447..c6a83f21ceb 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -23,10 +23,17 @@ module IconsHelper
render "shared/icons/#{icon_name}.svg", size: size
end
+ def sprite_icon_path
+ # SVG Sprites currently don't work across domains, so in the case of a CDN
+ # we have to set the current path deliberately to prevent addition of asset_host
+ sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
+ ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
+ end
+
def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank?
- content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
+ content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end
def audit_icon(names, options = {})
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index baa2d6e375e..a9840d19178 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -33,15 +33,17 @@ module IssuablesHelper
end
def serialize_issuable(issuable)
- case issuable
- when Issue
- IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json
- when MergeRequest
- MergeRequestSerializer
- .new(current_user: current_user, project: issuable.project)
- .represent(issuable)
- .to_json
- end
+ serializer_klass = case issuable
+ when Issue
+ IssueSerializer
+ when MergeRequest
+ MergeRequestSerializer
+ end
+
+ serializer_klass
+ .new(current_user: current_user, project: issuable.project)
+ .represent(issuable)
+ .to_json
end
def template_dropdown_tag(issuable, &block)
@@ -209,15 +211,13 @@ module IssuablesHelper
def issuable_initial_data(issuable)
data = {
- endpoint: project_issue_path(@project, issuable),
- canUpdate: can?(current_user, :update_issue, issuable),
- canDestroy: can?(current_user, :destroy_issue, issuable),
+ endpoint: issuable_path(issuable),
+ canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
+ canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference,
- markdownPreviewPath: preview_markdown_path(@project),
+ markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable),
- projectPath: ref_project.path,
- projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description),
@@ -225,6 +225,12 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status
}
+ if parent.is_a?(Group)
+ data[:groupPath] = parent.path
+ else
+ data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
+ end
+
data.merge!(updated_at_by(issuable))
data.to_json
@@ -243,8 +249,6 @@ module IssuablesHelper
end
def issuables_count_for_state(issuable_type, state)
- finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
-
Gitlab::IssuablesCountForState.new(finder)[state]
end
@@ -261,12 +265,7 @@ module IssuablesHelper
end
def issuable_path(issuable, *options)
- case issuable
- when Issue
- issue_path(issuable, *options)
- when MergeRequest
- merge_request_path(issuable, *options)
- end
+ polymorphic_path(issuable, *options)
end
def issuable_url(issuable, *options)
@@ -357,7 +356,8 @@ module IssuablesHelper
def issuable_sidebar_options(issuable, can_edit_issuable)
{
- endpoint: "#{issuable_json_path(issuable)}?basic=true",
+ endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar",
+ toggleSubscriptionEndpoint: toggle_subscription_path(issuable),
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
@@ -366,4 +366,8 @@ module IssuablesHelper
fullPath: @project.full_path
}
end
+
+ def parent
+ @project || @group
+ end
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 46bced00c72..9d269cb65d6 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -53,6 +53,7 @@ module MarkupHelper
# text, wrapping anything found in the requested link
fragment.children.each do |node|
next unless node.text?
+
node.replace(link_to(node.text, url, html_options))
end
end
@@ -69,10 +70,16 @@ module MarkupHelper
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
- def first_line_in_markdown(text, max_chars = nil, options = {})
- md = markdown(text, options).strip
+ def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
+ md = markdown_field(object, attribute, options)
+
+ text = truncate_visible(md, max_chars || md.length) if md.present?
- truncate_visible(md, max_chars || md.length) if md.present?
+ sanitize(
+ text,
+ tags: %w(a img gl-emoji b pre code p span),
+ attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
+ )
end
def markdown(text, context = {})
@@ -83,15 +90,17 @@ module MarkupHelper
prepare_for_rendering(html, context)
end
- def markdown_field(object, field)
+ def markdown_field(object, field, context = {})
object = object.for_display if object.respond_to?(:for_display)
redacted_field_html = object.try(:"redacted_#{field}_html")
return '' unless object.present?
return redacted_field_html if redacted_field_html
- html = Banzai.render_field(object, field)
- prepare_for_rendering(html, object.banzai_render_context(field))
+ html = Banzai.render_field(object, field, context)
+ context.reverse_merge!(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)
+
+ prepare_for_rendering(html, context)
end
def markup(file_name, text, context = {})
@@ -213,12 +222,12 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: 'button',
- class: 'toolbar-btn js-md has-tooltip hidden-xs',
+ class: 'toolbar-btn js-md has-tooltip',
tabindex: -1,
data: data,
title: options[:title],
aria: { label: options[:title] } do
- icon(options[:icon])
+ sprite_icon(options[:icon])
end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index d7df9bb06d2..b78d3072186 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -4,8 +4,11 @@ module NamespacesHelper
end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
- groups = current_user.owned_groups + current_user.masters_groups
- users = [current_user.namespace]
+ groups = current_user.manageable_groups
+ .joins(:route)
+ .includes(:route)
+ .order('routes.path')
+ users = [current_user.namespace]
unless extra_group.nil? || extra_group.is_a?(Group)
extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 5a74511afa7..8ada746b244 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -19,11 +19,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
- elsif current_path?('wikis#show') ||
- current_path?('wikis#edit') ||
- current_path?('wikis#update') ||
- current_path?('wikis#history') ||
- current_path?('wikis#git_access')
+ elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index fde961e2da4..3e42063224e 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -78,6 +78,7 @@ module NotificationsHelper
# Create hidden field to send notification setting source to controller
def hidden_setting_source_input(notification_setting)
return unless notification_setting.source_type
+
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d085c1a0e57..f48d47953e4 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -110,7 +110,15 @@ module ProjectsHelper
def remove_fork_project_message(project)
_("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") %
- { forked_from_project: @project.forked_from_project.name_with_namespace }
+ { forked_from_project: fork_source_name(project) }
+ end
+
+ def fork_source_name(project)
+ if @project.fork_source
+ @project.fork_source.full_name
+ else
+ @project.fork_network&.deleted_root_project_name
+ end
end
def project_nav_tabs
@@ -140,8 +148,8 @@ module ProjectsHelper
def can_change_visibility_level?(project, current_user)
return false unless can?(current_user, :change_visibility_level, project)
- if project.forked?
- project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE
+ if project.fork_source
+ project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE
else
true
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index c4ea0f5ac53..5b2ea38a03d 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -1,14 +1,23 @@
module TreeHelper
+ FILE_LIMIT = 1_000
+
# Sorts a repository's tree so that folders are before files and renders
# their corresponding partials
#
- # contents - A Grit::Tree object for the current tree
+ # tree - A `Tree` object for the current tree
def render_tree(tree)
# Sort submodules and folders together by name ahead of files
folders, files, submodules = tree.trees, tree.blobs, tree.submodules
- tree = ""
+ tree = ''
items = (folders + submodules).sort_by(&:name) + files
- tree << render(partial: "projects/tree/tree_row", collection: items) if items.present?
+
+ if items.size > FILE_LIMIT
+ tree << render(partial: 'projects/tree/truncated_notice_tree_row',
+ locals: { limit: FILE_LIMIT, total: items.size })
+ items = items.take(FILE_LIMIT)
+ end
+
+ tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present?
tree.html_safe
end
@@ -88,6 +97,7 @@ module TreeHelper
part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part)
+
yield(part, part_path)
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 46867d2d974..c3d5628f241 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -150,6 +150,7 @@ module VisibilityLevelHelper
def restricted_visibility_levels(show_all = false)
return [] if current_user.admin? && !show_all
+
current_application_settings.restricted_visibility_levels || []
end
@@ -159,6 +160,7 @@ module VisibilityLevelHelper
def disallowed_visibility_level?(form_model, level)
return false unless form_model.respond_to?(:visibility_level_allowed?)
+
!form_model.visibility_level_allowed?(level)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 5e16badabec..a7e0219b03a 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -295,6 +295,15 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
+ throttle_unauthenticated_enabled: false,
+ throttle_unauthenticated_requests_per_period: 3600,
+ throttle_unauthenticated_period_in_seconds: 3600,
+ throttle_authenticated_web_enabled: false,
+ throttle_authenticated_web_requests_per_period: 7200,
+ throttle_authenticated_web_period_in_seconds: 3600,
+ throttle_authenticated_api_enabled: false,
+ throttle_authenticated_api_requests_per_period: 7200,
+ throttle_authenticated_api_period_in_seconds: 3600,
two_factor_grace_period: 48,
user_default_external: false,
polling_interval_multiplier: 1,
diff --git a/app/models/blob.rb b/app/models/blob.rb
index ad0bc2e2ead..29e762724e3 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -76,12 +76,24 @@ class Blob < SimpleDelegator
new(blob, project)
end
+ def self.lazy(project, commit_id, path)
+ BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader|
+ project.repository.blobs_at(items.map(&:values)).each do |blob|
+ loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob
+ end
+ end
+ end
+
def initialize(blob, project = nil)
@project = project
super(blob)
end
+ def inspect
+ "#<#{self.class.name} oid:#{id[0..8]} commit:#{commit_id[0..8]} path:#{path}>"
+ end
+
# Returns the data of the blob.
#
# If the blob is a text based blob the content is converted to UTF-8 and any
@@ -95,7 +107,10 @@ class Blob < SimpleDelegator
end
def load_all_data!
- super(project.repository) if project
+ # Endpoint needed: gitlab-org/gitaly#756
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ super(project.repository) if project
+ end
end
def no_highlighting?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6ca46ae89c1..1d9f367183e 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -192,6 +192,10 @@ module Ci
project.build_timeout
end
+ def triggered_by?(current_user)
+ user == current_user
+ end
+
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
@@ -313,6 +317,7 @@ module Ci
def execute_hooks
return unless project
+
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index ca65e81f27a..ebbefc51a4f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -66,8 +66,8 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
- transition created: :pending
- transition [:success, :failed, :canceled, :skipped] => :running
+ transition [:created, :skipped] => :pending
+ transition [:success, :failed, :canceled] => :running
end
event :run do
@@ -149,34 +149,70 @@ module Ci
end
end
- # ref can't be HEAD or SHA, can only be branch/tag name
- scope :latest, ->(ref = nil) do
- max_id = unscope(:select)
- .select("max(#{quoted_table_name}.id)")
- .group(:ref, :sha)
+ scope :internal, -> { where(source: internal_sources) }
- if ref
- where(ref: ref, id: max_id.where(ref: ref))
- else
- where(id: max_id)
- end
+ # Returns the pipelines in descending order (= newest first), optionally
+ # limited to a number of references.
+ #
+ # ref - The name (or names) of the branch(es)/tag(s) to limit the list of
+ # pipelines to.
+ def self.newest_first(ref = nil)
+ relation = order(id: :desc)
+
+ ref ? relation.where(ref: ref) : relation
end
- scope :internal, -> { where(source: internal_sources) }
def self.latest_status(ref = nil)
- latest(ref).status
+ newest_first(ref).pluck(:status).first
end
def self.latest_successful_for(ref)
- success.latest(ref).order(id: :desc).first
+ newest_first(ref).success.take
end
def self.latest_successful_for_refs(refs)
- success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
+ relation = newest_first(refs).success
+
+ relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.ref] ||= pipeline
end
end
+ # Returns a Hash containing the latest pipeline status for every given
+ # commit.
+ #
+ # The keys of this Hash are the commit SHAs, the values the statuses.
+ #
+ # commits - The list of commit SHAs to get the status for.
+ # ref - The ref to scope the data to (e.g. "master"). If the ref is not
+ # given we simply get the latest status for the commits, regardless
+ # of what refs their pipelines belong to.
+ def self.latest_status_per_commit(commits, ref = nil)
+ p1 = arel_table
+ p2 = arel_table.alias
+
+ # This LEFT JOIN will filter out all but the newest row for every
+ # combination of (project_id, sha) or (project_id, sha, ref) if a ref is
+ # given.
+ cond = p1[:sha].eq(p2[:sha])
+ .and(p1[:project_id].eq(p2[:project_id]))
+ .and(p1[:id].lt(p2[:id]))
+
+ cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
+ join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)
+
+ relation = select(:sha, :status)
+ .where(sha: commits)
+ .where(p2[:id].eq(nil))
+ .joins(join.join_sources)
+
+ relation = relation.where(ref: ref) if ref
+
+ relation.each_with_object({}) do |row, hash|
+ hash[row[:sha]] = row[:status]
+ end
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
@@ -300,8 +336,10 @@ module Ci
def latest?
return false unless ref
+
commit = project.commit(ref)
return false unless commit
+
commit.sha == sha
end
@@ -409,7 +447,7 @@ module Ci
end
def notes
- Note.for_commit_id(sha)
+ project.notes.for_commit_id(sha)
end
def process!
@@ -469,7 +507,10 @@ module Ci
end
def latest_builds_with_artifacts
- @latest_builds_with_artifacts ||= builds.latest.with_artifacts
+ # We purposely cast the builds to an Array here. Because we always use the
+ # rows if there are more than 0 this prevents us from having to run two
+ # queries: one to get the count and one to get the rows.
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a
end
private
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
new file mode 100644
index 00000000000..c7949d11ef8
--- /dev/null
+++ b/app/models/clusters/applications/helm.rb
@@ -0,0 +1,35 @@
+module Clusters
+ module Applications
+ class Helm < ActiveRecord::Base
+ self.table_name = 'clusters_applications_helm'
+
+ include ::Clusters::Concerns::ApplicationStatus
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
+
+ validates :cluster, presence: true
+
+ after_initialize :set_initial_status
+
+ def self.application_name
+ self.to_s.demodulize.underscore
+ end
+
+ def set_initial_status
+ return unless not_installable?
+
+ self.status = 'installable' if cluster&.platform_kubernetes_active?
+ end
+
+ def name
+ self.class.application_name
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, true)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
new file mode 100644
index 00000000000..44bd979741e
--- /dev/null
+++ b/app/models/clusters/applications/ingress.rb
@@ -0,0 +1,44 @@
+module Clusters
+ module Applications
+ class Ingress < ActiveRecord::Base
+ self.table_name = 'clusters_applications_ingress'
+
+ include ::Clusters::Concerns::ApplicationStatus
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ validates :cluster, presence: true
+
+ default_value_for :ingress_type, :nginx
+ default_value_for :version, :nginx
+
+ after_initialize :set_initial_status
+
+ enum ingress_type: {
+ nginx: 1
+ }
+
+ def self.application_name
+ self.to_s.demodulize.underscore
+ end
+
+ def set_initial_status
+ return unless not_installable?
+
+ self.status = 'installable' if cluster&.application_helm_installed?
+ end
+
+ def name
+ self.class.application_name
+ end
+
+ def chart
+ 'stable/nginx-ingress'
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, false, chart)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
new file mode 100644
index 00000000000..185d9473aab
--- /dev/null
+++ b/app/models/clusters/cluster.rb
@@ -0,0 +1,102 @@
+module Clusters
+ class Cluster < ActiveRecord::Base
+ include Presentable
+
+ self.table_name = 'clusters'
+
+ APPLICATIONS = {
+ Applications::Helm.application_name => Applications::Helm,
+ Applications::Ingress.application_name => Applications::Ingress
+ }.freeze
+
+ belongs_to :user
+
+ has_many :cluster_projects, class_name: 'Clusters::Project'
+ has_many :projects, through: :cluster_projects, class_name: '::Project'
+
+ # we force autosave to happen when we save `Cluster` model
+ has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
+
+ # We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ has_one :application_helm, class_name: 'Clusters::Applications::Helm'
+ has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
+
+ accepts_nested_attributes_for :provider_gcp, update_only: true
+ accepts_nested_attributes_for :platform_kubernetes, update_only: true
+
+ validates :name, cluster_name: true
+ validate :restrict_modification, on: :update
+
+ # TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3
+ # We need callback here because `enabled` belongs to Clusters::Cluster
+ # Callbacks in Clusters::Platforms::Kubernetes will not be called after update
+ after_save :update_kubernetes_integration!
+
+ delegate :status, to: :provider, allow_nil: true
+ delegate :status_reason, to: :provider, allow_nil: true
+ delegate :on_creation?, to: :provider, allow_nil: true
+ delegate :update_kubernetes_integration!, to: :platform, allow_nil: true
+
+ delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
+ delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+
+ scope :enabled, -> { where(enabled: true) }
+ scope :disabled, -> { where(enabled: false) }
+
+ def status_name
+ if provider
+ provider.status_name
+ else
+ :created
+ end
+ end
+
+ def applications
+ [
+ application_helm || build_application_helm,
+ application_ingress || build_application_ingress
+ ]
+ end
+
+ def provider
+ return provider_gcp if gcp?
+ end
+
+ def platform
+ return platform_kubernetes if kubernetes?
+ end
+
+ def first_project
+ return @first_project if defined?(@first_project)
+
+ @first_project = projects.first
+ end
+ alias_method :project, :first_project
+
+ def kubeclient
+ platform_kubernetes.kubeclient if kubernetes?
+ end
+
+ private
+
+ def restrict_modification
+ if provider&.on_creation?
+ errors.add(:base, "cannot modify during creation")
+ return false
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
new file mode 100644
index 00000000000..7b7c8eac773
--- /dev/null
+++ b/app/models/clusters/concerns/application_status.rb
@@ -0,0 +1,43 @@
+module Clusters
+ module Concerns
+ module ApplicationStatus
+ extend ActiveSupport::Concern
+
+ included do
+ state_machine :status, initial: :not_installable do
+ state :not_installable, value: -2
+ state :errored, value: -1
+ state :installable, value: 0
+ state :scheduled, value: 1
+ state :installing, value: 2
+ state :installed, value: 3
+
+ event :make_scheduled do
+ transition [:installable, :errored] => :scheduled
+ end
+
+ event :make_installing do
+ transition [:scheduled] => :installing
+ end
+
+ event :make_installed do
+ transition [:installing] => :installed
+ end
+
+ event :make_errored do
+ transition any => :errored
+ end
+
+ before_transition any => [:scheduled] do |app_status, _|
+ app_status.status_reason = nil
+ end
+
+ before_transition any => [:errored] do |app_status, transition|
+ status_reason = transition.args.first
+ app_status.status_reason = status_reason if status_reason
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..6dc1ee810d3
--- /dev/null
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -0,0 +1,109 @@
+module Clusters
+ module Platforms
+ class Kubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+
+ belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
+
+ attr_encrypted :password,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ before_validation :enforce_namespace_to_lower_case
+
+ validates :namespace,
+ allow_blank: true,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
+ validates :api_url, url: true, presence: true
+ validates :token, presence: true
+
+ # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
+ after_destroy :destroy_kubernetes_integration!
+
+ alias_attribute :ca_pem, :ca_cert
+
+ delegate :project, to: :cluster, allow_nil: true
+ delegate :enabled?, to: :cluster, allow_nil: true
+
+ class << self
+ def namespace_for_project(project)
+ "#{project.path}-#{project.id}"
+ end
+ end
+
+ def actual_namespace
+ if namespace.present?
+ namespace
+ else
+ default_namespace
+ end
+ end
+
+ def default_namespace
+ self.class.namespace_for_project(project) if project
+ end
+
+ def kubeclient
+ @kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service?
+ end
+
+ def update_kubernetes_integration!
+ raise 'Kubernetes service already configured' unless manages_kubernetes_service?
+
+ # This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
+ cluster.reload
+
+ ensure_kubernetes_service&.update!(
+ active: enabled?,
+ api_url: api_url,
+ namespace: namespace,
+ token: token,
+ ca_pem: ca_cert
+ )
+ end
+
+ def active?
+ manages_kubernetes_service?
+ end
+
+ private
+
+ def enforce_namespace_to_lower_case
+ self.namespace = self.namespace&.downcase
+ end
+
+ # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
+ def manages_kubernetes_service?
+ return true unless kubernetes_service&.active?
+
+ kubernetes_service.api_url == api_url
+ end
+
+ def destroy_kubernetes_integration!
+ return unless manages_kubernetes_service?
+
+ kubernetes_service&.destroy!
+ end
+
+ def kubernetes_service
+ @kubernetes_service ||= project&.kubernetes_service
+ end
+
+ def ensure_kubernetes_service
+ @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb
new file mode 100644
index 00000000000..eeb734b20b8
--- /dev/null
+++ b/app/models/clusters/project.rb
@@ -0,0 +1,8 @@
+module Clusters
+ class Project < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster'
+ belongs_to :project, class_name: '::Project'
+ end
+end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..7fac32466ab
--- /dev/null
+++ b/app/models/clusters/providers/gcp.rb
@@ -0,0 +1,80 @@
+module Clusters
+ module Providers
+ class Gcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+
+ belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster'
+
+ default_value_for :zone, 'us-central1-a'
+ default_value_for :num_nodes, 3
+ default_value_for :machine_type, 'n1-standard-2'
+
+ attr_encrypted :access_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ validates :gcp_project_id,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :zone, presence: true
+
+ validates :num_nodes,
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than: 0
+ }
+
+ state_machine :status, initial: :scheduled do
+ state :scheduled, value: 1
+ state :creating, value: 2
+ state :created, value: 3
+ state :errored, value: 4
+
+ event :make_creating do
+ transition any - [:creating] => :creating
+ end
+
+ event :make_created do
+ transition any - [:created] => :created
+ end
+
+ event :make_errored do
+ transition any - [:errored] => :errored
+ end
+
+ before_transition any => [:errored, :created] do |provider|
+ provider.access_token = nil
+ provider.operation_id = nil
+ end
+
+ before_transition any => [:creating] do |provider, transition|
+ operation_id = transition.args.first
+ raise ArgumentError.new('operation_id is required') unless operation_id.present?
+
+ provider.operation_id = operation_id
+ end
+
+ before_transition any => [:errored] do |provider, transition|
+ status_reason = transition.args.first
+ provider.status_reason = status_reason if status_reason
+ end
+ end
+
+ def on_creation?
+ scheduled? || creating?
+ end
+
+ def api_client
+ return unless access_token
+
+ @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil)
+ end
+ end
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6dba154a6ea..8401d99a08f 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -80,10 +80,11 @@ class Commit
@raw = raw_commit
@project = project
+ @statuses = {}
end
def id
- @raw.id
+ raw.id
end
def ==(other)
@@ -236,11 +237,13 @@ class Commit
end
def status(ref = nil)
- @statuses ||= {}
-
return @statuses[ref] if @statuses.key?(ref)
- @statuses[ref] = pipelines.latest_status(ref)
+ @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id]
+ end
+
+ def set_status_for_ref(ref, status)
+ @statuses[ref] = status
end
def signature
@@ -358,7 +361,7 @@ class Commit
@deltas ||= raw.deltas
end
- def diffs(diff_options = nil)
+ def diffs(diff_options = {})
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
new file mode 100644
index 00000000000..dd93af9df64
--- /dev/null
+++ b/app/models/commit_collection.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# A collection of Commit instances for a specific project and Git reference.
+class CommitCollection
+ include Enumerable
+
+ attr_reader :project, :ref, :commits
+
+ # project - The project the commits belong to.
+ # commits - The Commit instances to store.
+ # ref - The name of the ref (e.g. "master").
+ def initialize(project, commits, ref = nil)
+ @project = project
+ @commits = commits
+ @ref = ref
+ end
+
+ def each(&block)
+ commits.each(&block)
+ end
+
+ # Sets the pipeline status for every commit.
+ #
+ # Setting this status ahead of time removes the need for running a query for
+ # every commit we're displaying.
+ def with_pipeline_status
+ statuses = project.pipelines.latest_status_per_commit(map(&:id), ref)
+
+ each do |commit|
+ commit.set_status_for_ref(ref, statuses[commit.id])
+ end
+
+ self
+ end
+
+ def respond_to_missing?(message, inc_private = false)
+ commits.respond_to?(message, inc_private)
+ end
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(message, *args, &block)
+ commits.public_send(message, *args, &block)
+ end
+end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index f3888528940..6b07dbdf3ea 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -14,7 +14,6 @@ class CommitStatus < ActiveRecord::Base
delegate :sha, :short_sha, to: :pipeline
validates :pipeline, presence: true, unless: :importing?
-
validates :name, presence: true, unless: :importing?
alias_attribute :author, :user
@@ -46,6 +45,17 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4
}
+ ##
+ # We still create some CommitStatuses outside of CreatePipelineService.
+ #
+ # These are pages deployments and external statuses.
+ #
+ before_create unless: :importing? do
+ Ci::EnsureStageService.new(project, user).execute(self) do |stage|
+ self.run_after_commit { StageUpdateWorker.perform_async(stage.id) }
+ end
+ end
+
state_machine :status do
event :process do
transition [:skipped, :manual] => :created
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 2ec70203710..10659030910 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -4,15 +4,26 @@ module Avatarable
def avatar_path(only_path: true)
return unless self[:avatar].present?
- # If only_path is true then use the relative path of avatar.
- # Otherwise use full path (including host).
asset_host = ActionController::Base.asset_host
- gitlab_host = only_path ? gitlab_config.relative_url_root : gitlab_config.url
+ use_asset_host = asset_host.present?
- # If asset_host is set then it is expected that assets are handled by a standalone host.
- # That means we do not want to get GitLab's relative_url_root option anymore.
- host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host
+ # Avatars for private and internal groups and projects require authentication to be viewed,
+ # which means they can only be served by Rails, on the regular GitLab host.
+ # If an asset host is configured, we need to return the fully qualified URL
+ # instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
+ if use_asset_host && respond_to?(:public?) && !public?
+ use_asset_host = false
+ only_path = false
+ end
- [host, avatar.url].join
+ url_base = ""
+ if use_asset_host
+ url_base << asset_host unless only_path
+ else
+ url_base << gitlab_config.base_url unless only_path
+ url_base << gitlab_config.relative_url_root
+ end
+
+ url_base + avatar.url
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 9adc309a22b..d8394415362 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -98,6 +98,7 @@ module Awardable
def create_award_emoji(name, current_user)
return unless emoji_awardable?
+
award_emoji.create(name: normalize_name(name), user: current_user)
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9417033d1f6..98776eab424 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -49,7 +49,8 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
- context = cached_markdown_fields[field].merge(project: project)
+ group = self.group if self.respond_to?(:group)
+ context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb
index eb9f3423e48..03793e8bcbb 100644
--- a/app/models/concerns/ignorable_column.rb
+++ b/app/models/concerns/ignorable_column.rb
@@ -21,8 +21,8 @@ module IgnorableColumn
@ignored_columns ||= Set.new
end
- def ignore_column(name)
- ignored_columns << name.to_s
+ def ignore_column(*names)
+ ignored_columns.merge(names.map(&:to_s))
end
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 27f4dedffd3..35090181bd9 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -14,10 +14,11 @@ module Issuable
include StripAttribute
include Awardable
include Taskable
- include TimeTrackable
include Importable
include Editable
include AfterCommitQueue
+ include Sortable
+ include CreatedAtFilterable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
@@ -95,8 +96,6 @@ module Issuable
strip_attributes :title
- acts_as_paranoid
-
after_save :record_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved
@@ -256,7 +255,7 @@ module Issuable
participants(user).include?(user)
end
- def to_hook_data(user, old_labels: [], old_assignees: [])
+ def to_hook_data(user, old_labels: [], old_assignees: [], old_total_time_spent: nil)
changes = previous_changes
if old_labels != labels
@@ -271,6 +270,10 @@ module Issuable
end
end
+ if old_total_time_spent != total_time_spent
+ changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ end
+
Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 710fc1ed647..7026f565706 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -86,6 +86,14 @@ module Milestoneish
false
end
+ def total_issue_time_spent
+ @total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
+ end
+
+ def human_total_issue_time_spent
+ Gitlab::TimeTrackingFormatter.output(total_issue_time_spent)
+ end
+
private
def count_issues_by_state(user)
diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb
deleted file mode 100644
index f6aba91bc4c..00000000000
--- a/app/models/concerns/repository_mirroring.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module RepositoryMirroring
- IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
- IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
-
- def set_remote_as_mirror(name)
- # This is used to define repository as equivalent as "git clone --mirror"
- raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*'
- raw_repository.rugged.config["remote.#{name}.mirror"] = true
- raw_repository.rugged.config["remote.#{name}.prune"] = true
- end
-
- def set_import_remote_as_mirror(remote_name)
- # Add first fetch with Rugged so it does not create its own.
- raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
-
- add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
-
- raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true
- raw_repository.rugged.config["remote.#{remote_name}.prune"] = true
- end
-
- def add_remote_fetch_config(remote_name, refspec)
- run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
- end
-
- def fetch_mirror(remote, url)
- add_remote(remote, url)
- set_remote_as_mirror(remote)
- fetch_remote(remote, forced: true)
- remove_remote(remote)
- end
-end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 274b38a7708..f478c8ede18 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -13,6 +13,8 @@ module Subscribable
end
def subscribed?(user, project = nil)
+ return false unless user
+
if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index d88a92dc027..ae5f138a920 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -18,7 +18,8 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
- before_validation :set_original_position, :update_position, on: :create
+ before_validation :set_original_position, on: :create
+ before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code
after_save :keep_around_commits
diff --git a/app/models/environment.rb b/app/models/environment.rb
index e613d21add6..21a028e351c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -30,7 +30,6 @@ class Environment < ActiveRecord::Base
message: Gitlab::Regex.environment_slug_regex_message }
validates :external_url,
- uniqueness: { scope: :project_id },
length: { maximum: 255 },
allow_nil: true,
addressable_url: true
@@ -110,7 +109,7 @@ class Environment < ActiveRecord::Base
end
def ref_path
- "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}"
+ "refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
def formatted_external_url
@@ -164,6 +163,10 @@ class Environment < ActiveRecord::Base
end
end
+ def slug
+ super.presence || generate_slug
+ end
+
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
diff --git a/app/models/epic.rb b/app/models/epic.rb
new file mode 100644
index 00000000000..62898a02e2d
--- /dev/null
+++ b/app/models/epic.rb
@@ -0,0 +1,7 @@
+# Placeholder class for model that is implemented in EE
+# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
+class Epic < ActiveRecord::Base
+ # TODO: this will be implemented as part of #3853
+ def to_reference
+ end
+end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 0bf18e529f0..9ff56f229bc 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -47,4 +47,8 @@ class ExternalIssue
id
end
+
+ def notes
+ Note.none
+ end
end
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
index 218e37a5312..7f1728e8c77 100644
--- a/app/models/fork_network.rb
+++ b/app/models/fork_network.rb
@@ -12,4 +12,8 @@ class ForkNetwork < ActiveRecord::Base
def find_forks_in(other_projects)
projects.where(id: other_projects)
end
+
+ def merge_requests
+ MergeRequest.where(target_project: projects)
+ end
end
diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb
index 6a9b52a1ef8..eb9417dc34f 100644
--- a/app/models/fork_network_member.rb
+++ b/app/models/fork_network_member.rb
@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base
belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true
+
+ after_destroy :cleanup_fork_network
+
+ private
+
+ def cleanup_fork_network
+ # Explicitly using `#count` makes sure we have the correct number if the
+ # relation was loaded in the fork_network.
+ fork_network.destroy if fork_network.fork_network_members.count == 0
+ end
end
diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb
deleted file mode 100644
index 162a690c0e3..00000000000
--- a/app/models/gcp/cluster.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-module Gcp
- class Cluster < ActiveRecord::Base
- extend Gitlab::Gcp::Model
- include Presentable
-
- belongs_to :project, inverse_of: :cluster
- belongs_to :user
- belongs_to :service
-
- scope :enabled, -> { where(enabled: true) }
- scope :disabled, -> { where(enabled: false) }
-
- default_value_for :gcp_cluster_zone, 'us-central1-a'
- default_value_for :gcp_cluster_size, 3
- default_value_for :gcp_machine_type, 'n1-standard-4'
-
- attr_encrypted :password,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :kubernetes_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :gcp_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- validates :gcp_project_id,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_name,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_zone, presence: true
-
- validates :gcp_cluster_size,
- presence: true,
- numericality: {
- only_integer: true,
- greater_than: 0
- }
-
- validates :project_namespace,
- allow_blank: true,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- # if we do not do status transition we prevent change
- validate :restrict_modification, on: :update, unless: :status_changed?
-
- state_machine :status, initial: :scheduled do
- state :scheduled, value: 1
- state :creating, value: 2
- state :created, value: 3
- state :errored, value: 4
-
- event :make_creating do
- transition any - [:creating] => :creating
- end
-
- event :make_created do
- transition any - [:created] => :created
- end
-
- event :make_errored do
- transition any - [:errored] => :errored
- end
-
- before_transition any => [:errored, :created] do |cluster|
- cluster.gcp_token = nil
- cluster.gcp_operation_id = nil
- end
-
- before_transition any => [:errored] do |cluster, transition|
- status_reason = transition.args.first
- cluster.status_reason = status_reason if status_reason
- end
- end
-
- def project_namespace_placeholder
- "#{project.path}-#{project.id}"
- end
-
- def on_creation?
- scheduled? || creating?
- end
-
- def api_url
- 'https://' + endpoint if endpoint
- end
-
- def restrict_modification
- if on_creation?
- errors.add(:base, "cannot modify during creation")
- return false
- end
-
- true
- end
- end
-end
diff --git a/app/models/group.rb b/app/models/group.rb
index 07fb62bb249..8cf632fb566 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -26,6 +26,7 @@ class Group < Namespace
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
+ has_many :custom_attributes, class_name: 'GroupCustomAttribute'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
@@ -42,6 +43,7 @@ class Group < Namespace
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
+ after_update :path_changed_hook, if: :path_changed?
class << self
def supports_nested_groups?
@@ -180,6 +182,12 @@ class Group < Namespace
add_user(user, :owner, current_user: current_user)
end
+ def member?(user, min_access_level = Gitlab::Access::GUEST)
+ return false unless user
+
+ max_member_access_for_user(user) >= min_access_level
+ end
+
def has_owner?(user)
return false unless user
@@ -289,6 +297,12 @@ class Group < Namespace
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end
+ def full_path_was
+ return path_was unless has_parent?
+
+ "#{parent.full_path}/#{path_was}"
+ end
+
private
def update_two_factor_requirement
@@ -297,6 +311,10 @@ class Group < Namespace
users.find_each(&:update_two_factor_requirement)
end
+ def path_changed_hook
+ system_hook_service.execute_hooks_for(self, :rename)
+ end
+
def visibility_level_allowed_by_parent
return if visibility_level_allowed_by_parent?
diff --git a/app/models/group_custom_attribute.rb b/app/models/group_custom_attribute.rb
new file mode 100644
index 00000000000..8157d602d67
--- /dev/null
+++ b/app/models/group_custom_attribute.rb
@@ -0,0 +1,6 @@
+class GroupCustomAttribute < ActiveRecord::Base
+ belongs_to :group
+
+ validates :group, :key, :value, presence: true
+ validates :key, uniqueness: { scope: [:group_id] }
+end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 920a25932b4..ff811e19f8a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,15 +1,27 @@
class Identity < ActiveRecord::Base
include Sortable
include CaseSensitivity
+
belongs_to :user
validates :provider, presence: true
- validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider }
- scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) }
+ scope :with_provider, ->(provider) { where(provider: provider) }
+ scope :with_extern_uid, ->(provider, extern_uid) do
+ iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
+ end
def ldap?
provider.starts_with?('ldap')
end
+
+ def self.normalize_uid(provider, uid)
+ if provider.to_s.starts_with?('ldap')
+ Gitlab::LDAP::Person.normalize_dn(uid)
+ else
+ uid.to_s
+ end
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 36e4108b9d6..a9863a50d84 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -5,11 +5,10 @@ class Issue < ActiveRecord::Base
include Issuable
include Noteable
include Referable
- include Sortable
include Spammable
include FasterCacheKeys
include RelativePositioning
- include CreatedAtFilterable
+ include TimeTrackable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -74,6 +73,8 @@ class Issue < ActiveRecord::Base
end
end
+ acts_as_paranoid
+
def self.reference_prefix
'#'
end
@@ -245,7 +246,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user]
+ if options.key?(:sidebar_endpoints) && project
+ url_helper = Gitlab::Routing.url_helpers
+
+ json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
+ toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
+ end
if options.key?(:labels)
json[:labels] = labels.as_json(
@@ -261,10 +267,6 @@ class Issue < ActiveRecord::Base
true
end
- def update_project_counter_caches?
- state_changed? || confidential_changed?
- end
-
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
diff --git a/app/models/key.rb b/app/models/key.rb
index f119b15c737..815fd1de909 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -27,8 +27,10 @@ class Key < ActiveRecord::Base
after_commit :add_to_shell, on: :create
after_create :post_create_hook
+ after_create :refresh_user_cache
after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook
+ after_destroy :refresh_user_cache
def key=(value)
value&.delete!("\n\r")
@@ -76,6 +78,12 @@ class Key < ActiveRecord::Base
)
end
+ def refresh_user_cache
+ return unless user
+
+ Users::KeysCountService.new(user).refresh_cache
+ end
+
def post_destroy_hook
SystemHooksService.new.execute_hooks_for(self, :destroy)
end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index b7cf96abe83..fc586fa216e 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -6,16 +6,8 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- def storage_project(project)
- if project && project.forked?
- storage_project(project.forked_from_project)
- else
- project
- end
- end
-
def project_allowed_access?(project)
- projects.exists?(storage_project(project).id)
+ projects.exists?(project.lfs_storage_project.id)
end
def self.destroy_unreferenced
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d45b9c805a4..f1a5cc73e83 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -3,11 +3,11 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Noteable
include Referable
- include Sortable
include IgnorableColumn
- include CreatedAtFilterable
+ include TimeTrackable
- ignore_column :locked_at
+ ignore_column :locked_at,
+ :ref_fetched
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
@@ -119,6 +119,8 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit
+ acts_as_paranoid
+
def self.reference_prefix
'!'
end
@@ -423,7 +425,7 @@ class MergeRequest < ActiveRecord::Base
end
def create_merge_request_diff
- fetch_ref
+ fetch_ref!
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do
@@ -574,7 +576,7 @@ class MergeRequest < ActiveRecord::Base
commit_notes = Note
.except(:order)
.where(project_id: [source_project_id, target_project_id])
- .where(noteable_type: 'Commit', commit_id: commit_ids)
+ .for_commit_id(commit_ids)
# We're using a UNION ALL here since this results in better performance
# compared to using OR statements. We're using UNION ALL since the queries
@@ -808,29 +810,14 @@ class MergeRequest < ActiveRecord::Base
end
end
- def fetch_ref
- write_ref
- update_column(:ref_fetched, true)
+ def fetch_ref!
+ target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
end
def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
- def ref_fetched?
- super ||
- begin
- computed_value = project.repository.ref_exists?(ref_path)
- update_column(:ref_fetched, true) if computed_value
-
- computed_value
- end
- end
-
- def ensure_ref_fetched
- fetch_ref unless ref_fetched?
- end
-
def in_locked_state
begin
lock_mr
@@ -878,7 +865,19 @@ class MergeRequest < ActiveRecord::Base
#
def all_commit_shas
if persisted?
- column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha')
+ # MySQL doesn't support LIMIT in a subquery.
+ diffs_relation =
+ if Gitlab::Database.postgresql?
+ merge_request_diffs.order(id: :desc).limit(100)
+ else
+ merge_request_diffs
+ end
+
+ column_shas = MergeRequestDiffCommit
+ .where(merge_request_diff: diffs_relation)
+ .limit(10_000)
+ .pluck('sha')
+
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq
@@ -959,10 +958,6 @@ class MergeRequest < ActiveRecord::Base
true
end
- def update_project_counter_caches?
- state_changed?
- end
-
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
@@ -972,10 +967,4 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty?
end
-
- private
-
- def write_ref
- target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
- end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index faf0b95f842..5382f5cc627 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
+ MergeRequest
+ .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
+ .update_all(latest_merge_request_diff_id: self.id)
+
ensure_commit_shas
save_commits
save_diffs
@@ -280,8 +284,10 @@ class MergeRequestDiff < ActiveRecord::Base
def load_commits
commits = st_commits.presence || merge_request_diff_commits
+ commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
- commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
+ CommitCollection
+ .new(merge_request.source_project, commits, merge_request.source_branch)
end
def save_diffs
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 670b26d4ca3..b75387e236e 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -17,7 +17,9 @@ class MergeRequestDiffCommit < ActiveRecord::Base
commit_hash.merge(
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
- sha: sha_attribute.type_cast_for_database(sha)
+ sha: sha_attribute.type_cast_for_database(sha),
+ authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
+ committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
)
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 47e6b785c39..e01e52131f0 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -256,7 +256,7 @@ class Milestone < ActiveRecord::Base
def start_date_should_be_less_than_due_date
if due_date <= start_date
- errors.add(:start_date, "Can't be greater than due date")
+ errors.add(:due_date, "must be greater than start date")
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 0601a61a926..4d401e7ba18 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -36,7 +36,7 @@ class Namespace < ActiveRecord::Base
validates :path,
presence: true,
length: { maximum: 255 },
- dynamic_path: true
+ namespace_path: true
validate :nesting_level_allowed
diff --git a/app/models/note.rb b/app/models/note.rb
index 8939e590ef1..50c9caf8529 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -69,7 +69,7 @@ class Note < ActiveRecord::Base
delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true
- validates :project, presence: true, unless: :for_personal_snippet?
+ validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -110,11 +110,12 @@ class Note < ActiveRecord::Base
includes(:author, :noteable, :updated_by,
project: [:project_members, { group: [:group_members] }])
end
+ scope :with_metadata, -> { includes(:system_note_metadata) }
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id, on: :create
- after_save :keep_around_commit, unless: :for_personal_snippet?
+ after_save :keep_around_commit, if: :for_project_noteable?
after_save :expire_etag_cache
after_destroy :expire_etag_cache
@@ -169,7 +170,13 @@ class Note < ActiveRecord::Base
end
def cross_reference?
- system? && matches_cross_reference_regex?
+ return unless system?
+
+ if force_cross_reference_regex_check?
+ matches_cross_reference_regex?
+ else
+ SystemNoteService.cross_reference?(note)
+ end
end
def diff_note?
@@ -208,6 +215,10 @@ class Note < ActiveRecord::Base
noteable.is_a?(PersonalSnippet)
end
+ def for_project_noteable?
+ !for_personal_snippet?
+ end
+
def skip_project_check?
for_personal_snippet?
end
@@ -378,4 +389,10 @@ class Note < ActiveRecord::Base
def set_discussion_id
self.discussion_id ||= discussion_class.discussion_id(self)
end
+
+ def force_cross_reference_regex_check?
+ return unless system?
+
+ SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action)
+ end
end
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index f89e60ad9f4..e8595b13d6d 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
- alias_method :user, :resource_owner
+ alias_attribute :user, :resource_owner
+
+ def scopes=(value)
+ if value.is_a?(Array)
+ super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
+ else
+ super
+ end
+ end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 2e824cda525..8de42ff9d2e 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -65,12 +65,18 @@ class PagesDomain < ActiveRecord::Base
def expired?
return false unless x509
+
current = Time.new
current < x509.not_before || x509.not_after < current
end
+ def expiration
+ x509&.not_after
+ end
+
def subject
return unless x509
+
x509.subject.to_s
end
@@ -98,6 +104,7 @@ class PagesDomain < ActiveRecord::Base
def validate_pages_domain
return unless domain
+
if domain.downcase.ends_with?(Settings.pages.host.downcase)
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
end
@@ -105,6 +112,7 @@ class PagesDomain < ActiveRecord::Base
def x509
return unless certificate
+
@x509 ||= OpenSSL::X509::Certificate.new(certificate)
rescue OpenSSL::X509::CertificateError
nil
@@ -112,6 +120,7 @@ class PagesDomain < ActiveRecord::Base
def pkey
return unless key
+
@pkey ||= OpenSSL::PKey::RSA.new(key)
rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
nil
diff --git a/app/models/project.rb b/app/models/project.rb
index 7185b4d44fc..894ded2a9f6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -26,7 +26,15 @@ class Project < ActiveRecord::Base
NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
- LATEST_STORAGE_VERSION = 1
+ # Hashed Storage versions handle rolling out new storage to project and dependents models:
+ # nil: legacy
+ # 1: repository
+ # 2: attachments
+ LATEST_STORAGE_VERSION = 2
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
cache_markdown_field :description, pipeline: :description
@@ -120,6 +128,7 @@ class Project < ActiveRecord::Base
has_one :mock_deployment_service
has_one :mock_monitoring_service
has_one :microsoft_teams_service
+ has_one :packagist_service
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
@@ -177,7 +186,10 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
- has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
+
+ has_one :cluster_project, class_name: 'Clusters::Project'
+ has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
+ has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -204,6 +216,7 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
+ has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
@@ -231,10 +244,8 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
- dynamic_path: true,
+ project_path: true,
length: { maximum: 255 },
- format: { with: Gitlab::PathRegex.project_path_format_regex,
- message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
@@ -354,6 +365,7 @@ class Project < ActiveRecord::Base
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
scope :excluding_project, ->(project) { where.not(id: project) }
+ scope :import_started, -> { where(import_status: 'started') }
state_machine :import_status, initial: :none do
event :import_schedule do
@@ -692,10 +704,6 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
- def github_import?
- import_type == 'github'
- end
-
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
@@ -1031,6 +1039,22 @@ class Project < ActiveRecord::Base
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
+ def fork_source
+ forked_from_project || fork_network&.root_project
+ end
+
+ def lfs_storage_project
+ @lfs_storage_project ||= begin
+ result = self
+
+ # TODO: Make this go to the fork_network root immeadiatly
+ # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+ result = result.fork_source while result&.forked?
+
+ result || self
+ end
+ end
+
def personal?
!group
end
@@ -1083,6 +1107,7 @@ class Project < ActiveRecord::Base
def hook_attrs(backward: true)
attrs = {
+ id: id,
name: name,
description: description,
web_url: web_url,
@@ -1174,6 +1199,10 @@ class Project < ActiveRecord::Base
!!repository.exists?
end
+ def wiki_repository_exists?
+ wiki.repository_exists?
+ end
+
# update visibility_level of forks
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
@@ -1394,6 +1423,19 @@ class Project < ActiveRecord::Base
end
end
+ def after_rename_repo
+ path_before_change = previous_changes['path'].first
+
+ # We need to check if project had been rolled out to move resource to hashed storage or not and decide
+ # if we need execute any take action or no-op.
+
+ unless hashed_storage?(:attachments)
+ Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ end
+
+ Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ end
+
def rename_repo_notify!
send_move_instructions(full_path_was)
expires_full_path_cache
@@ -1404,11 +1446,29 @@ class Project < ActiveRecord::Base
reload_repository!
end
- def after_rename_repo
- path_before_change = previous_changes['path'].first
+ def after_import
+ repository.after_import
+ import_finish
+ remove_import_jid
+ update_project_counter_caches
+ end
- Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
- Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ def update_project_counter_caches
+ classes = [
+ Projects::OpenIssuesCountService,
+ Projects::OpenMergeRequestsCountService
+ ]
+
+ classes.each do |klass|
+ klass.new(self).refresh_cache
+ end
+ end
+
+ def remove_import_jid
+ return unless import_jid
+
+ Gitlab::SidekiqStatus.unset(import_jid)
+ update_column(:import_jid, nil)
end
def running_or_pending_build_count(force: false)
@@ -1472,7 +1532,8 @@ class Project < ActiveRecord::Base
{ key: 'CI_PROJECT_PATH', value: full_path, public: true },
{ key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
- { key: 'CI_PROJECT_URL', value: web_url, public: true }
+ { key: 'CI_PROJECT_URL', value: web_url, public: true },
+ { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true }
]
end
@@ -1600,8 +1661,13 @@ class Project < ActiveRecord::Base
[nil, 0].include?(self.storage_version)
end
- def hashed_storage?
- self.storage_version && self.storage_version >= 1
+ # Check if Hashed Storage is enabled for the project with at least informed feature rolled out
+ #
+ # @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments)
+ def hashed_storage?(feature)
+ raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
+
+ self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
end
def renamed?
@@ -1637,7 +1703,7 @@ class Project < ActiveRecord::Base
end
def migrate_to_hashed_storage!
- return if hashed_storage?
+ return if hashed_storage?(:repository)
update!(repository_read_only: true)
@@ -1658,11 +1724,26 @@ class Project < ActiveRecord::Base
Gitlab::GlRepository.gl_repository(self, is_wiki)
end
+ def reference_counter(wiki: false)
+ Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
+ end
+
+ # Refreshes the expiration time of the associated import job ID.
+ #
+ # This method can be used by asynchronous importers to refresh the status,
+ # preventing the StuckImportJobsWorker from marking the import as failed.
+ def refresh_import_jid_expiration
+ return unless import_jid
+
+ Gitlab::SidekiqStatus
+ .set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ end
+
private
def storage
@storage ||=
- if hashed_storage?
+ if hashed_storage?(:repository)
Storage::HashedProject.new(self)
else
Storage::LegacyProject.new(self)
@@ -1676,11 +1757,11 @@ class Project < ActiveRecord::Base
end
def repo_reference_count
- Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value
+ reference_counter.value
end
def wiki_reference_count
- Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
+ reference_counter(wiki: true).value
end
def check_repository_absence!
diff --git a/app/models/project_custom_attribute.rb b/app/models/project_custom_attribute.rb
new file mode 100644
index 00000000000..3f1a7b86a82
--- /dev/null
+++ b/app/models/project_custom_attribute.rb
@@ -0,0 +1,6 @@
+class ProjectCustomAttribute < ActiveRecord::Base
+ belongs_to :project
+
+ validates :project, :key, :value, presence: true
+ validates :key, uniqueness: { scope: [:project_id] }
+end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index 1327b075858..3273f41dbd2 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -39,7 +39,7 @@ module ChatMessage
private
def message
- if state == 'opened'
+ if opened_issue?
"[#{project_link}] Issue #{state} by #{user_combined_name}"
else
"[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 976d85246a8..768f0a7472e 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -51,8 +51,10 @@ class HipchatService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
+
message = create_message(data)
return unless message.present?
+
gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index b487378edd2..1c065e1ddbd 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -176,6 +176,7 @@ class JiraService < IssueTrackerService
def test_settings
return unless client_url.present?
+
# Test settings by getting the project
jira_request { client.ServerInfo.all.attrs }
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 5c0b3338a62..bc62972dbb0 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -136,6 +136,10 @@ class KubernetesService < DeploymentService
{ pods: read_pods }
end
+ def kubeclient
+ @kubeclient ||= build_kubeclient!
+ end
+
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private
@@ -178,6 +182,7 @@ class KubernetesService < DeploymentService
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
+
[]
end
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
new file mode 100644
index 00000000000..f68a0c1a3c3
--- /dev/null
+++ b/app/models/project_services/packagist_service.rb
@@ -0,0 +1,65 @@
+class PackagistService < Service
+ include HTTParty
+
+ prop_accessor :username, :token, :server
+
+ validates :username, presence: true, if: :activated?
+ validates :token, presence: true, if: :activated?
+
+ default_value_for :push_events, true
+ default_value_for :tag_push_events, true
+
+ after_save :compose_service_hook, if: :activated?
+
+ def title
+ 'Packagist'
+ end
+
+ def description
+ 'Update your project on Packagist, the main Composer repository'
+ end
+
+ def self.to_param
+ 'packagist'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'username', placeholder: '', required: true },
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false }
+ ]
+ end
+
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ service_hook.execute(data)
+ end
+
+ def test(data)
+ begin
+ result = execute(data)
+ return { success: false, result: result[:message] } if result[:http_status] != 202
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result[:message] }
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = hook_url
+ hook.save
+ end
+
+ def hook_url
+ base_url = server.present? ? server : 'https://packagist.org'
+ "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}"
+ end
+end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 217f753f05f..fa7b3f2bcaf 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -25,7 +25,7 @@ class PrometheusService < MonitoringService
end
def description
- 'Prometheus monitoring'
+ s_('PrometheusService|Prometheus monitoring')
end
def self.to_param
@@ -38,8 +38,8 @@ class PrometheusService < MonitoringService
type: 'text',
name: 'api_url',
title: 'API URL',
- placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
- help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.',
+ placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
+ help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
required: true
}
]
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index bb7be29ef66..a0af749a93f 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -21,7 +21,7 @@ class ProjectWiki
end
delegate :empty?, to: :pages
- delegate :repository_storage_path, to: :project
+ delegate :repository_storage_path, :hashed_storage?, to: :project
def path
@project.path + '.wiki'
@@ -76,8 +76,8 @@ class ProjectWiki
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
- def pages
- wiki.pages.map { |page| WikiPage.new(self, page, true) }
+ def pages(limit: nil)
+ wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
end
# Finds a page within the repository based on a tile
@@ -135,7 +135,7 @@ class ProjectWiki
end
def repository
- @repository ||= Repository.new(full_path, @project, disk_path: disk_path)
+ @repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)
end
def default_branch
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 44a1e9ce529..2bf21cbdcc4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -15,9 +15,8 @@ class Repository
].freeze
include Gitlab::ShellAdapter
- include RepositoryMirroring
- attr_accessor :full_path, :disk_path, :project
+ attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
@@ -72,11 +71,12 @@ class Repository
end
end
- def initialize(full_path, project, disk_path: nil)
+ def initialize(full_path, project, disk_path: nil, is_wiki: false)
@full_path = full_path
@disk_path = disk_path || full_path
@project = project
@commit_cache = {}
+ @is_wiki = is_wiki
end
def ==(other)
@@ -132,7 +132,8 @@ class Repository
commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
- commits
+
+ CommitCollection.new(project, commits, ref)
end
def commits_between(from, to)
@@ -148,11 +149,14 @@ class Repository
end
raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled|
- if is_enabled
- find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- else
- find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- end
+ commits =
+ if is_enabled
+ find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
+ else
+ find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
+ end
+
+ CommitCollection.new(project, commits, ref)
end
end
@@ -213,11 +217,7 @@ class Repository
def branch_exists?(branch_name)
return false unless raw_repository
- @branch_exists_memo ||= Hash.new do |hash, key|
- hash[key] = raw_repository.branch_exists?(key)
- end
-
- @branch_exists_memo[branch_name]
+ branch_names.include?(branch_name)
end
def ref_exists?(ref)
@@ -242,6 +242,7 @@ class Repository
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex
raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
+
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
end
@@ -473,6 +474,11 @@ class Repository
nil
end
+ # items is an Array like: [[oid, path], [oid1, path1]]
+ def blobs_at(items)
+ raw_repository.batch_blobs(items).map { |blob| Blob.decorate(blob, project) }
+ end
+
def root_ref
if raw_repository
raw_repository.root_ref
@@ -662,6 +668,7 @@ class Repository
def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
+
result = n.match(/\A#{name}-([0-9]+)\z/)
result[1].to_i if result
end.compact
@@ -906,13 +913,13 @@ class Repository
branch = Gitlab::Git::Branch.find(self, branch_or_name)
if branch
- root_ref_sha = commit(root_ref).sha
- same_head = branch.target == root_ref_sha
+ @root_ref_sha ||= commit(root_ref).sha
+ same_head = branch.target == @root_ref_sha
merged =
if pre_loaded_merged_branches
pre_loaded_merged_branches.include?(branch.name)
else
- ancestor?(branch.target, root_ref_sha)
+ ancestor?(branch.target, @root_ref_sha)
end
!same_head && merged
@@ -965,25 +972,16 @@ class Repository
run_git(args).first.lines.map(&:strip)
end
- def add_remote(name, url)
- raw_repository.remote_add(name, url)
- rescue Rugged::ConfigError
- raw_repository.remote_update(name, url: url)
+ def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
+ gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
- def remove_remote(name)
- raw_repository.remote_delete(name)
- true
- rescue Rugged::ConfigError
- false
+ def fetch_source_branch!(source_repository, source_branch, local_ref)
+ raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
- def fetch_remote(remote, forced: false, no_tags: false)
- gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags)
- end
-
- def fetch_source_branch(source_repository, source_branch, local_ref)
- raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
+ def remote_exists?(name)
+ raw_repository.remote_exists?(name)
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
@@ -999,10 +997,6 @@ class Repository
raw_repository.ls_files(actual_ref)
end
- def gitattribute(path, name)
- raw_repository.attributes(path)[name]
- end
-
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
@@ -1071,6 +1065,10 @@ class Repository
blob_data_at(sha, path)
end
+ def fetch_ref(source_repository, source_ref:, target_ref:)
+ raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
@@ -1141,7 +1139,7 @@ class Repository
end
def initialize_raw_repository
- Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
+ Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
diff --git a/app/models/service.rb b/app/models/service.rb
index 6b64079215f..fdd2605e3e3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -238,6 +238,7 @@ class Service < ActiveRecord::Base
kubernetes
mattermost_slash_commands
mattermost
+ packagist
pipelines_email
pivotaltracker
prometheus
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 1f9f8d7286b..29035480371 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -1,4 +1,14 @@
class SystemNoteMetadata < ActiveRecord::Base
+ # These notes's action text might contain a reference that is external.
+ # We should always force a deep validation upon references that are found
+ # in this note type.
+ # Other notes can always be safely shown as all its references are
+ # in the same project (i.e. with the same permissions)
+ TYPES_WITH_CROSS_REFERENCES = %w[
+ commit cross_reference
+ close duplicate
+ ].freeze
+
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
diff --git a/app/models/user.rb b/app/models/user.rb
index 9459b6d4fa4..f98165754ca 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -21,8 +21,8 @@ class User < ActiveRecord::Base
ignore_column :external_email
ignore_column :email_provider
+ ignore_column :authentication_token
- add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token
@@ -146,7 +146,7 @@ class User < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
- dynamic_path: true,
+ user_path: true,
presence: true,
uniqueness: { case_sensitive: false }
@@ -163,12 +163,14 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed?
- before_save :ensure_authentication_token, :ensure_incoming_email_token
- before_save :ensure_user_rights_and_limits, if: :external_changed?
+ before_save :ensure_incoming_email_token
+ before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
+ after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
+ after_destroy :remove_key_cache
after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
@@ -185,8 +187,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files]
- alias_attribute :private_token, :authentication_token
-
delegate :path, to: :namespace, allow_nil: true, prefix: true
state_machine :state, initial: :active do
@@ -268,18 +268,22 @@ class User < ActiveRecord::Base
end
end
+ def for_github_id(id)
+ joins(:identities).merge(Identity.with_extern_uid(:github, id))
+ end
+
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email)
- sql = 'SELECT *
- FROM users
- WHERE id IN (
- SELECT id FROM users WHERE email = :email
- UNION
- SELECT emails.user_id FROM emails WHERE email = :email
- )
- LIMIT 1;'
+ by_any_email(email).take
+ end
- User.find_by_sql([sql, { email: email }]).first
+ # Returns a relation containing all the users for the given Email address
+ def by_any_email(email)
+ users = where(email: email)
+ emails = joins(:emails).where(emails: { email: email })
+ union = Gitlab::SQL::Union.new([users, emails])
+
+ from("(#{union.to_sql}) #{table_name}")
end
def filter(filter_name)
@@ -441,6 +445,10 @@ class User < ActiveRecord::Base
skip_confirmation! if bool
end
+ def skip_reconfirmation=(bool)
+ skip_reconfirmation! if bool
+ end
+
def generate_reset_token
@reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
@@ -620,7 +628,9 @@ class User < ActiveRecord::Base
end
def require_ssh_key?
- keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
+ count = Users::KeysCountService.new(self).count
+
+ count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
end
def require_password_creation?
@@ -873,11 +883,19 @@ class User < ActiveRecord::Base
end
end
+ def username_changed_hook
+ system_hook_service.execute_hooks_for(self, :rename)
+ end
+
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def remove_key_cache
+ Users::KeysCountService.new(self).delete_cache
+ end
+
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params)
@@ -913,7 +931,16 @@ class User < ActiveRecord::Base
end
def manageable_namespaces
- @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
+ @manageable_namespaces ||= [namespace] + manageable_groups
+ end
+
+ def manageable_groups
+ union = Gitlab::SQL::Union.new([owned_groups.select(:id),
+ masters_groups.select(:id)])
+ arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
+ owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
+
+ Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
def namespaces
@@ -1102,6 +1129,7 @@ class User < ActiveRecord::Base
# override, from Devise::Validatable
def password_required?
return false if internal?
+
super
end
@@ -1119,6 +1147,7 @@ class User < ActiveRecord::Base
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
+
devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
end
@@ -1136,8 +1165,9 @@ class User < ActiveRecord::Base
self.can_create_group = false
self.projects_limit = 0
else
- self.can_create_group = gitlab_config.default_can_create_group
- self.projects_limit = current_application_settings.default_projects_limit
+ # Only revert these back to the default if they weren't specifically changed in this update.
+ self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
+ self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 5f710961f95..bdfef677ef3 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -127,19 +127,24 @@ class WikiPage
@version ||= @page.version
end
- # Returns an array of Gitlab Commit instances.
- def versions
+ def versions(options = {})
return [] unless persisted?
- wiki.wiki.page_versions(@page.path)
+ wiki.wiki.page_versions(@page.path, options)
end
- def commit
- versions.first
+ def count_versions
+ return [] unless persisted?
+
+ wiki.wiki.count_page_versions(@page.path)
+ end
+
+ def last_version
+ @last_version ||= versions(limit: 1).first
end
def last_commit_sha
- commit&.sha
+ last_version&.sha
end
# Returns the Date that this latest version was
@@ -151,7 +156,7 @@ class WikiPage
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
- @page.historical? && versions.first.sha != version.sha
+ @page.historical? && last_version.sha != version.sha
end
# Returns boolean True or False if this instance
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 984e5482288..1ab391a5a9d 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -10,6 +10,15 @@ module Ci
end
end
- rule { protected_ref }.prevent :update_build
+ condition(:owner_of_job) do
+ can?(:developer_access) && @subject.triggered_by?(@user)
+ end
+
+ rule { protected_ref }.policy do
+ prevent :update_build
+ prevent :erase_build
+ end
+
+ rule { can?(:master_access) | owner_of_job }.enable :erase_build
end
end
diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index e77173ea6e1..1f7c13072b9 100644
--- a/app/policies/gcp/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -1,8 +1,8 @@
-module Gcp
+module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
- delegate { @subject.project }
+ delegate { cluster.project }
rule { can?(:master_access) }.policy do
enable :update_cluster
diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index f7908f92a37..01cb59d0d44 100644
--- a/app/presenters/gcp/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -1,9 +1,9 @@
-module Gcp
+module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
presents :cluster
def gke_cluster_url
- "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}"
+ "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
end
end
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index 56f173e5a27..ad039a2623d 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -3,10 +3,6 @@ class BlobEntity < Grape::Entity
expose :id, :path, :name, :mode
- expose :last_commit do |blob|
- request.project.repository.last_commit_for_path(blob.commit_id, blob.path)
- end
-
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 8c89eea607f..69d46f5ec14 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity
expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
- expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build|
+ expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
new file mode 100644
index 00000000000..3f9a275ad08
--- /dev/null
+++ b/app/serializers/cluster_application_entity.rb
@@ -0,0 +1,5 @@
+class ClusterApplicationEntity < Grape::Entity
+ expose :name
+ expose :status_name, as: :status
+ expose :status_reason
+end
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index 08a113c4d8a..7e5b0997878 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -3,4 +3,5 @@ class ClusterEntity < Grape::Entity
expose :status_name, as: :status
expose :status_reason
+ expose :applications, using: ClusterApplicationEntity
end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
index 2c87202a105..2e13c1501e7 100644
--- a/app/serializers/cluster_serializer.rb
+++ b/app/serializers/cluster_serializer.rb
@@ -2,6 +2,6 @@ class ClusterSerializer < BaseSerializer
entity ClusterEntity
def represent_status(resource)
- represent(resource, { only: [:status, :status_reason] })
+ represent(resource, { only: [:status, :status_reason, :applications] })
end
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 61c7a428745..3b5a4fd4f79 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -1,20 +1,16 @@
class IssuableEntity < Grape::Entity
+ include RequestAwareEntity
+
expose :id
expose :iid
expose :author_id
expose :description
expose :lock_version
expose :milestone_id
- expose :state
expose :title
expose :updated_by_id
expose :created_at
expose :updated_at
- expose :deleted_at
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
end
diff --git a/app/serializers/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_entity.rb
new file mode 100644
index 00000000000..ff23d8bf0c7
--- /dev/null
+++ b/app/serializers/issuable_sidebar_entity.rb
@@ -0,0 +1,16 @@
+class IssuableSidebarEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :participants, using: ::API::Entities::UserBasic do |issuable|
+ issuable.participants(request.current_user)
+ end
+
+ expose :subscribed do |issuable|
+ issuable.subscribed?(request.current_user, issuable.project)
+ end
+
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 10d3ad0214b..9d52b8d9752 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -1,7 +1,8 @@
class IssueEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
- expose :branch_name
+ expose :state
+ expose :deleted_at
expose :confidential
expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
index 4fff54a9126..2555595379b 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -1,3 +1,16 @@
class IssueSerializer < BaseSerializer
- entity IssueEntity
+ # This overrided method takes care of which entity should be used
+ # to serialize the `issue` based on `basic` key in `opts` param.
+ # Hence, `entity` doesn't need to be declared on the class scope.
+ def represent(merge_request, opts = {})
+ entity =
+ case opts[:serializer]
+ when 'sidebar'
+ IssueSidebarEntity
+ else
+ IssueEntity
+ end
+
+ super(merge_request, opts, entity)
+ end
end
diff --git a/app/serializers/issue_sidebar_entity.rb b/app/serializers/issue_sidebar_entity.rb
new file mode 100644
index 00000000000..6c823dbfe95
--- /dev/null
+++ b/app/serializers/issue_sidebar_entity.rb
@@ -0,0 +1,3 @@
+class IssueSidebarEntity < IssuableSidebarEntity
+ expose :assignees, using: API::Entities::UserBasic
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 8461f158bb5..d54a6516aed 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -1,11 +1,7 @@
-class MergeRequestBasicEntity < Grape::Entity
+class MergeRequestBasicEntity < IssuableSidebarEntity
expose :assignee_id
expose :merge_status
expose :merge_error
expose :state
expose :source_branch_exists?, as: :source_branch_exists
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 297a459e394..b53a49fe59e 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -1,6 +1,8 @@
class MergeRequestEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
+ expose :state
+ expose :deleted_at
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
expose :merge_error
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index f67034ce47a..e9d98d8baca 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer
# to serialize the `merge_request` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
- entity = opts[:basic] ? MergeRequestBasicEntity : MergeRequestEntity
+ entity =
+ case opts[:serializer]
+ when 'basic', 'sidebar'
+ MergeRequestBasicEntity
+ else
+ MergeRequestEntity
+ end
+
super(merge_request, opts, entity)
end
end
diff --git a/app/serializers/time_trackable_entity.rb b/app/serializers/time_trackable_entity.rb
new file mode 100644
index 00000000000..e81cd7bec72
--- /dev/null
+++ b/app/serializers/time_trackable_entity.rb
@@ -0,0 +1,11 @@
+module TimeTrackableEntity
+ extend ActiveSupport::Concern
+ extend Grape
+
+ included do
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+end
diff --git a/app/serializers/tree_entity.rb b/app/serializers/tree_entity.rb
index 555e5cf83bd..9f1b485347f 100644
--- a/app/serializers/tree_entity.rb
+++ b/app/serializers/tree_entity.rb
@@ -3,10 +3,6 @@ class TreeEntity < Grape::Entity
expose :id, :path, :name, :mode
- expose :last_commit do |tree|
- request.project.repository.last_commit_for_path(tree.commit_id, tree.path)
- end
-
expose :icon do |tree|
IconsHelper.file_type_icon_class('folder', tree.mode, tree.name)
end
diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb
index 69702ae1493..496f070ddbd 100644
--- a/app/serializers/tree_root_entity.rb
+++ b/app/serializers/tree_root_entity.rb
@@ -18,4 +18,8 @@ class TreeRootEntity < Grape::Entity
project_tree_path(request.project, File.join(request.ref, parent_tree_path))
end
+
+ expose :last_commit_path do |tree|
+ logs_file_project_ref_path(request.project, request.ref, tree.path)
+ end
end
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index 9c00ea789ec..46e19230328 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym)
required_scopes.any? do |scope|
- if scope.respond_to?(:sufficient?)
- scope.sufficient?(token_scopes, request)
- else
- API::Scope.new(scope).sufficient?(token_scopes, request)
- end
+ scope = API::Scope.new(scope) unless scope.is_a?(API::Scope)
+ scope.sufficient?(token_scopes, request)
end
end
end
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
new file mode 100644
index 00000000000..35d45f25a71
--- /dev/null
+++ b/app/services/applications/create_service.rb
@@ -0,0 +1,13 @@
+module Applications
+ class CreateService
+ def initialize(current_user, params)
+ @current_user = current_user
+ @params = params
+ @ip_address = @params.delete(:ip_address)
+ end
+
+ def execute(request = nil)
+ Doorkeeper::Application.create(@params)
+ end
+ end
+end
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
new file mode 100644
index 00000000000..99cc9a196e6
--- /dev/null
+++ b/app/services/base_count_service.rb
@@ -0,0 +1,34 @@
+# Base class for services that count a single resource such as the number of
+# issues for a project.
+class BaseCountService
+ def relation_for_count
+ raise(
+ NotImplementedError,
+ '"relation_for_count" must be implemented and return an ActiveRecord::Relation'
+ )
+ end
+
+ def count
+ Rails.cache.fetch(cache_key, raw: raw?) { uncached_count }.to_i
+ end
+
+ def refresh_cache
+ Rails.cache.write(cache_key, uncached_count, raw: raw?)
+ end
+
+ def uncached_count
+ relation_for_count.count
+ end
+
+ def delete_cache
+ Rails.cache.delete(cache_key)
+ end
+
+ def raw?
+ false
+ end
+
+ def cache_key
+ raise NotImplementedError, 'cache_key must be implemented and return a String'
+ end
+end
diff --git a/app/services/base_renderer.rb b/app/services/base_renderer.rb
new file mode 100644
index 00000000000..d6e30bd7008
--- /dev/null
+++ b/app/services/base_renderer.rb
@@ -0,0 +1,7 @@
+class BaseRenderer
+ attr_reader :current_user
+
+ def initialize(current_user = nil)
+ @current_user = current_user
+ end
+end
diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb
deleted file mode 100644
index f7ee0e468e2..00000000000
--- a/app/services/ci/create_cluster_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Ci
- class CreateClusterService < BaseService
- def execute(access_token)
- params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE
-
- cluster_params =
- params.merge(user: current_user,
- gcp_token: access_token)
-
- project.create_cluster(cluster_params).tap do |cluster|
- ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
- end
- end
- end
-end
diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb
new file mode 100644
index 00000000000..dc2f49e8db1
--- /dev/null
+++ b/app/services/ci/ensure_stage_service.rb
@@ -0,0 +1,39 @@
+module Ci
+ ##
+ # We call this service everytime we persist a CI/CD job.
+ #
+ # In most cases a job should already have a stage assigned, but in cases it
+ # doesn't have we need to either find existing one or create a brand new
+ # stage.
+ #
+ class EnsureStageService < BaseService
+ def execute(build)
+ @build = build
+
+ return if build.stage_id.present?
+ return if build.invalid?
+
+ ensure_stage.tap do |stage|
+ build.stage_id = stage.id
+
+ yield stage if block_given?
+ end
+ end
+
+ private
+
+ def ensure_stage
+ find_stage || create_stage
+ end
+
+ def find_stage
+ @build.pipeline.stages.find_by(name: @build.stage)
+ end
+
+ def create_stage
+ Ci::Stage.create!(name: @build.stage,
+ pipeline: @build.pipeline,
+ project: @build.project)
+ end
+ end
+end
diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb
deleted file mode 100644
index 0b68e4d6ea9..00000000000
--- a/app/services/ci/fetch_gcp_operation_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Ci
- class FetchGcpOperationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- operation = api_client.projects_zones_operations(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_operation_id)
-
- yield(operation) if block_given?
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb
index 44da87cb00c..e73c6ad6780 100644
--- a/app/services/ci/fetch_kubernetes_token_service.rb
+++ b/app/services/ci/fetch_kubernetes_token_service.rb
@@ -34,6 +34,7 @@ module Ci
kubeclient.get_secrets.as_json
rescue KubeException => err
raise err unless err.error_code == 404
+
[]
end
diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb
deleted file mode 100644
index 347875c5697..00000000000
--- a/app/services/ci/finalize_cluster_creation_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Ci
- class FinalizeClusterCreationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- gke_cluster = api_client.projects_zones_clusters_get(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- endpoint = gke_cluster.endpoint
- api_url = 'https://' + endpoint
- ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate)
- username = gke_cluster.master_auth.username
- password = gke_cluster.master_auth.password
-
- kubernetes_token = Ci::FetchKubernetesTokenService.new(
- api_url, ca_cert, username, password).execute
-
- unless kubernetes_token
- return cluster.make_errored!('Failed to get a default token of kubernetes')
- end
-
- Ci::IntegrateClusterService.new.execute(
- cluster, endpoint, ca_cert, kubernetes_token, username, password)
- end
- end
-end
diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb
deleted file mode 100644
index d123ce8d26b..00000000000
--- a/app/services/ci/integrate_cluster_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Ci
- class IntegrateClusterService
- def execute(cluster, endpoint, ca_cert, token, username, password)
- Gcp::Cluster.transaction do
- cluster.update!(
- enabled: true,
- endpoint: endpoint,
- ca_cert: ca_cert,
- kubernetes_token: token,
- username: username,
- password: password,
- service: cluster.project.find_or_initialize_service('kubernetes'),
- status_event: :make_created)
-
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: ca_cert,
- namespace: cluster.project_namespace,
- token: token)
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 120af8c1e61..a9813d774bb 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -1,5 +1,7 @@
module Ci
class PipelineTriggerService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
@@ -26,9 +28,9 @@ module Ci
end
def trigger_from_token
- return @trigger if defined?(@trigger)
-
- @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ strong_memoize(:trigger) do
+ Ci::Trigger.find_by_token(params[:token].to_s)
+ end
end
def create_pipeline_variables!(pipeline)
diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb
deleted file mode 100644
index 52d80b01813..00000000000
--- a/app/services/ci/provision_cluster_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- class ProvisionClusterService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- operation = api_client.projects_zones_clusters_create(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name,
- cluster.gcp_cluster_size,
- machine_type: cluster.gcp_machine_type)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- unless operation.status == 'RUNNING' || operation.status == 'PENDING'
- return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}")
- end
-
- cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link)
-
- unless cluster.gcp_operation_id
- return cluster.make_errored!('Can not find operation_id from self_link')
- end
-
- if cluster.make_creating
- WaitForClusterCreationWorker.perform_in(
- WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id)
- else
- return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}")
- end
- end
- end
-end
diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb
deleted file mode 100644
index 70d88fca660..00000000000
--- a/app/services/ci/update_cluster_service.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Ci
- class UpdateClusterService < BaseService
- def execute(cluster)
- Gcp::Cluster.transaction do
- cluster.update!(params)
-
- if params['enabled'] == 'true'
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: cluster.ca_cert,
- namespace: cluster.project_namespace,
- token: cluster.kubernetes_token)
- else
- cluster.service.update!(active: false)
- end
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.errors.add(:base, e.message)
- end
- end
-end
diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb
new file mode 100644
index 00000000000..9a4ce31cb39
--- /dev/null
+++ b/app/services/clusters/applications/base_helm_service.rb
@@ -0,0 +1,29 @@
+module Clusters
+ module Applications
+ class BaseHelmService
+ attr_accessor :app
+
+ def initialize(app)
+ @app = app
+ end
+
+ protected
+
+ def cluster
+ app.cluster
+ end
+
+ def kubeclient
+ cluster.kubeclient
+ end
+
+ def helm_api
+ @helm_api ||= Gitlab::Kubernetes::Helm.new(kubeclient)
+ end
+
+ def install_command
+ @install_command ||= app.install_command
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
new file mode 100644
index 00000000000..bde090eaeec
--- /dev/null
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -0,0 +1,65 @@
+module Clusters
+ module Applications
+ class CheckInstallationProgressService < BaseHelmService
+ def execute
+ return unless app.installing?
+
+ case installation_phase
+ when Gitlab::Kubernetes::Pod::SUCCEEDED
+ on_success
+ when Gitlab::Kubernetes::Pod::FAILED
+ on_failed
+ else
+ check_timeout
+ end
+ rescue KubeException => ke
+ app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored?
+ end
+
+ private
+
+ def on_success
+ app.make_installed!
+ ensure
+ remove_installation_pod
+ end
+
+ def on_failed
+ app.make_errored!(installation_errors || 'Installation silently failed')
+ ensure
+ remove_installation_pod
+ end
+
+ def check_timeout
+ if timeouted?
+ begin
+ app.make_errored!('Installation timeouted')
+ ensure
+ remove_installation_pod
+ end
+ else
+ ClusterWaitForAppInstallationWorker.perform_in(
+ ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
+ end
+ end
+
+ def timeouted?
+ Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
+ end
+
+ def remove_installation_pod
+ helm_api.delete_installation_pod!(install_command.pod_name)
+ rescue
+ # no-op
+ end
+
+ def installation_phase
+ helm_api.installation_status(install_command.pod_name)
+ end
+
+ def installation_errors
+ helm_api.installation_log(install_command.pod_name)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
new file mode 100644
index 00000000000..8ceeec687cd
--- /dev/null
+++ b/app/services/clusters/applications/install_service.rb
@@ -0,0 +1,21 @@
+module Clusters
+ module Applications
+ class InstallService < BaseHelmService
+ def execute
+ return unless app.scheduled?
+
+ begin
+ app.make_installing!
+ helm_api.install(install_command)
+
+ ClusterWaitForAppInstallationWorker.perform_in(
+ ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
+ rescue KubeException => ke
+ app.make_errored!("Kubernetes error: #{ke.message}")
+ rescue StandardError
+ app.make_errored!("Can't start installation process")
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb
new file mode 100644
index 00000000000..eb8caa68ef7
--- /dev/null
+++ b/app/services/clusters/applications/schedule_installation_service.rb
@@ -0,0 +1,22 @@
+module Clusters
+ module Applications
+ class ScheduleInstallationService < ::BaseService
+ def execute
+ application_class.find_or_create_by!(cluster: cluster).try do |application|
+ application.make_scheduled!
+ ClusterInstallAppWorker.perform_async(application.name, application.id)
+ end
+ end
+
+ private
+
+ def application_class
+ params[:application_class]
+ end
+
+ def cluster
+ params[:cluster]
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
new file mode 100644
index 00000000000..1d407739b21
--- /dev/null
+++ b/app/services/clusters/create_service.rb
@@ -0,0 +1,29 @@
+module Clusters
+ class CreateService < BaseService
+ attr_reader :access_token
+
+ def execute(access_token)
+ @access_token = access_token
+
+ create_cluster.tap do |cluster|
+ ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
+ end
+ end
+
+ private
+
+ def create_cluster
+ Clusters::Cluster.create(cluster_params)
+ end
+
+ def cluster_params
+ return @cluster_params if defined?(@cluster_params)
+
+ params[:provider_gcp_attributes].try do |provider|
+ provider[:access_token] = access_token
+ end
+
+ @cluster_params = params.merge(user: current_user, projects: [project])
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb
new file mode 100644
index 00000000000..a4cd3ca5c11
--- /dev/null
+++ b/app/services/clusters/gcp/fetch_operation_service.rb
@@ -0,0 +1,16 @@
+module Clusters
+ module Gcp
+ class FetchOperationService
+ def execute(provider)
+ operation = provider.api_client.projects_zones_operations(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.operation_id)
+
+ yield(operation) if block_given?
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
new file mode 100644
index 00000000000..cea56f4e849
--- /dev/null
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -0,0 +1,56 @@
+module Clusters
+ module Gcp
+ class FinalizeCreationService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ configure_provider
+ configure_kubernetes
+
+ cluster.save!
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ rescue ActiveRecord::RecordInvalid => e
+ provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
+ end
+
+ private
+
+ def configure_provider
+ provider.endpoint = gke_cluster.endpoint
+ provider.status_event = :make_created
+ end
+
+ def configure_kubernetes
+ cluster.platform_type = :kubernetes
+ cluster.build_platform_kubernetes(
+ api_url: 'https://' + gke_cluster.endpoint,
+ ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ username: gke_cluster.master_auth.username,
+ password: gke_cluster.master_auth.password,
+ token: request_kuberenetes_token)
+ end
+
+ def request_kuberenetes_token
+ Ci::FetchKubernetesTokenService.new(
+ 'https://' + gke_cluster.endpoint,
+ Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ gke_cluster.master_auth.username,
+ gke_cluster.master_auth.password).execute
+ end
+
+ def gke_cluster
+ @gke_cluster ||= provider.api_client.projects_zones_clusters_get(
+ provider.gcp_project_id,
+ provider.zone,
+ cluster.name)
+ end
+
+ def cluster
+ @cluster ||= provider.cluster
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb
new file mode 100644
index 00000000000..8beea5a8cfb
--- /dev/null
+++ b/app/services/clusters/gcp/provision_service.rb
@@ -0,0 +1,47 @@
+module Clusters
+ module Gcp
+ class ProvisionService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ get_operation_id do |operation_id|
+ if provider.make_creating(operation_id)
+ WaitForClusterCreationWorker.perform_in(
+ Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL,
+ provider.cluster_id)
+ else
+ provider.make_errored!("Failed to update provider record; #{provider.errors}")
+ end
+ end
+ end
+
+ private
+
+ def get_operation_id
+ operation = provider.api_client.projects_zones_clusters_create(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.cluster.name,
+ provider.num_nodes,
+ machine_type: provider.machine_type)
+
+ unless operation.status == 'PENDING' || operation.status == 'RUNNING'
+ return provider.make_errored!("Operation status is unexpected; #{operation.status_message}")
+ end
+
+ operation_id = provider.api_client.parse_operation_id(operation.self_link)
+
+ unless operation_id
+ return provider.make_errored!('Can not find operation_id from self_link')
+ end
+
+ yield(operation_id)
+
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
new file mode 100644
index 00000000000..bc33756f27c
--- /dev/null
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -0,0 +1,48 @@
+module Clusters
+ module Gcp
+ class VerifyProvisionStatusService
+ attr_reader :provider
+
+ INITIAL_INTERVAL = 2.minutes
+ EAGER_INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def execute(provider)
+ @provider = provider
+
+ request_operation do |operation|
+ case operation.status
+ when 'PENDING', 'RUNNING'
+ continue_creation(operation)
+ when 'DONE'
+ finalize_creation
+ else
+ return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ end
+ end
+ end
+
+ private
+
+ def continue_creation(operation)
+ if elapsed_time_from_creation(operation) < TIMEOUT
+ WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
+ else
+ provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
+ end
+ end
+
+ def elapsed_time_from_creation(operation)
+ Time.now.utc - operation.start_time.to_time.utc
+ end
+
+ def finalize_creation
+ Clusters::Gcp::FinalizeCreationService.new.execute(provider)
+ end
+
+ def request_operation(&blk)
+ Clusters::Gcp::FetchOperationService.new.execute(provider, &blk)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb
new file mode 100644
index 00000000000..989218e32a2
--- /dev/null
+++ b/app/services/clusters/update_service.rb
@@ -0,0 +1,7 @@
+module Clusters
+ class UpdateService < BaseService
+ def execute(cluster)
+ cluster.update(params)
+ end
+ end
+end
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index 077268b2388..cb235a85daf 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -13,7 +13,7 @@ class DeleteMergedBranchesService < BaseService
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
# Prevent deletion of protected branches
- branches = branches.reject { |branch| project.protected_for?(branch) }
+ branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) }
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
diff --git a/app/services/events/render_service.rb b/app/services/events/render_service.rb
new file mode 100644
index 00000000000..0b62d8aedf1
--- /dev/null
+++ b/app/services/events/render_service.rb
@@ -0,0 +1,21 @@
+module Events
+ class RenderService < BaseRenderer
+ def execute(events, atom_request: false)
+ events.map(&:note).compact.group_by(&:project).each do |project, notes|
+ render_notes(notes, project, atom_request)
+ end
+ end
+
+ private
+
+ def render_notes(notes, project, atom_request)
+ Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request))
+ end
+
+ def render_options(atom_request)
+ return {} unless atom_request
+
+ { only_path: false, xhtml: true }
+ end
+ end
+end
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
new file mode 100644
index 00000000000..92eaa5d5115
--- /dev/null
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -0,0 +1,81 @@
+module Issuable
+ class CommonSystemNotesService < ::BaseService
+ attr_reader :issuable
+
+ def execute(issuable, old_labels)
+ @issuable = issuable
+
+ if issuable.previous_changes.include?('title')
+ create_title_change_note(issuable.previous_changes['title'].first)
+ end
+
+ handle_description_change_note
+
+ handle_time_tracking_note if issuable.is_a?(TimeTrackable)
+ create_labels_note(old_labels) if issuable.labels != old_labels
+ create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
+ create_milestone_note if issuable.previous_changes.include?('milestone_id')
+ end
+
+ private
+
+ def handle_time_tracking_note
+ if issuable.previous_changes.include?('time_estimate')
+ create_time_estimate_note
+ end
+
+ if issuable.time_spent?
+ create_time_spent_note
+ end
+ end
+
+ def handle_description_change_note
+ if issuable.previous_changes.include?('description')
+ if issuable.tasks? && issuable.updated_tasks.any?
+ create_task_status_note
+ else
+ # TODO: Show this note if non-task content was modified.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
+ create_description_change_note
+ end
+ end
+ end
+
+ def create_labels_note(old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
+ SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels)
+ end
+
+ def create_title_change_note(old_title)
+ SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
+ end
+
+ def create_description_change_note
+ SystemNoteService.change_description(issuable, issuable.project, current_user)
+ end
+
+ def create_task_status_note
+ issuable.updated_tasks.each do |task|
+ SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
+ end
+ end
+
+ def create_time_estimate_note
+ SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
+ end
+
+ def create_time_spent_note
+ SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
+ end
+
+ def create_milestone_note
+ SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
+ end
+
+ def create_discussion_lock_note
+ SystemNoteService.discussion_lock(issuable, current_user)
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index d61a342ebad..39a7299ff60 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -1,56 +1,10 @@
class IssuableBaseService < BaseService
private
- def create_milestone_note(issuable)
- SystemNoteService.change_milestone(
- issuable, issuable.project, current_user, issuable.milestone)
- end
-
- def create_labels_note(issuable, old_labels)
- added_labels = issuable.labels - old_labels
- removed_labels = old_labels - issuable.labels
-
- SystemNoteService.change_label(
- issuable, issuable.project, current_user, added_labels, removed_labels)
- end
-
- def create_title_change_note(issuable, old_title)
- SystemNoteService.change_title(
- issuable, issuable.project, current_user, old_title)
- end
-
- def create_description_change_note(issuable)
- SystemNoteService.change_description(issuable, issuable.project, current_user)
- end
-
- def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
- SystemNoteService.change_branch(
- issuable, issuable.project, current_user, branch_type,
- old_branch, new_branch)
- end
-
- def create_task_status_note(issuable)
- issuable.updated_tasks.each do |task|
- SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
- end
- end
-
- def create_time_estimate_note(issuable)
- SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
- end
-
- def create_time_spent_note(issuable)
- SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
- end
-
- def create_discussion_lock_note(issuable)
- SystemNoteService.discussion_lock(issuable, current_user)
- end
-
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
- unless can?(current_user, ability_name, project)
+ unless can?(current_user, ability_name, issuable)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
@@ -218,6 +172,7 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
old_assignees = issuable.assignees.to_a
+ old_total_time_spent = issuable.total_time_spent
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
@@ -233,15 +188,14 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
- update_project_counters = issuable.update_project_counter_caches?
+ update_project_counters = issuable.project && update_project_counter_caches?(issuable)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
- handle_common_system_notes(issuable, old_labels: old_labels)
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels)
end
- change_discussion_lock(issuable)
handle_changes(
issuable,
old_labels: old_labels,
@@ -255,7 +209,12 @@ class IssuableBaseService < BaseService
invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
- execute_hooks(issuable, 'update', old_labels: old_labels, old_assignees: old_assignees)
+ execute_hooks(
+ issuable,
+ 'update',
+ old_labels: old_labels,
+ old_assignees: old_assignees,
+ old_total_time_spent: old_total_time_spent)
issuable.update_project_counter_caches if update_project_counters
end
@@ -300,12 +259,6 @@ class IssuableBaseService < BaseService
end
end
- def change_discussion_lock(issuable)
- if issuable.previous_changes.include?('discussion_locked')
- create_discussion_lock_note(issuable)
- end
- end
-
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
@@ -328,35 +281,21 @@ class IssuableBaseService < BaseService
attrs_changed || labels_changed || assignees_changed
end
- def handle_common_system_notes(issuable, old_labels: [])
- if issuable.previous_changes.include?('title')
- create_title_change_note(issuable, issuable.previous_changes['title'].first)
- end
-
- if issuable.previous_changes.include?('description')
- if issuable.tasks? && issuable.updated_tasks.any?
- create_task_status_note(issuable)
- else
- # TODO: Show this note if non-task content was modified.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
- create_description_change_note(issuable)
- end
- end
-
- if issuable.previous_changes.include?('time_estimate')
- create_time_estimate_note(issuable)
+ def invalidate_cache_counts(issuable, users: [])
+ users.each do |user|
+ user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
+ end
- if issuable.time_spent?
- create_time_spent_note(issuable)
- end
+ # override if needed
+ def handle_changes(issuable, options)
+ end
- create_labels_note(issuable, old_labels) if issuable.labels != old_labels
+ # override if needed
+ def execute_hooks(issuable, action = 'open', params = {})
end
- def invalidate_cache_counts(issuable, users: [])
- users.each do |user|
- user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
- end
+ def update_project_counter_caches?(issuable)
+ issuable.state_changed?
end
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 735257c4779..0f711bcc3cf 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,7 +1,7 @@
module Issues
class BaseService < ::IssuableBaseService
- def hook_data(issue, action, old_labels: [], old_assignees: [])
- hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
+ def hook_data(issue, action, old_labels: [], old_assignees: [], old_total_time_spent: nil)
+ hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hook_data[:object_attributes][:action] = action
hook_data
@@ -22,8 +22,8 @@ module Issues
issue, issue.project, current_user, old_assignees)
end
- def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [])
- issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees)
+ def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [], old_total_time_spent: nil)
+ issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope)
@@ -45,5 +45,9 @@ module Issues
params.delete(:assignee_ids)
end
end
+
+ def update_project_counter_caches?(issue)
+ super || issue.confidential_changed?
+ end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index e0339ddf9bb..1b7b5927c5a 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -27,10 +27,6 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
- if issue.previous_changes.include?('milestone_id')
- create_milestone_note(issue)
- end
-
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees)
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
index 545832d0bd4..f78791932a7 100644
--- a/app/services/keys/base_service.rb
+++ b/app/services/keys/base_service.rb
@@ -4,6 +4,7 @@ module Keys
def initialize(user, params)
@user, @params = user, params
+ @ip_address = @params.delete(:ip_address)
end
def notification_service
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index 43b539ded53..997d247be46 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -19,6 +19,7 @@ module Labels
# We skipped validations during creation. Let's run them now, after deleting conflicting labels
raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid?
+
new_label
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 112606a82d7..d3938b065bc 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -18,8 +18,8 @@ module MergeRequests
super if changed_title
end
- def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [])
- hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
+ def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
+ hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hook_data[:object_attributes][:action] = action
if old_rev && !Gitlab::Git.blank_ref?(old_rev)
hook_data[:object_attributes][:oldrev] = old_rev
@@ -28,9 +28,9 @@ module MergeRequests
hook_data
end
- def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [])
+ def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
if merge_request.project
- merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees)
+ merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index bc0e7ad4e39..f3b99e1ec8c 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -28,6 +28,7 @@ module MergeRequests
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+
project.default_merge_request_target
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 156e7b2f078..cedfcb50e09 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -10,6 +10,8 @@ module MergeRequests
attr_reader :merge_request, :source
+ delegate :merge_jid, :state, to: :@merge_request
+
def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
@@ -18,15 +20,7 @@ module MergeRequests
@merge_request = merge_request
- unless @merge_request.mergeable?
- return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
- end
-
- @source = find_merge_source
-
- unless @source
- return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
- end
+ error_check!
merge_request.in_locked_state do
if commit
@@ -35,16 +29,32 @@ module MergeRequests
success
end
end
+ log_info("Merge process finished on JID #{merge_jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
private
+ def error_check!
+ error =
+ if @merge_request.should_be_rebased?
+ 'Only fast-forward merge is allowed for your project. Please update your source branch'
+ elsif !@merge_request.mergeable?
+ 'Merge request is not mergeable'
+ elsif !source
+ 'No source for merge'
+ end
+
+ raise MergeError, error if error
+ end
+
def commit
message = params[:commit_message] || merge_request.merge_commit_message
+ log_info("Git merge started on JID #{merge_jid}")
commit_id = repository.merge(current_user, source, merge_request, message)
+ log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
raise MergeError, 'Conflicts detected during merge' unless commit_id
@@ -58,7 +68,9 @@ module MergeRequests
end
def after_merge
+ log_info("Post merge started on JID #{merge_jid} with state #{state}")
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
+ log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
@@ -87,12 +99,17 @@ module MergeRequests
@merge_request.update(merge_error: log_message) if save_message_on_model
end
+ def log_info(message)
+ @logger ||= Rails.logger
+ @logger.info("#{merge_request_info} - #{message}")
+ end
+
def merge_request_info
merge_request.to_reference(full: true)
end
- def find_merge_source
- merge_request.diff_head_sha
+ def source
+ @source ||= @merge_request.diff_head_sha
end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 2832d893e95..1f394cacc64 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -40,10 +40,6 @@ module MergeRequests
merge_request.target_branch)
end
- if merge_request.previous_changes.include?('milestone_id')
- create_milestone_note(merge_request)
- end
-
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
@@ -111,5 +107,11 @@ module MergeRequests
end
end
end
+
+ def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
+ SystemNoteService.change_branch(
+ issuable, issuable.project, current_user, branch_type,
+ old_branch, new_branch)
+ end
end
end
diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb
index a02eee4961b..6b3939aeba5 100644
--- a/app/services/metrics_service.rb
+++ b/app/services/metrics_service.rb
@@ -6,8 +6,7 @@ class MetricsService
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::FsShardsCheck
+ Gitlab::HealthChecks::Redis::SharedStateCheck
].freeze
def prometheus_metrics_text
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
new file mode 100644
index 00000000000..2187f26d1ed
--- /dev/null
+++ b/app/services/milestones/promote_service.rb
@@ -0,0 +1,85 @@
+module Milestones
+ class PromoteService < Milestones::BaseService
+ PromoteMilestoneError = Class.new(StandardError)
+
+ def execute(milestone)
+ check_project_milestone!(milestone)
+
+ Milestone.transaction do
+ group_milestone = clone_project_milestone(milestone)
+
+ move_children_to_group_milestone(group_milestone)
+
+ # Destroy all milestones with same title across projects
+ destroy_old_milestones(milestone)
+
+ # Rollback if milestone is not valid
+ unless group_milestone.valid?
+ raise_error(group_milestone.errors.full_messages.to_sentence)
+ end
+
+ group_milestone
+ end
+ end
+
+ private
+
+ def milestone_ids_for_merge(group_milestone)
+ # Pluck need to be used here instead of select so the array of ids
+ # is persistent after old milestones gets deleted.
+ @milestone_ids_for_merge ||= begin
+ search_params = { title: group_milestone.title, project_ids: group_project_ids, state: 'all' }
+ milestones = MilestonesFinder.new(search_params).execute
+ milestones.pluck(:id)
+ end
+ end
+
+ def move_children_to_group_milestone(group_milestone)
+ milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids|
+ update_children(group_milestone, milestone_ids)
+ end
+ end
+
+ def check_project_milestone!(milestone)
+ raise_error('Only project milestones can be promoted.') unless milestone.project_milestone?
+ end
+
+ def clone_project_milestone(milestone)
+ params = milestone.slice(:title, :description, :start_date, :due_date, :state_event)
+
+ create_service = CreateService.new(group, current_user, params)
+
+ milestone = create_service.execute
+
+ # milestone won't be valid here because of duplicated title
+ milestone.save(validate: false)
+
+ milestone
+ end
+
+ def update_children(group_milestone, milestone_ids)
+ issues = Issue.where(project_id: group_project_ids, milestone_id: milestone_ids)
+ merge_requests = MergeRequest.where(source_project_id: group_project_ids, milestone_id: milestone_ids)
+
+ [issues, merge_requests].each do |issuable_collection|
+ issuable_collection.update_all(milestone_id: group_milestone.id)
+ end
+ end
+
+ def group
+ @group ||= parent.group || raise_error('Project does not belong to a group.')
+ end
+
+ def destroy_old_milestones(milestone)
+ Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all
+ end
+
+ def group_project_ids
+ @group_project_ids ||= group.projects.pluck(:id)
+ end
+
+ def raise_error(message)
+ raise PromoteMilestoneError, "Promotion failed - #{message}"
+ end
+ end
+end
diff --git a/app/services/notes/render_service.rb b/app/services/notes/render_service.rb
new file mode 100644
index 00000000000..a77e98c2b07
--- /dev/null
+++ b/app/services/notes/render_service.rb
@@ -0,0 +1,21 @@
+module Notes
+ class RenderService < BaseRenderer
+ # Renders a collection of Note instances.
+ #
+ # notes - The notes to render.
+ # project - The project to use for redacting.
+ # user - The user viewing the notes.
+
+ # Possible options:
+ # requested_path - The request path.
+ # project_wiki - The project's wiki.
+ # ref - The current Git reference.
+ # only_path - flag to turn relative paths into absolute ones.
+ # xhtml - flag to save the html in XHTML
+ def execute(notes, project, **opts)
+ renderer = Banzai::ObjectRenderer.new(project, current_user, **opts)
+
+ renderer.render(notes, :note)
+ end
+ end
+end
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index aa034315280..7e575b2d6f3 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Base class for the various service classes that count project data (e.g.
# issues or forks).
- class CountService
+ class CountService < BaseCountService
# The version of the cache format. This should be bumped whenever the
# underlying logic changes. This removes the need for explicitly flushing
# all caches.
@@ -11,29 +11,6 @@ module Projects
@project = project
end
- def relation_for_count
- raise(
- NotImplementedError,
- '"relation_for_count" must be implemented and return an ActiveRecord::Relation'
- )
- end
-
- def count
- Rails.cache.fetch(cache_key) { uncached_count }
- end
-
- def refresh_cache
- Rails.cache.write(cache_key, uncached_count)
- end
-
- def uncached_count
- relation_for_count.count
- end
-
- def delete_cache
- Rails.cache.delete(cache_key)
- end
-
def cache_key_name
raise(
NotImplementedError,
diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb
index 3a0fa84b868..d9bdf3a8ad7 100644
--- a/app/services/projects/forks_count_service.rb
+++ b/app/services/projects/forks_count_service.rb
@@ -1,6 +1,6 @@
module Projects
# Service class for getting and caching the number of forks of a project.
- class ForksCountService < CountService
+ class ForksCountService < Projects::CountService
def relation_for_count
@project.forks
end
diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb
new file mode 100644
index 00000000000..35624577024
--- /dev/null
+++ b/app/services/projects/group_links/create_service.rb
@@ -0,0 +1,15 @@
+module Projects
+ module GroupLinks
+ class CreateService < BaseService
+ def execute(group)
+ return false unless group
+
+ project.project_group_links.create(
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
new file mode 100644
index 00000000000..e3a20b4c1e4
--- /dev/null
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -0,0 +1,11 @@
+module Projects
+ module GroupLinks
+ class DestroyService < BaseService
+ def execute(group_link)
+ return false unless group_link
+
+ group_link.destroy
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb
index 41259de3a16..f5945f3b87f 100644
--- a/app/services/projects/hashed_storage_migration_service.rb
+++ b/app/services/projects/hashed_storage_migration_service.rb
@@ -10,7 +10,7 @@ module Projects
end
def execute
- return if project.hashed_storage?
+ return if project.hashed_storage?(:repository)
@old_disk_path = project.disk_path
has_wiki = project.wiki.repository_exists?
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index c3bf0031409..c3b11341b4d 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -4,8 +4,18 @@ module Projects
Error = Class.new(StandardError)
+ # Returns true if this importer is supposed to perform its work in the
+ # background.
+ #
+ # This method will only return `true` if async importing is explicitly
+ # supported by an importer class (`Gitlab::GithubImport::ParallelImporter`
+ # for example).
+ def async?
+ has_importer? && !!importer_class.try(:async?)
+ end
+
def execute
- add_repository_to_project unless project.gitlab_project_import?
+ add_repository_to_project
import_data
@@ -17,6 +27,14 @@ module Projects
private
def add_repository_to_project
+ if project.external_import? && !unknown_url?
+ raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
+ end
+
+ # We should skip the repository for a GitHub import or GitLab project import,
+ # because these importers fetch the project repositories for us.
+ return if has_importer? && importer_class.try(:imports_repository?)
+
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
@@ -32,19 +50,13 @@ module Projects
end
def import_repository
- raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
-
- # We should return early for a GitHub import because the new GitHub
- # importer fetch the project repositories for us.
- return if project.github_import?
-
begin
if project.gitea_import?
fetch_repository
else
clone_repository
end
- rescue Gitlab::Shell::Error => e
+ rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
@@ -75,12 +87,16 @@ module Projects
end
end
+ def importer_class
+ @importer_class ||= Gitlab::ImportSources.importer(project.import_type)
+ end
+
def has_importer?
Gitlab::ImportSources.importer_names.include?(project.import_type)
end
def importer
- Gitlab::ImportSources.importer(project.import_type).new(project)
+ importer_class.new(project)
end
def unknown_url?
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 3c0d186a73c..25de97325e2 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Service class for counting and caching the number of open issues of a
# project.
- class OpenIssuesCountService < CountService
+ class OpenIssuesCountService < Projects::CountService
def relation_for_count
# We don't include confidential issues in this number since this would
# expose the number of confidential issues to non project members.
diff --git a/app/services/projects/open_merge_requests_count_service.rb b/app/services/projects/open_merge_requests_count_service.rb
index 2a90f78b90d..77e6448fd5e 100644
--- a/app/services/projects/open_merge_requests_count_service.rb
+++ b/app/services/projects/open_merge_requests_count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Service class for counting and caching the number of open merge requests of
# a project.
- class OpenMergeRequestsCountService < CountService
+ class OpenMergeRequestsCountService < Projects::CountService
def relation_for_count
@project.merge_requests.opened
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 5957f612e84..e5cd6fcdfe3 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -60,21 +60,14 @@ module Projects
# Notifications
project.send_move_instructions(@old_path)
- # Move main repository
- # TODO: check storage type and NOOP when not using Legacy
- unless move_repo_folder(@old_path, @new_path)
- raise TransferError.new('Cannot move project')
- end
-
- # Move wiki repo also if present
- # TODO: check storage type and NOOP when not using Legacy
- move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
+ # Directories on disk
+ move_project_folders(project)
# Move missing group labels to project
Labels::TransferService.new(current_user, @old_group, project).execute
# Move uploads
- Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
+ move_project_uploads(project)
# Move pages
Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
@@ -131,5 +124,30 @@ module Projects
def execute_system_hooks
SystemHooksService.new.execute_hooks_for(project, :transfer)
end
+
+ def move_project_folders(project)
+ return if project.hashed_storage?(:repository)
+
+ # Move main repository
+ unless move_repo_folder(@old_path, @new_path)
+ raise TransferError.new("Cannot move project")
+ end
+
+ # Disk path is changed; we need to ensure we reload it
+ project.reload_repository!
+
+ # Move wiki repo also if present
+ move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
+ end
+
+ def move_project_uploads(project)
+ return if project.hashed_storage?(:attachments)
+
+ Gitlab::UploadsTransfer.new.move_project(
+ project.path,
+ @old_namespace.full_path,
+ @new_namespace.full_path
+ )
+ end
end
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index 2b82e5732e4..c499f384426 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -3,18 +3,24 @@ module Projects
def execute
return unless @project.forked?
- @project.forked_from_project.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project
+ if fork_source = @project.fork_source
+ fork_source.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << @project
+ end
+
+ refresh_forks_count(fork_source)
end
- merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project)
+ merge_requests = @project.fork_network
+ .merge_requests
+ .opened
+ .where.not(target_project: @project)
+ .from_project(@project)
merge_requests.each do |mr|
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
- refresh_forks_count(@project.forked_from_project)
-
@project.fork_network_member.destroy
@project.forked_project_link.destroy
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 5d275967821..911cc919bb8 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -35,24 +35,22 @@ class SystemHooksService
data[:old_path_with_namespace] = model.old_path_with_namespace
end
when User
- data.merge!({
- name: model.name,
- email: model.email,
- user_id: model.id,
- username: model.username
- })
+ data.merge!(user_data(model))
+
+ if event == :rename
+ data[:old_username] = model.username_was
+ end
when ProjectMember
data.merge!(project_member_data(model))
when Group
- owner = model.owner
+ data.merge!(group_data(model))
- data.merge!(
- name: model.name,
- path: model.path,
- group_id: model.id,
- owner_name: owner.respond_to?(:name) ? owner.name : nil,
- owner_email: owner.respond_to?(:email) ? owner.email : nil
- )
+ if event == :rename
+ data.merge!(
+ old_path: model.path_was,
+ old_full_path: model.full_path_was
+ )
+ end
when GroupMember
data.merge!(group_member_data(model))
end
@@ -104,6 +102,19 @@ class SystemHooksService
}
end
+ def group_data(model)
+ owner = model.owner
+
+ {
+ name: model.name,
+ path: model.path,
+ full_path: model.full_path,
+ group_id: model.id,
+ owner_name: owner.try(:name),
+ owner_email: owner.try(:email)
+ }
+ end
+
def group_member_data(model)
{
group_name: model.group.name,
@@ -116,4 +127,13 @@ class SystemHooksService
group_access: model.human_access
}
end
+
+ def user_data(model)
+ {
+ name: model.name,
+ email: model.email,
+ user_id: model.id,
+ username: model.username
+ }
+ end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 69bd19c1977..fe71a405565 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -481,17 +481,7 @@ module SystemNoteService
#
# Returns Boolean
def cross_reference_exists?(noteable, mentioner)
- # Initial scope should be system notes of this noteable type
- notes = Note.system.where(noteable_type: noteable.class)
-
- notes =
- if noteable.is_a?(Commit)
- # Commits have non-integer IDs, so they're stored in `commit_id`
- notes.where(commit_id: noteable.id)
- else
- notes.where(noteable_id: noteable.id)
- end
-
+ notes = noteable.notes.system
notes_for_mentioner(mentioner, noteable, notes).exists?
end
@@ -593,6 +583,10 @@ module SystemNoteService
create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
end
+ def cross_reference?(note_text)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index b6125cafa83..575853fd66b 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -31,12 +31,12 @@ class TodoService
mark_pending_todos_as_done(issue, current_user)
end
- # When we destroy an issue we should:
+ # When we destroy an issuable we should:
#
# * refresh the todos count cache for the current user
#
- def destroy_issue(issue, current_user)
- destroy_issuable(issue, current_user)
+ def destroy_issuable(issuable, user)
+ user.update_todos_count_cache
end
# When we reassign an issue we should:
@@ -72,14 +72,6 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
- # When we destroy a merge request we should:
- #
- # * refresh the todos count cache for the current user
- #
- def destroy_merge_request(merge_request, current_user)
- destroy_issuable(merge_request, current_user)
- end
-
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
@@ -216,6 +208,7 @@ class TodoService
def create_todos(users, attributes)
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
+
todo = Todo.create(attributes.merge(user_id: user.id))
user.update_todos_count_cache
todo
@@ -234,10 +227,6 @@ class TodoService
create_mention_todos(issuable.project, issuable, author, nil, skip_users)
end
- def destroy_issuable(issuable, user)
- user.update_todos_count_cache
- end
-
def toggling_tasks?(issuable)
issuable.previous_changes.include?('description') &&
issuable.tasks? && issuable.updated_tasks.any?
diff --git a/app/services/users/keys_count_service.rb b/app/services/users/keys_count_service.rb
new file mode 100644
index 00000000000..f82d27eded9
--- /dev/null
+++ b/app/services/users/keys_count_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Users
+ # Service class for getting the number of SSH keys that belong to a user.
+ class KeysCountService < BaseCountService
+ attr_reader :user
+
+ # user - The User for which to get the number of SSH keys.
+ def initialize(user)
+ @user = user
+ end
+
+ def relation_for_count
+ user.keys
+ end
+
+ def raw?
+ # Since we're storing simple integers we don't need all of the additional
+ # Marshal data Rails includes by default.
+ true
+ end
+
+ def cache_key
+ "users/key-count-service/#{user.id}"
+ end
+ end
+end
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 3a9c151cf9b..976017dfa82 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -25,7 +25,7 @@ module Users
user.block
# Reverse the user block if record migration fails
- if !migrate_records && transition
+ if !migrate_records_in_transaction && transition
transition.rollback
user.save!
end
@@ -36,18 +36,22 @@ module Users
private
- def migrate_records
+ def migrate_records_in_transaction
user.transaction(requires_new: true) do
@ghost_user = User.ghost
- migrate_issues
- migrate_merge_requests
- migrate_notes
- migrate_abuse_reports
- migrate_award_emojis
+ migrate_records
end
end
+ def migrate_records
+ migrate_issues
+ migrate_merge_requests
+ migrate_notes
+ migrate_abuse_reports
+ migrate_award_emojis
+ end
+
def migrate_issues
user.issues.update_all(author_id: ghost_user.id)
Issue.where(last_edited_by_id: user.id).update_all(last_edited_by_id: ghost_user.id)
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 7027ac4b5db..f4a5cf75018 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -26,11 +26,15 @@ class FileUploader < GitlabUploader
# This is used to build Upload paths dynamically based on the model's current
# namespace and path, allowing us to ignore renames or transfers.
#
- # model - Object that responds to `path_with_namespace`
+ # model - Object that responds to `full_path` and `disk_path`
#
# Returns a String without a trailing slash
- def self.dynamic_path_segment(model)
- File.join(CarrierWave.root, base_dir, model.full_path)
+ def self.dynamic_path_segment(project)
+ if project.hashed_storage?(:attachments)
+ File.join(CarrierWave.root, base_dir, project.disk_path)
+ else
+ File.join(CarrierWave.root, base_dir, project.full_path)
+ end
end
attr_accessor :model
diff --git a/app/validators/abstract_path_validator.rb b/app/validators/abstract_path_validator.rb
new file mode 100644
index 00000000000..adbccb65a84
--- /dev/null
+++ b/app/validators/abstract_path_validator.rb
@@ -0,0 +1,38 @@
+class AbstractPathValidator < ActiveModel::EachValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ raise NotImplementedError
+ end
+
+ def self.format_regex
+ raise NotImplementedError
+ end
+
+ def self.format_error_message
+ raise NotImplementedError
+ end
+
+ def self.full_path(record, value)
+ value
+ end
+
+ def self.valid_path?(path)
+ encode!(path)
+ "#{path}/" =~ path_regex
+ end
+
+ def validate_each(record, attribute, value)
+ unless value =~ self.class.format_regex
+ record.errors.add(attribute, self.class.format_error_message)
+ return
+ end
+
+ full_path = self.class.full_path(record, value)
+ return unless full_path
+
+ unless self.class.valid_path?(full_path)
+ record.errors.add(attribute, "#{value} is a reserved name")
+ end
+ end
+end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
index 098b16017d2..8c7bb750339 100644
--- a/app/validators/certificate_key_validator.rb
+++ b/app/validators/certificate_key_validator.rb
@@ -17,6 +17,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator
def valid_private_key_pem?(value)
return false unless value
+
pkey = OpenSSL::PKey::RSA.new(value)
pkey.private?
rescue OpenSSL::PKey::PKeyError
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
index e3d18097f71..5239e70a326 100644
--- a/app/validators/certificate_validator.rb
+++ b/app/validators/certificate_validator.rb
@@ -17,6 +17,7 @@ class CertificateValidator < ActiveModel::EachValidator
def valid_certificate_pem?(value)
return false unless value
+
OpenSSL::X509::Certificate.new(value).present?
rescue OpenSSL::X509::CertificateError
false
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
new file mode 100644
index 00000000000..13ec342f399
--- /dev/null
+++ b/app/validators/cluster_name_validator.rb
@@ -0,0 +1,24 @@
+# ClusterNameValidator
+#
+# Custom validator for ClusterName.
+class ClusterNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if record.user?
+ unless value.present?
+ record.errors.add(attribute, " has to be present")
+ end
+ elsif record.gcp?
+ if record.persisted? && record.name_changed?
+ record.errors.add(attribute, " can not be changed because it's synchronized with provider")
+ end
+
+ unless value.length >= 1 && value.length <= 63
+ record.errors.add(attribute, " is invalid syntax")
+ end
+
+ unless value =~ Gitlab::Regex.kubernetes_namespace_regex
+ record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
+ end
+ end
+ end
+end
diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb
deleted file mode 100644
index 4688aabc2a8..00000000000
--- a/app/validators/dynamic_path_validator.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# DynamicPathValidator
-#
-# Custom validator for GitLab path values.
-# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
-#
-# Values are checked for formatting and exclusion from a list of illegal path
-# names.
-class DynamicPathValidator < ActiveModel::EachValidator
- extend Gitlab::EncodingHelper
-
- class << self
- def valid_user_path?(path)
- encode!(path)
- "#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
- end
-
- def valid_group_path?(path)
- encode!(path)
- "#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
- end
-
- def valid_project_path?(path)
- encode!(path)
- "#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
- end
- end
-
- def path_valid_for_record?(record, value)
- full_path = record.respond_to?(:build_full_path) ? record.build_full_path : value
-
- return true unless full_path
-
- case record
- when Project
- self.class.valid_project_path?(full_path)
- when Group
- self.class.valid_group_path?(full_path)
- else # User or non-Group Namespace
- self.class.valid_user_path?(full_path)
- end
- end
-
- def validate_each(record, attribute, value)
- unless value =~ Gitlab::PathRegex.namespace_format_regex
- record.errors.add(attribute, Gitlab::PathRegex.namespace_format_message)
- return
- end
-
- unless path_valid_for_record?(record, value)
- record.errors.add(attribute, "#{value} is a reserved name")
- end
- end
-end
diff --git a/app/validators/namespace_path_validator.rb b/app/validators/namespace_path_validator.rb
new file mode 100644
index 00000000000..4a0aa64ae0c
--- /dev/null
+++ b/app/validators/namespace_path_validator.rb
@@ -0,0 +1,19 @@
+class NamespacePathValidator < AbstractPathValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ Gitlab::PathRegex.full_namespace_path_regex
+ end
+
+ def self.format_regex
+ Gitlab::PathRegex.namespace_format_regex
+ end
+
+ def self.format_error_message
+ Gitlab::PathRegex.namespace_format_message
+ end
+
+ def self.full_path(record, value)
+ record.build_full_path
+ end
+end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
new file mode 100644
index 00000000000..829b596ad3c
--- /dev/null
+++ b/app/validators/project_path_validator.rb
@@ -0,0 +1,19 @@
+class ProjectPathValidator < AbstractPathValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ Gitlab::PathRegex.full_project_path_regex
+ end
+
+ def self.format_regex
+ Gitlab::PathRegex.project_path_format_regex
+ end
+
+ def self.format_error_message
+ Gitlab::PathRegex.project_path_format_message
+ end
+
+ def self.full_path(record, value)
+ record.build_full_path
+ end
+end
diff --git a/app/validators/user_path_validator.rb b/app/validators/user_path_validator.rb
new file mode 100644
index 00000000000..adf02901802
--- /dev/null
+++ b/app/validators/user_path_validator.rb
@@ -0,0 +1,15 @@
+class UserPathValidator < AbstractPathValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ Gitlab::PathRegex.root_namespace_path_regex
+ end
+
+ def self.format_regex
+ Gitlab::PathRegex.namespace_format_regex
+ end
+
+ def self.format_error_message
+ Gitlab::PathRegex.namespace_format_message
+ end
+end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 935787d1a4a..4a2238fe277 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -43,7 +43,7 @@
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
.hint
- Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
+ Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
.form-actions
= f.submit 'Save', class: 'btn btn-save append-right-10'
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 3a4d5ce0b5c..12658dddc06 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -743,5 +743,56 @@
installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
+ %fieldset
+ %legend User and IP Rate Limits
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_unauthenticated_enabled do
+ = f.check_box :throttle_unauthenticated_enabled
+ Enable unauthenticated request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_api_enabled do
+ = f.check_box :throttle_authenticated_api_enabled
+ Enable authenticated API request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_web_enabled do
+ = f.check_box :throttle_authenticated_web_enabled
+ Enable authenticated web request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 3ef8f2a3acb..f0cc4d7ee62 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -42,4 +42,4 @@
.panel.panel-default
- %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" }
+ %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
index 7dd9943190f..91a8c0c62fe 100644
--- a/app/views/admin/hook_logs/_index.html.haml
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -24,7 +24,7 @@
%td
= truncate(hook_log.url, length: 50)
%td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 4d8754afdd2..c37d8ac45b9 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -14,7 +14,7 @@
= hidden_field_tag :namespace_id, params[:namespace_id]
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.full_path}"
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index ab4165c0bf2..42f92079d85 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -115,7 +115,7 @@
= f.label :new_namespace_id, "Namespace", class: 'control-label'
.col-sm-10
.dropdown
- = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' })
+ = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 76f4a817744..4f60be698e9 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -52,22 +52,23 @@
%br
- if @runners.any?
- .table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Version
- %th Projects
- %th Jobs
- %th Tags
- %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
- %th
+ .runners-content
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Version
+ %th Projects
+ %th Jobs
+ %th Tags
+ %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
+ %th
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
- = paginate @runners, theme: "gitlab"
+ - @runners.each do |runner|
+ = render "admin/runners/runner", runner: runner
+ = paginate @runners, theme: "gitlab"
- else
.nothing-here-block No runners found
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index df2bf27be9d..6d8fad0eb8d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -99,7 +99,7 @@
%td.build-link
- if project
- = link_to ci_status_path(build.pipeline) do
+ = link_to pipeline_path(build.pipeline) do
%strong= build.pipeline.short_sha
%td.timestamp
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
index 39c7fb0eba2..35a3563dff1 100644
--- a/app/views/ci/status/_badge.html.haml
+++ b/app/views/ci/status/_badge.html.haml
@@ -5,9 +5,9 @@
- if link && status.has_details?
= link_to status.details_path, class: css_classes, title: title do
- = custom_icon(status.icon)
+ = sprite_icon(status.icon)
= status.text
- else
%span{ class: css_classes, title: title }
- = custom_icon(status.icon)
+ = sprite_icon(status.icon)
= status.text
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index dcfb7f0c32d..c5b4439e273 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -7,13 +7,13 @@
- if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
- %span{ class: klass }= custom_icon(status.icon)
+ %span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
- else
.menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
- %span{ class: klass }= custom_icon(status.icon)
+ %span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
- if status.has_action?
- = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
- = custom_icon(status.action_icon)
+ = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
+ = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 57544559824..573a4b93d67 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -1,33 +1,41 @@
-.blank-state
- .blank-state-icon
- = custom_icon("add_new_user", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Add user
- %p.blank-state-text
- Add your team members and others to GitLab.
- = link_to new_admin_user_path, class: "btn btn-new" do
- New user
+.blank-state-row
+ = link_to new_project_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
-.blank-state
- .blank-state-icon
- = custom_icon("configure_server", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Configure GitLab
- %p.blank-state-text
- Make adjustments to how your GitLab instance is set up.
- = link_to admin_root_path, class: "btn btn-new" do
- Configure
+ - if current_user.can_create_group?
+ = link_to admin_root_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_group", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a group
+ %p.blank-state-text
+ Groups are a great way to organize projects and people.
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group
- %p.blank-state-text
- Groups are a great way to organize projects and people.
- = link_to new_group_path, class: "btn btn-new" do
- New group
+ = link_to new_admin_user_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_user", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Add people
+ %p.blank-state-text
+ Add your team members and others to GitLab.
+
+ = link_to admin_root_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("configure_server", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Configure GitLab
+ %p.blank-state-text
+ Make adjustments to how your GitLab instance is set up.
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index a93a3415ee1..8d5bddbb288 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -1,48 +1,58 @@
- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group for several dependent projects.
- %p.blank-state-text
- Groups are the best way to manage projects and members.
- = link_to new_group_path, class: "btn btn-new" do
- New group
+.blank-state-row
+ - if current_user.can_create_project?
+ = link_to new_project_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
+ - else
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ If you are added to a project, it will be displayed here.
-.blank-state
- .blank-state-icon
- = custom_icon("add_new_project", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a project
- %p.blank-state-text
- - if current_user.can_create_project?
- You don't have access to any projects right now.
- You can create up to
- %strong= number_with_delimiter(current_user.projects_limit)
- = succeed "." do
- = "project".pluralize(current_user.projects_limit)
- - else
- If you are added to a project, it will be displayed here.
- - if current_user.can_create_project?
- = link_to new_project_path, class: "btn btn-new" do
- New project
+ - if current_user.can_create_group?
+ = link_to new_group_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_group", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a group
+ %p.blank-state-text
+ Groups are the best way to manage projects and members.
-- if public_project_count > 0
- .blank-state
- .blank-state-icon
- = custom_icon("globe", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Explore public projects
- %p.blank-state-text
- There are
- = number_with_delimiter(public_project_count)
- public projects on this server.
- Public projects are an easy way to allow
- everyone to have read-only access.
- = link_to trending_explore_projects_path, class: "btn btn-new" do
- Browse projects
+ - if public_project_count > 0
+ = link_to trending_explore_projects_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("globe", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Explore public projects
+ %p.blank-state-text
+ There are
+ = number_with_delimiter(public_project_count)
+ public projects on this server.
+ Public projects are an easy way to allow
+ everyone to have read-only access.
+
+ = link_to "https://docs.gitlab.com/", class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("lightbulb", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Learn more about GitLab
+ %p.blank-state-text
+ Take a look at the documentation to discover all of GitLab's capabilities.
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index ad3fac6d164..18a82feb189 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,12 +1,13 @@
-.row.blank-state-parent-container
+.blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
- .blank-state.blank-state-welcome
- %h2.blank-state-welcome-title
- Welcome to GitLab
- %p.blank-state-text
- Code, test, and deploy together
- - if current_user.admin?
- = render "blank_state_admin_welcome"
- - else
- = render "blank_state_welcome"
+ .row
+ .blank-state-welcome
+ %h2.blank-state-welcome-title
+ Welcome to GitLab
+ %p.blank-state-text
+ Code, test, and deploy together
+ - if current_user.admin?
+ = render "blank_state_admin_welcome"
+ - else
+ = render "blank_state_welcome"
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 38fd053ae65..efe1fb99efc 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -36,7 +36,7 @@
.todo-body
.todo-note
.md
- = event_note(todo.body, project: todo.project)
+ = first_line_in_markdown(todo, :body, 150, project: todo.project)
- if todo.pending?
.todo-actions
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index f62a0cd681e..a5686002328 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -8,7 +8,7 @@
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
- To do
+ Todos
%span.badge
= number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }>
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index b3313c7c985..cf0e0de1ca4 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
+= form_for application, url: doorkeeper_submit_path(application), html: { role: 'form', class: 'doorkeeper-app-form' } do |f|
= form_errors(application)
.form-group
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 8ba88906714..6d9c6b5572a 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,5 +1,5 @@
%main{ :role => "main" }
- .modal-no-backdrop
+ .modal-no-backdrop.modal-doorkeepr-auth
.modal-content
.modal-header
%h3.page-title
@@ -16,14 +16,26 @@
%strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
%p
- You are about to authorize
+ An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
- to use your account.
- - if @pre_auth.scopes
+ is requesting access to your GitLab account.
+
+ - auth_app_owner = @pre_auth.client.application.owner
+ - if auth_app_owner
+ This application was created by
+ = succeed "." do
+ = link_to auth_app_owner.name, user_path(auth_app_owner)
+
+ Please note that this application is not provided by GitLab and you should verify its authenticity before
+ allowing access.
+ - if @pre_auth.scopes
+ %p
This application will be able to:
%ul
- @pre_auth.scopes.each do |scope|
- %li= t scope, scope: [:doorkeeper, :scopes]
+ %li
+ %strong= t scope, scope: [:doorkeeper, :scopes]
+ .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index 6fa2f9bd4db..7e264eb5575 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
%div{ xmlns: "http://www.w3.org/1999/xhtml" }
- = markdown(note.note, pipeline: :atom, project: note.project, author: note.author)
+ = markdown_field(note, :note)
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index df4b9562215..de6383e4097 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -10,7 +10,7 @@
.event-body
.event-note
.md
- = event_note(event.target.note, project: event.project)
+ = first_line_in_markdown(event.target, :note, 150, project: event.project)
- note = event.target
- if note.attachment.url
- if note.attachment.image?
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index d0c2e0b1d69..021de4f0caf 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -29,7 +29,7 @@
.row.prepend-top-default
.col-md-8
- .documentation-index
+ .documentation-index.wiki
= markdown(@help_index)
.col-md-4
.panel.panel-default
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 29387d6627e..4c5cc249159 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -5,7 +5,7 @@
- if @group && @group.persisted? && @group.path
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
- - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project) }
+ - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }
.search.search-form{ class: "#{'has-location-badge' if label.present?}" }
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 5ff6ac5fc00..e2407f6a428 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -7,7 +7,7 @@
= link_to root_path, title: 'Dashboard', id: 'logo' do
= brand_header_logo
%span.logo-text.hidden-xs
- = render 'shared/logo_type.svg'
+ = brand_header_logo_type
- if current_user
= render "layouts/nav/dashboard"
@@ -61,7 +61,7 @@
= link_to "Help", help_path
%li.divider
%li
- = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
+ = link_to "Sign out", destroy_user_session_path, class: "sign-out-link"
- if session[:impersonator_id]
%li.impersonation
= link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index a80518f7986..3e36da31ea3 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -1,10 +1,15 @@
- discussion = @note.discussion if @note.part_of_discussion?
+- diff_discussion = discussion&.diff_discussion?
+- on_image = discussion.on_image? if diff_discussion
+
- if discussion
+ - phrase_end_char = on_image ? "." : ":"
+
%p.details
- = succeed ':' do
+ = succeed phrase_end_char do
= link_to @note.author_name, user_url(@note.author)
- - if discussion.diff_discussion?
+ - if diff_discussion
- if discussion.new_discussion?
started a new discussion
- else
@@ -21,7 +26,7 @@
%p.details
#{link_to @note.author_name, user_url(@note.author)} commented:
-- if discussion&.diff_discussion?
+- if diff_discussion && !on_image
= content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email'
diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml
deleted file mode 100644
index c31a4a8ecd4..00000000000
--- a/app/views/profiles/accounts/_reset_token.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- name = label.parameterize
-- attribute = name.underscore
-
-.reset-action
- %p.cgray
- = label_tag name, label, class: "label-light"
- = text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()'
- %p.help-block
- = help_text
- .prepend-top-default
- = link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token'
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 7f79168dfb3..ced58dffcdc 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -9,22 +9,6 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Private Tokens
- %p
- Keep these tokens secret, anyone with access to them can interact with
- GitLab as if they were you.
- .col-lg-8.private-tokens-reset
- = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
-
- = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
-
- - if incoming_email_token_enabled?
- = render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' }
-
-%hr
-.row.prepend-top-default
- .col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0
Two-Factor Authentication
%p
Increase your account's security by enabling Two-Factor Authentication (2FA).
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 06bb72b9f0d..26c2e4c5936 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -30,3 +30,40 @@
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
+
+%hr
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ RSS token
+ %p
+ Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs.
+ %p
+ It cannot be used to access any other data.
+ .col-lg-8.rss-token-reset
+ = label_tag :rss_token, 'RSS token', class: "label-light"
+ = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.help-block
+ Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
+ You should
+ = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
+ if that ever happens.
+
+- if incoming_email_token_enabled?
+ %hr
+ .row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ Incoming email token
+ %p
+ Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.
+ %p
+ It cannot be used to access any other data.
+ .col-lg-8.incoming-email-token-reset
+ = label_tag :incoming_email_token, 'Incoming email token', class: "label-light"
+ = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.help-block
+ Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
+ You should
+ = link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
+ if that ever happens.
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 623d3bc91c6..c5b1897c492 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -3,7 +3,7 @@
- project = local_assigns.fetch(:project)
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Export project
@@ -11,7 +11,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.bs-callout.bs-callout-info
%p.append-bottom-0
%p
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 619b632918e..1d644dda177 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,6 +1,5 @@
- empty_repo = @project.empty_repo?
- fork_network = @project.fork_network
-- forked_from_project = @project.forked_from_project || fork_network&.root_project
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
@@ -16,13 +15,13 @@
- if @project.forked?
%p
- - if forked_from_project
+ - if @project.fork_source
#{ s_('ForkedFromProjectPath|Forked from') }
- = link_to project_path(forked_from_project) do
- = forked_from_project.full_name
+ = link_to project_path(@project.fork_source) do
+ = fork_source_name(@project)
- else
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
- = deleted_message % { project_name: fork_network.deleted_root_project_name }
+ = deleted_message % { project_name: fork_source_name(@project) }
.project-repo-buttons
.count-buttons
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 770608eddff..a9431cc4956 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -10,25 +10,23 @@
.md-area
.md-header
%ul.nav-links.clearfix
- %li.active
+ %li.md-header-tab.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
- %li
+ %li.md-header-tab
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
- %li.pull-right
- .toolbar-group
- = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
- = markdown_toolbar_button({ icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
- = markdown_toolbar_button({ icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
- = markdown_toolbar_button({ icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
- = markdown_toolbar_button({ icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
- = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
- = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
- .toolbar-group
- %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
- = icon("arrows-alt fw")
+ %li.md-header-toolbar
+ = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
+ = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
+ = sprite_icon("screen-full")
.md-write-holder
= yield
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 6c162481dd8..97532f1e2bd 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -10,5 +10,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 project.')
+ = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine.')
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
index 371cdb1e403..1f8ae463d0f 100644
--- a/app/views/projects/clusters/_form.html.haml
+++ b/app/views/projects/clusters/_form.html.haml
@@ -4,34 +4,32 @@
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = field.hidden_field :provider_type, value: :gcp
= form_errors(@cluster)
.form-group
- = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name')
- = field.text_field :gcp_cluster_name, class: 'form-control'
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control'
- .form-group
- = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
- = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_project_id, class: 'form-control'
-
- .form-group
- = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a'
+ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
+ .form-group
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :gcp_project_id, class: 'form-control'
- .form-group
- = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes')
- = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3'
+ .form-group
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
- .form-group
- = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type')
- = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+ .form-group
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group
- = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-2'
.form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/_header.html.haml b/app/views/projects/clusters/_header.html.haml
index 0134d46491c..beb798e7154 100644
--- a/app/views/projects/clusters/_header.html.haml
+++ b/app/views/projects/clusters/_header.html.haml
@@ -11,4 +11,4 @@
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|A %{link_to_container_project} must have been created under this account').html_safe % { link_to_container_project: link_to_container_project }
+ = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index c538d41ffad..6b321f60212 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,9 +1,20 @@
- breadcrumb_title "Cluster"
-- page_title _("New Cluster")
+- page_title _("Cluster")
.row.prepend-top-default
.col-sm-4
= render 'sidebar'
.col-sm-8
- = render 'header'
-= render 'form'
+ - if @project.kubernetes_service&.active?
+ %h4.prepend-top-0= s_('ClusterIntegration|Cluster management')
+
+ %p= s_('ClusterIntegration|A cluster has been set up on this project through the Kubernetes integration page')
+ = link_to s_('ClusterIntegration|Manage Kubernetes integration'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
+
+ - else
+ %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
+
+ %p= s_('ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab')
+ = link_to s_('ClusterIntegration|Create on GKE'), providers_gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
+ = link_to s_('ClusterIntegration|Add an existing cluster'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
diff --git a/app/views/projects/clusters/new_gcp.html.haml b/app/views/projects/clusters/new_gcp.html.haml
new file mode 100644
index 00000000000..48e6b6ae8e8
--- /dev/null
+++ b/app/views/projects/clusters/new_gcp.html.haml
@@ -0,0 +1,10 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ = render 'header'
+
+= render 'form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index b127e06030e..b7671f5e3c4 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -4,15 +4,22 @@
- expanded = Rails.env.test?
-- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation?
+- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
+ install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
+ install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
- cluster_status_reason: @cluster.status_reason } }
+ cluster_status_reason: @cluster.status_reason,
+ help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } }
- %section.settings
+
+ .js-cluster-application-notice
+ .flash-container
+
+ %section.settings.no-animate.expanded
%h4= s_('ClusterIntegration|Enable cluster integration')
- .settings-content.expanded
+ .settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
@@ -33,7 +40,7 @@
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group.append-bottom-20
%label.append-bottom-10
@@ -49,6 +56,8 @@
.form-group
= field.submit _('Save'), class: 'btn btn-success'
+ .cluster-applications-table#js-cluster-applications
+
%section.settings#js-cluster-details
.settings-header
%h4= s_('ClusterIntegration|Cluster details')
@@ -56,21 +65,21 @@
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster')
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.form_group.append-bottom-20
- %label.append-bottom-10{ for: 'cluter-name' }
+ %label.append-bottom-10{ for: 'cluster-name' }
= s_('ClusterIntegration|Cluster name')
.input-group
- %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true }
+ %input.form-control.cluster-name{ value: @cluster.name, disabled: true }
%span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'))
- %section.settings#js-cluster-advanced-settings
+ %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'advanced_settings'
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index 83821326aec..36b28c731a1 100644
--- a/app/views/projects/commit/_ajax_signature.html.haml
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -1,2 +1,2 @@
- if commit.has_signature?
- %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
+ %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index ff17372fdd9..8b9c1bbb602 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -61,13 +61,13 @@
%span.cgray= n_('parent', 'parents', @commit.parents.count)
- @commit.parents.each do |parent|
= link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha"
- %span.commit-info.branches
+ .commit-info.branches
%i.fa.fa-spinner.fa-spin
- if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info
- .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
+ .status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" }
= link_to project_pipeline_path(@project, last_pipeline.id) do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml
new file mode 100644
index 00000000000..84a52d49487
--- /dev/null
+++ b/app/views/projects/commit/_limit_exceeded_message.html.haml
@@ -0,0 +1,8 @@
+.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: "Project has too many #{label_for_message} to search"} }
+ .limit-icon
+ - if objects == :branch
+ = icon('code-fork')
+ - else
+ = icon('tag')
+ .limit-message
+ %span #{label_for_message.capitalize} unavailable
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index edff018ba6d..44aa8002f12 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -24,5 +24,5 @@
= link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
-%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
+%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
= label
diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml
index 911c9ddce06..8611129b356 100644
--- a/app/views/projects/commit/branches.html.haml
+++ b/app/views/projects/commit/branches.html.haml
@@ -1,15 +1,15 @@
-- if @branches.any? || @tags.any?
+- if @branches_limit_exceeded
+ = render 'limit_exceeded_message', objects: :branch, label_for_message: "branches"
+- elsif @branches.any?
- branch = commit_default_branch(@project, @branches)
- = link_to(project_ref_path(@project, branch), class: "label label-gray ref-name") do
- = icon('code-fork')
- = branch
+ = commit_branch_link(project_ref_path(@project, branch), branch)
- -# `commit_default_branch` deletes the default branch from `@branches`,
- -# so only render this if we have more branches left
- - if @branches.any? || @tags.any?
- %span
- = link_to "…", "#", class: "js-details-expand label label-gray"
-
- %span.js-details-content.hide
- = commit_branches_links(@project, @branches) if @branches.any?
- = commit_tags_links(@project, @tags) if @tags.any?
+- if @branches.any? || @tags.any? || @tags_limit_exceeded
+ %span
+ = link_to "…", "#", class: "js-details-expand label label-gray"
+ %span.js-details-content.hide
+ = commit_branches_links(@project, @branches)
+ - if @tags_limit_exceeded
+ = render 'limit_exceeded_message', objects: :tag, label_for_message: "tags"
+ - else
+ = commit_tags_links(@project, @tags)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index a16ffb433a5..a66177f20e9 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -1,11 +1,6 @@
- ref = local_assigns.fetch(:ref)
-- if @note_counts
- - note_count = @note_counts.fetch(commit.id, 0)
-- else
- - notes = commit.notes
- - note_count = notes.user.count
-- cache_key = [project.full_path, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits), I18n.locale]
+- cache_key = [project.full_path, commit.id, current_application_settings, @path.presence, current_controller?(:commits), I18n.locale]
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 45985a5ecef..e75ae87e771 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -1,5 +1,5 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Deploy Keys
@@ -7,7 +7,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%h5.prepend-top-0
Create a new deploy key for this project
= render @deploy_keys.form_partial_path
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 2de2cf9e38c..dd473ebe580 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -22,9 +22,11 @@
- diff_files.each do |diff_file|
%li
%a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
- = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5")
- %span.diff-file-changes-path.append-right-5= diff_file.new_path
- .pull-right
+ = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8")
+ %span.diff-changed-file-content.append-right-8
+ %strong.diff-changed-file-name= diff_file.blob.name
+ %span.diff-changed-file-path.prepend-top-5= diff_file.new_path
+ %span.diff-changed-stats
%span.cgreen<
+#{diff_file.added_lines}
%span.cred<
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 893e536e289..5ebeae5c35f 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -4,7 +4,7 @@
- expanded = Rails.env.test?
.project-edit-container
- %section.settings.general-settings
+ %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
General project settings
@@ -12,7 +12,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Update your project name, description, avatar, and other general settings.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset
@@ -61,7 +61,7 @@
= link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings.sharing-permissions
+ %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Permissions
@@ -69,13 +69,13 @@
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
+ %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
%h4
Merge request settings
@@ -83,14 +83,14 @@
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save"
= render 'export', project: @project
- %section.settings.advanced-settings
+ %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Advanced settings
@@ -98,7 +98,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.sub-section
%h4 Housekeeping
%p
@@ -173,7 +173,10 @@
%p
This will remove the fork relationship to source project
= succeed "." do
- = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
+ - if @project.fork_source
+ = link_to(fork_source_name(@project), project_path(@project.fork_source))
+ - else
+ = fork_source_name(@project)
= form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
%p
%strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index d7859c9fbeb..add394a6356 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -19,14 +19,15 @@
.environments-container
- if @deployments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any deployments right now.
- %p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ .blank-state-row
+ .blank-state-center
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 70156c03e3c..cce16bc58b3 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Contributors"
+- page_title _('Contributors')
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_d3')
= webpack_bundle_tag('graphs')
@@ -7,23 +7,23 @@
.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
.sub-header-block
- .tree-ref-holder
+ .tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+ = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
.loading-graph
.center
%h3.page-title
%i.fa.fa-spinner.fa-spin
- Building repository graph.
- %p.slead Please wait a moment, this page will automatically refresh when ready.
+ = s_('ContributorsPage|Building repository graph.')
+ %p.slead
+ = s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
.stat-graph.hide
.header.clearfix
%h3#date_header.page-title
%p.light
- Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
+ = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
%input#brush_change{ :type => "hidden" }
.graphs.row
#contributors-master
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index 05b06cfc8b2..8096d9530c3 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -24,7 +24,7 @@
%td
= truncate(hook_log.url, length: 50)
%td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 13809da6523..0d39edb7bfd 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -3,8 +3,8 @@
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
+ issue: { assignee_id: finder.assignee.try(:id),
+ milestone_id: finder.milestones.first.try(:id) }),
class: "btn btn-new",
title: "New issue",
id: "new_issue_link"
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index e1b4a49850a..4f78102be0c 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,3 +1,7 @@
+- can_create_merge_request = can?(current_user, :create_merge_request, @project)
+- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
+- value = can_create_merge_request ? 'Create a merge request' : 'Create a branch'
+
- if can?(current_user, :push_code, @project)
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
.btn-group.unavailable
@@ -6,20 +10,21 @@
%span.text
Checking branch availability…
.btn-group.available.hide
- %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: 'Create a merge request', data: { action: 'create-mr' } }
+ %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } }
%button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
= icon('caret-down')
%ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
- %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
- .menu-item
- .icon-container
- = icon('check')
- .description
- %strong Create a merge request
- %span
- Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
- %li.divider.droplab-item-ignore
- %li{ role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
+ - if can_create_merge_request
+ %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
+ .menu-item
+ .icon-container
+ = icon('check')
+ .description
+ %strong Create a merge request
+ %span
+ Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
+ %li.divider.droplab-item-ignore
+ %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
.menu-item
.icon-container
= icon('check')
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index b9fec8af4d7..48410ffee21 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -27,9 +27,9 @@
.issuable-meta
- if @issue.confidential
- = icon('eye-slash', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon')
- if @issue.discussion_locked?
- = icon('lock', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
@@ -40,7 +40,7 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can_update_issue
- %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
+ %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 7da4ffd5e43..17ac8a20a30 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -4,7 +4,7 @@
.sidebar-container
.blocks-container
.block
- %strong.prepend-top-10
+ %strong.inline.prepend-top-8
= @build.name
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
@@ -91,7 +91,7 @@
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to project_job_path(@project, build) do
- = icon('arrow-right')
+ = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
@@ -100,4 +100,5 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ = sprite_icon('retry', size:16, css_class: 'icon-retry')
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index ce0e3872240..1d0aaa47b60 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -57,13 +57,13 @@
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
- .js-truncated-info.truncated-info.hidden<
+ .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
KiB of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
- .controllers
+ .controllers.pull-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
@@ -71,7 +71,7 @@
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
- - if can?(current_user, :update_build, @project) && @build.erasable?
+ - if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index cb723fe6a18..75b3db7e505 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -16,7 +16,7 @@
.issuable-meta
- if @merge_request.discussion_locked?
- = icon('lock', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request")
.issuable-actions.js-issuable-actions
@@ -34,7 +34,7 @@
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index a5153df1159..9fc297ab7f6 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -23,14 +23,18 @@
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
+ = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
+ Edit
+
+ - if @project.group
+ = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do
+ Promote
+
- if @milestone.active?
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
- else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
- Edit
-
= link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
Delete
@@ -40,6 +44,7 @@
.detail-page-description.milestone-detail
%h2.title
= markdown_field(@milestone, :title)
+
%div
- if @milestone.description.present?
.description
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index f8627a3818b..b2e71cff6ce 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -9,12 +9,6 @@
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
- "all-path" => project_pipelines_path(@project),
- "pending-path" => project_pipelines_path(@project, scope: :pending),
- "running-path" => project_pipelines_path(@project, scope: :running),
- "finished-path" => project_pipelines_path(@project, scope: :finished),
- "branches-path" => project_pipelines_path(@project, scope: :branches),
- "tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } }
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 6a47cbdf724..ba7d98228c3 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Branches
@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%p
By default, protected branches are designed to:
%ul
diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml
index ea91e8af70e..f53b81cada6 100644
--- a/app/views/projects/protected_tags/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml
@@ -2,7 +2,7 @@
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide',
- dropdown_class: 'dropdown-menu-selectable',
+ dropdown_class: 'dropdown-menu-selectable capitalize-header',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
= render 'projects/protected_tags/shared/create_protected_tag'
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index c07bd454ff6..e764a37bbd7 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Tags
@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%p
By default, protected tags are designed to:
%ul
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index d8e11500964..b0cb5ce5e8f 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -4,42 +4,39 @@
.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
.col-lg-3
%h4.prepend-top-0
- Metrics
+ = s_('PrometheusService|Metrics')
%p
- Metrics are automatically configured and monitored
- based on a library of metrics from popular exporters.
- = link_to 'More information', help_page_path('user/project/integrations/prometheus')
+ = s_('PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters.')
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
.col-lg-9
.panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } }
.panel-heading
%h3.panel-title
- Monitored
+ = s_('PrometheusService|Monitored')
%span.badge.js-monitored-count 0
.panel-body
.loading-metrics.text-center.js-loading-metrics
= icon('spinner spin 3x', class: 'metrics-load-spinner')
- %p Finding and configuring metrics...
+ %p
+ = s_('PrometheusService|Finding and configuring metrics...')
.empty-metrics.text-center.hidden.js-empty-metrics
= custom_icon('icon_empty_metrics')
- %p No metrics are being monitored. To start monitoring, deploy to an environment.
- = link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do
- View environments
+ %p
+ = s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.')
+ = link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success'
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
.panel.panel-default.hidden.js-panel-missing-env-vars
.panel-heading
%h3.panel-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
- Missing environment variable
+ = s_('PrometheusService|Missing environment variable')
%span.badge.js-env-var-count 0
.panel-body.hidden
.flash-container
.flash-notice
.flash-text
- To set up automatic monitoring, add the environment variable
- %code
- $CI_ENVIRONMENT_SLUG
- to exporter&rsquo;s queries.
- = link_to 'More information', help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
+ = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 62455d0d40d..664a4554692 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -4,7 +4,7 @@
- expanded = Rails.env.test?
-%section.settings#js-general-pipeline-settings
+%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
General pipelines settings
@@ -12,10 +12,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/pipelines_settings/show'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Runners settings
@@ -23,10 +23,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
Register and see your runners for this project.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/runners/index'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Secret variables
@@ -35,10 +35,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
= render "ci/variables/content"
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'ci/variables/index'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Pipeline triggers
@@ -48,5 +48,5 @@
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
impersonate their associated user including their access to projects and their project
permissions.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/triggers/index'
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 1927216e191..467f19b4c56 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -7,7 +7,7 @@
- if protected_tag?(@project, tag)
%span.label.label-success.prepend-left-4
- protected
+ = s_('TagsPage|protected')
- if tag.message.present?
&nbsp;
@@ -18,7 +18,7 @@
= render 'projects/branches/commit', commit: commit, project: @project
- else
%p
- Cant find HEAD commit for this tag
+ = s_("TagsPage|Can't find HEAD commit for this tag")
- if release && release.description.present?
.description.prepend-top-default
.wiki
@@ -28,9 +28,9 @@
= render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
+ = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
- = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
= icon("trash-o")
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 27d58d4c0e8..da364b58e36 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,16 +1,16 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
-- page_title "Tags"
+- page_title s_('TagsPage|Tags')
- add_to_breadcrumbs("Repository", project_tree_path(@project))
.flex-list{ class: container_class }
.top-area.adjust
.nav-text.row-main-content
- Tags give the ability to mark specific points in history as being important
+ = s_('TagsPage|Tags give the ability to mark specific points in history as being important')
.nav-controls.row-fixed-content
= form_tag(filter_tags_path, method: :get) do
- = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
@@ -19,13 +19,13 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
- Sort by
+ = s_('TagsPage|Sort by')
- tags_sort_options_hash.each do |value, title|
%li
= link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)
- if can?(current_user, :push_code, @project)
= link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do
- New tag
+ = s_('TagsPage|New tag')
.tags
- if @tags.any?
@@ -36,9 +36,9 @@
- else
.nothing-here-block
- Repository has no tags yet.
+ = s_('TagsPage|Repository has no tags yet.')
%br
%small
- Use git tag command to add a new one:
+ = s_('TagsPage|Use git tag command to add a new one:')
%br
%span.monospace git tag -a v1.4 -m 'version 1.4'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 521b4d927bc..031efa903c5 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -1,4 +1,4 @@
-- page_title "New Tag"
+- page_title s_('TagsPage|New Tag')
- default_ref = params[:ref] || @project.default_branch
- if @error
@@ -7,7 +7,7 @@
= @error
%h3.page-title
- New Tag
+ = s_('TagsPage|New Tag')
%hr
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do
@@ -23,21 +23,24 @@
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref
= render 'shared/ref_dropdown', dropdown_class: 'wide'
- .help-block Existing branch name, tag, or commit SHA
+ .help-block
+ = s_('TagsPage|Existing branch name, tag, or commit SHA')
.form-group
= label_tag :message, nil, class: 'control-label'
.col-sm-10
= text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5
- .help-block Optionally, add a message to the tag.
+ .help-block
+ = s_('TagsPage|Optionally, add a message to the tag.')
%hr
.form-group
- = label_tag :release_description, 'Release notes', class: 'control-label'
+ = label_tag :release_description, s_('TagsPage|Release notes'), class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here...", current_text: @release_description
+ = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description
= render 'shared/notes/hints'
- .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
+ .help-block
+ = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
.form-actions
- = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
- = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel'
+ = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create', tabindex: 3
+ = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 43aa2b27af6..dfe2c37ed8e 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
-- add_to_breadcrumbs "Tags", project_tags_path(@project)
+- add_to_breadcrumbs s_('TagsPage|Tags'), project_tags_path(@project)
- breadcrumb_title @tag.name
-- page_title @tag.name, "Tags"
+- page_title @tag.name, s_('TagsPage|Tags')
%div{ class: container_class }
.top-area.multi-line
@@ -12,25 +12,25 @@
= @tag.name
- if protected_tag?(@project, @tag)
%span.label.label-success
- protected
+ = s_('TagsPage|protected')
- if @commit
= render 'projects/branches/commit', commit: @commit, project: @project
- else
- Cant find HEAD commit for this tag
+ = s_("TagsPage|Can't find HEAD commit for this tag")
.nav-controls.controls-flex
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Edit release notes' do
+ = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= icon("pencil")
- = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse files' do
+ = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse files') do
= icon('files-o')
- = link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do
+ = link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse commits') do
= icon('history')
.btn-container.controls-item
= render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project)
.btn-container.controls-item-full
- = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do
%i.fa.fa-trash-o
- if @tag.message.present?
@@ -43,4 +43,4 @@
.wiki
= markdown_field(@release, :description)
- else
- This tag has no release notes.
+ = s_('TagsPage|This tag has no release notes.')
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 7ea19e6c828..c02f7ee37ed 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,14 +2,14 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- - if show_new_repo?
+ - if show_new_repo? && can_push_branch?(@project, @ref)
.js-new-dropdown
- else
= render 'projects/tree/old_tree_header'
.tree-controls
- if show_new_repo?
- = render 'shared/repo/editable_mode'
+ .editable-mode
- else
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
diff --git a/app/views/projects/tree/_truncated_notice_tree_row.html.haml b/app/views/projects/tree/_truncated_notice_tree_row.html.haml
new file mode 100644
index 00000000000..693b641888b
--- /dev/null
+++ b/app/views/projects/tree/_truncated_notice_tree_row.html.haml
@@ -0,0 +1,7 @@
+%tr.tree-truncated-warning
+ %td{ colspan: '3' }
+ = icon('exclamation-triangle fw')
+ %span
+ Too many items to show. To preserve performance only
+ %strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)}
+ items are displayed.
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
index 0a1ccbc5f1c..efa16d38f84 100644
--- a/app/views/projects/wikis/_pages_wiki_page.html.haml
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -2,4 +2,4 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
.pull-right
- %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe
+ %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 9ee09262324..969a1677d9a 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -21,7 +21,7 @@
%th= _("Last updated")
%th= _("Format")
%tbody
- - @page.versions.each_with_index do |version, index|
+ - @page_versions.each_with_index do |version, index|
- commit = version
%tr
%td
@@ -37,5 +37,6 @@
%td
%strong
= version.format
+= paginate @page_versions, theme: 'gitlab'
= render 'sidebar'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index de15fc99eda..b3b83cee81a 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -11,8 +11,8 @@
.nav-text
%h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe
- #{time_ago_with_tooltip(@page.commit.authored_date)}
+ = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
+ #{time_ago_with_tooltip(@page.last_version.authored_date)}
.nav-controls
= render 'main_links'
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 233d8c95eda..736afa085e8 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -11,6 +11,7 @@
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
- The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
+ The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}.
+ For repositories that take longer, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}.
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index dff847159d3..901a177323b 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -7,7 +7,7 @@
.stage-container.dropdown{ class: klass }
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
- = custom_icon(icon_status)
+ = sprite_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 6d7c9633913..f4a4bfaec54 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,3 +1,5 @@
+- show_create = local_assigns.fetch(:show_create, false)
+
- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
@@ -7,7 +9,7 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml
index 9b989c23cab..333dd1a00b4 100644
--- a/app/views/shared/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml
@@ -1,7 +1,5 @@
- if current_user
- .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span
- {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
+ .block.subscriptions
+ %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
+ ":subscribed" => "issue.subscribed",
+ ":id" => "issue.id" }
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index af6a499fadb..c80b179d525 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -11,7 +11,7 @@
= hook_log.trigger.singularize.titleize
%p
%strong Elapsed time:
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%p
%strong Request time:
= time_ago_with_tooltip(hook_log.created_at)
diff --git a/app/views/shared/icons/_add_new_project.svg b/app/views/shared/icons/_add_new_project.svg
index 3c1e15453df..cf8762944ca 100644
--- a/app/views/shared/icons/_add_new_project.svg
+++ b/app/views/shared/icons/_add_new_project.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18c4.418 0 8 3.582 8 8v22c0 4.418-3.582 8-8 8H30c-4.418 0-8-3.582-8-8V28c0-4.418 3.582-8 8-8z"/><path fill="#6B4FBB" d="M33 30h8c1.105 0 2 .895 2 2s-.895 2-2 2h-8c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg
index dde84e14048..7e47c084bde 100644
--- a/app/views/shared/icons/_icon_autodevops.svg
+++ b/app/views/shared/icons/_icon_autodevops.svg
@@ -29,7 +29,7 @@
</g>
<g fill-rule="nonzero" transform="rotate(15 -315.035 277.714)">
<path fill="#FFFFFF" d="M12.275,10.57 C13.986216,9.15630755 15.921048,8.03765363 18,7.26 L18,5.5 C18,2.463 20.47,0 23.493,0 L26.507,0 C27.9648848,0.000530018716 29.3628038,0.580386367 30.3930274,1.61192286 C31.4232511,2.64345935 32.0013267,4.04211574 32,5.5 L32,7.26 C34.098,8.043 36.03,9.17 37.725,10.57 L39.253,9.688 C41.8816141,8.17268496 45.2407537,9.07039379 46.763,11.695 L48.27,14.305 C48.9984289,15.5678669 49.1951495,17.0684426 48.8168566,18.4763972 C48.4385638,19.8843518 47.5162683,21.0842673 46.253,21.812 L44.728,22.693 C44.907,23.769 45,24.873 45,26 C45,27.127 44.907,28.231 44.728,29.307 L46.253,30.187 C48.8800379,31.705769 49.7822744,35.0642181 48.27,37.695 L46.763,40.305 C46.0335844,41.5673849 44.8323832,42.4881439 43.4238487,42.8645658 C42.0153143,43.2409877 40.5149245,43.0422119 39.253,42.312 L37.725,41.43 C36.013784,42.8436924 34.078952,43.9623464 32,44.74 L32,46.5 C32,49.537 29.53,52 26.507,52 L23.493,52 C22.0351152,51.99947 20.6371962,51.4196136 19.6069726,50.3880771 C18.5767489,49.3565406 17.9986733,47.9578843 18,46.5 L18,44.74 C15.921048,43.9623464 13.986216,42.8436924 12.275,41.43 L10.747,42.312 C8.11838594,43.827315 4.75924629,42.9296062 3.237,40.305 L1.73,37.695 C1.00157113,36.4321331 0.804850523,34.9315574 1.18314337,33.5236028 C1.56143621,32.1156482 2.48373172,30.9157327 3.747,30.188 L5.272,29.307 C5.09051204,28.2140265 4.9995366,27.107939 5,26 C5,24.873 5.093,23.769 5.272,22.693 L3.747,21.813 C1.11996213,20.294231 0.217725591,16.9357819 1.73,14.305 L3.237,11.695 C3.96641559,10.4326151 5.16761682,9.51185609 6.57615125,9.13543417 C7.98468568,8.75901226 9.48507553,8.95778814 10.747,9.688 L12.275,10.57 Z"/>
- <path class="animated spin infinite" fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/>
+ <path fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/>
<g transform="rotate(15 -59.137 82.348)">
<circle cx="8" cy="8" r="8" fill="#FFFFFF" transform="translate(.035 6.008)"/>
<path fill="#6B4FBB" d="M7.40192379,14.7679492 C2.98364579,14.7679492 -0.598076211,11.1862272 -0.598076211,6.76794919 C-0.598076211,2.34967119 2.98364579,-1.23205081 7.40192379,-1.23205081 C11.8202018,-1.23205081 15.4019238,2.34967119 15.4019238,6.76794919 C15.4019238,11.1862272 11.8202018,14.7679492 7.40192379,14.7679492 Z M7.40192379,10.7679492 C9.61106279,10.7679492 11.4019238,8.97708819 11.4019238,6.76794919 C11.4019238,4.55881019 9.61106279,2.76794919 7.40192379,2.76794919 C5.19278479,2.76794919 3.40192379,4.55881019 3.40192379,6.76794919 C3.40192379,8.97708819 5.19278479,10.7679492 7.40192379,10.7679492 Z"/>
@@ -37,7 +37,7 @@
</g>
<g fill-rule="nonzero" transform="rotate(15 -402.968 460.884)">
<path fill="#FFFFFF" d="M9.82,8.53730769 C11.1889728,7.39547918 12.7368384,6.49195101 14.4,5.86384615 L14.4,4.44230769 C14.4,1.98934615 16.376,0 18.7944,0 L21.2056,0 C22.3719078,0.00042809204 23.4902431,0.468773604 24.314422,1.30193769 C25.1386009,2.13510179 25.6010613,3.26478579 25.6,4.44230769 L25.6,5.86384615 C27.2784,6.49626923 28.824,7.40653846 30.18,8.53730769 L31.4024,7.82492308 C33.5052912,6.60101478 36.192603,7.32608729 37.4104,9.44596154 L38.616,11.5540385 C39.1987431,12.5740464 39.3561196,13.7860498 39.0534853,14.9232439 C38.750851,16.060438 38.0130146,17.0296006 37.0024,17.6173846 L35.7824,18.3289615 C35.9256,19.1980385 36,20.0897308 36,21 C36,21.9102692 35.9256,22.8019615 35.7824,23.6710385 L37.0024,24.3818077 C39.1040303,25.6085057 39.8258195,28.3210992 38.616,30.4459615 L37.4104,32.5540385 C36.8268675,33.573657 35.8659065,34.317347 34.739079,34.6213801 C33.6122515,34.9254132 32.4119396,34.7648634 31.4024,34.1750769 L30.18,33.4626923 C28.8110272,34.6045208 27.2631616,35.508049 25.6,36.1361538 L25.6,37.5576923 C25.6,40.0106538 23.624,42 21.2056,42 L18.7944,42 C17.6280922,41.9995719 16.5097569,41.5312264 15.685578,40.6980623 C14.8613991,39.8648982 14.3989387,38.7352142 14.4,37.5576923 L14.4,36.1361538 C12.7368384,35.508049 11.1889728,34.6045208 9.82,33.4626923 L8.5976,34.1750769 C6.49470875,35.3989852 3.80739703,34.6739127 2.5896,32.5540385 L1.384,30.4459615 C0.8012569,29.4259536 0.643880418,28.2139502 0.946514692,27.0767561 C1.24914897,25.939562 1.98698538,24.9703994 2.9976,24.3826154 L4.2176,23.6710385 C4.07240963,22.7882521 3.99962928,21.8948738 4,21 C4,20.0897308 4.0744,19.1980385 4.2176,18.3289615 L2.9976,17.6181923 C0.895969702,16.3914943 0.174180473,13.6789008 1.384,11.5540385 L2.5896,9.44596154 C3.17313247,8.42634297 4.13409345,7.682653 5.260921,7.37861991 C6.38774855,7.07458682 7.58806043,7.23513658 8.5976,7.82492308 L9.82,8.53730769 Z"/>
- <path class="animated spin infinite" fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/>
+ <path fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/>
<g transform="rotate(15 -47.892 66.043)">
<ellipse cx="6.4" cy="6.462" fill="#FFFFFF" rx="6.4" ry="6.462" transform="translate(.028 4.853)"/>
<path fill="#FC6D26" d="M5.92153903,11.9125743 C2.3834711,11.9125743 -0.478460969,9.0231237 -0.478460969,5.4664205 C-0.478460969,1.9097173 2.3834711,-0.979733345 5.92153903,-0.979733345 C9.45960696,-0.979733345 12.321539,1.9097173 12.321539,5.4664205 C12.321539,9.0231237 9.45960696,11.9125743 5.92153903,11.9125743 Z M5.92153903,8.71257435 C7.6854047,8.71257435 9.12153903,7.26263103 9.12153903,5.4664205 C9.12153903,3.67020997 7.6854047,2.22026666 5.92153903,2.22026666 C4.15767337,2.22026666 2.72153903,3.67020997 2.72153903,5.4664205 C2.72153903,7.26263103 4.15767337,8.71257435 5.92153903,8.71257435 Z"/>
diff --git a/app/views/shared/icons/_icon_hourglass.svg b/app/views/shared/icons/_icon_hourglass.svg
new file mode 100644
index 00000000000..fe7e497ce13
--- /dev/null
+++ b/app/views/shared/icons/_icon_hourglass.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></svg>
diff --git a/app/views/shared/icons/_lightbulb.svg b/app/views/shared/icons/_lightbulb.svg
new file mode 100644
index 00000000000..2fcc4c65f99
--- /dev/null
+++ b/app/views/shared/icons/_lightbulb.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm1 5h10c1.105 0 2 .895 2 2s-.895 2-2 2H34c-1.105 0-2-.895-2-2s.895-2 2-2z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36c.198-1.348.737-2.623 1.566-3.705 3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846.815 1.08 1.343 2.345 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1c-.097-.67-.36-1.303-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3-.416.54-.685 1.18-.784 1.853l-.346 2.36c-.288 1.958-1.963 3.41-3.942 3.42l-13.08.053c-1.994.008-3.69-1.455-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268zm-6 0c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268z"/></g></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index d3f0aa2d339..8442d7ff4a2 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,4 +1,3 @@
-- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
deleted file mode 100644
index 3f553c9fede..00000000000
--- a/app/views/shared/issuable/_participants.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- participants_row = 7
-- participants_size = participants.size
-- participants_extra = participants_size - participants_row
-.block.participants
- .sidebar-collapsed-icon
- = icon('users')
- %span
- = participants.count
- .title.hide-collapsed
- = pluralize participants.count, "participant"
- .hide-collapsed.participants-list
- - participants.each do |participant|
- .participants-author.js-participants-author
- = link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
- - if participants_extra > 0
- .hide-collapsed.participants-more
- %button.btn-transparent.btn-blank.js-participants-more{ type: 'button', data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
- + #{participants_extra} more
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7b7411b1e23..e0009a35b9f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -123,17 +123,10 @@
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
- = render "shared/issuable/participants", participants: issuable.participants(current_user)
+ .js-sidebar-participants-entry-point
+
- if current_user
- - subscribed = issuable.subscribed?(current_user, @project)
- .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
- .sidebar-collapsed-icon
- = icon('rss', 'aria-hidden': 'true')
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+ .js-sidebar-subscriptions-entry-point
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 951b4dd7b36..2c27dd638a7 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -104,7 +104,6 @@
class: 'btn btn-remove prepend-left-10'
- else
= link_to member,
- remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 305e2542281..7ba8f9d4313 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -49,6 +49,13 @@
= link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do
Edit
\
+
+ - if @project.group
+ = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do
+ Promote
+
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
+
= link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do
Delete
+
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index f03e0ab154c..4f51455c26e 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -85,6 +85,22 @@
Closed:
= milestone.issues_visible_to_user(current_user).closed.count
+ .block.time_spent
+ .sidebar-collapsed-icon
+ = custom_icon('icon_hourglass')
+ %span.collapsed-milestone-total-time-spent
+ - if milestone.human_total_issue_time_spent
+ = milestone.human_total_issue_time_spent
+ - else
+ = _("None")
+ .title.hide-collapsed
+ = _("Total issue time spent")
+ .value.hide-collapsed
+ - if milestone.human_total_issue_time_spent
+ %span.bold= milestone.human_total_issue_time_spent
+ - else
+ %span.no-value= _("No time spent")
+
.block.merge-requests
.sidebar-collapsed-icon
%strong
diff --git a/app/views/shared/repo/_editable_mode.html.haml b/app/views/shared/repo/_editable_mode.html.haml
deleted file mode 100644
index 73fdb8b523f..00000000000
--- a/app/views/shared/repo/_editable_mode.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.editable-mode
- %repo-edit-button
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
index 7861f92b33f..5867ea58378 100644
--- a/app/views/shared/repo/_repo.html.haml
+++ b/app/views/shared/repo/_repo.html.haml
@@ -1,11 +1,12 @@
#repo{ data: { root: @path.empty?.to_s,
+ root_url: project_tree_path(project),
url: content_url,
+ current_branch: @ref,
+ ref: @commit.id,
project_name: project.name,
- refs_url: refs_project_path(project, format: :json),
project_url: project_path(project),
project_id: project.id,
- blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
- new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }),
+ 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/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 8bbaf431536..ae437dd16d6 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -7,3 +7,4 @@
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag ("#{prefix}_scopes_#{scope}"), scope
%span= t(scope, scope: [:doorkeeper, :scopes])
+ .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
new file mode 100644
index 00000000000..899aed904e4
--- /dev/null
+++ b/app/workers/cluster_install_app_worker.rb
@@ -0,0 +1,11 @@
+class ClusterInstallAppWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+ include ClusterApplications
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::InstallService.new(app).execute
+ end
+ end
+end
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 63300b58a25..b01f9708424 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -3,8 +3,10 @@ class ClusterProvisionWorker
include ClusterQueue
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::ProvisionClusterService.new.execute(cluster)
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
+ end
end
end
end
diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb
new file mode 100644
index 00000000000..4bb8c293e5d
--- /dev/null
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -0,0 +1,14 @@
+class ClusterWaitForAppInstallationWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+ include ClusterApplications
+
+ INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::CheckInstallationProgressService.new(app).execute
+ end
+ end
+end
diff --git a/app/workers/concerns/cluster_applications.rb b/app/workers/concerns/cluster_applications.rb
new file mode 100644
index 00000000000..24ecaa0b52f
--- /dev/null
+++ b/app/workers/concerns/cluster_applications.rb
@@ -0,0 +1,9 @@
+module ClusterApplications
+ extend ActiveSupport::Concern
+
+ included do
+ def find_application(app_name, id, &blk)
+ Clusters::Cluster::APPLICATIONS[app_name].find(id).try(&blk)
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/notify_upon_death.rb b/app/workers/concerns/gitlab/github_import/notify_upon_death.rb
new file mode 100644
index 00000000000..3d7120665b6
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/notify_upon_death.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # NotifyUponDeath can be included into a GitHub worker class if it should
+ # notify any JobWaiter instances upon being moved to the Sidekiq dead queue.
+ #
+ # Note that this will only notify the waiter upon graceful termination, a
+ # SIGKILL will still result in the waiter _not_ being notified.
+ #
+ # Workers including this module must have jobs passed where the last
+ # argument is the key to notify, as a String.
+ module NotifyUponDeath
+ extend ActiveSupport::Concern
+
+ included do
+ # If a job is being exhausted we still want to notify the
+ # AdvanceStageWorker. This prevents the entire import from getting stuck
+ # just because 1 job threw too many errors.
+ sidekiq_retries_exhausted do |job|
+ args = job['args']
+ jid = job['jid']
+
+ if args.length == 3 && (key = args.last) && key.is_a?(String)
+ JobWaiter.notify(key, jid)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
new file mode 100644
index 00000000000..67e36c811de
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # ObjectImporter defines the base behaviour for every Sidekiq worker that
+ # imports a single resource such as a note or pull request.
+ module ObjectImporter
+ extend ActiveSupport::Concern
+
+ included do
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include ReschedulingMethods
+ include NotifyUponDeath
+ end
+
+ # project - An instance of `Project` to import the data into.
+ # client - An instance of `Gitlab::GithubImport::Client`
+ # hash - A Hash containing the details of the object to import.
+ def import(project, client, hash)
+ object = representation_class.from_json_hash(hash)
+
+ importer_class.new(object, project, client).execute
+
+ counter.increment(project: project.path_with_namespace)
+ end
+
+ def counter
+ @counter ||= Gitlab::Metrics.counter(counter_name, counter_description)
+ end
+
+ # Returns the representation class to use for the object. This class must
+ # define the class method `from_json_hash`.
+ def representation_class
+ raise NotImplementedError
+ end
+
+ # Returns the class to use for importing the object.
+ def importer_class
+ raise NotImplementedError
+ end
+
+ # Returns the name (as a Symbol) of the Prometheus counter.
+ def counter_name
+ raise NotImplementedError
+ end
+
+ # Returns the description (as a String) of the Prometheus counter.
+ def counter_description
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
new file mode 100644
index 00000000000..a2bee361b86
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module GithubImport
+ module Queue
+ extend ActiveSupport::Concern
+
+ included do
+ # If a job produces an error it may block a stage from advancing
+ # forever. To prevent this from happening we prevent jobs from going to
+ # the dead queue. This does mean some resources may not be imported, but
+ # this is better than a project being stuck in the "import" state
+ # forever.
+ sidekiq_options queue: 'github_importer', dead: false, retry: 5
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
new file mode 100644
index 00000000000..692ca6b7f42
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # Module that provides methods shared by the various workers used for
+ # importing GitHub projects.
+ module ReschedulingMethods
+ # project_id - The ID of the GitLab project to import the note into.
+ # hash - A Hash containing the details of the GitHub object to imoprt.
+ # notify_key - The Redis key to notify upon completion, if any.
+ def perform(project_id, hash, notify_key = nil)
+ project = Project.find_by(id: project_id)
+
+ return notify_waiter(notify_key) unless project
+
+ client = GithubImport.new_client_for(project, parallel: true)
+
+ if try_import(project, client, hash)
+ notify_waiter(notify_key)
+ else
+ # In the event of hitting the rate limit we want to reschedule the job
+ # so its retried after our rate limit has been reset.
+ self.class
+ .perform_in(client.rate_limit_resets_in, project.id, hash, notify_key)
+ end
+ end
+
+ def try_import(*args)
+ import(*args)
+ true
+ rescue RateLimitError
+ false
+ end
+
+ def notify_waiter(key = nil)
+ JobWaiter.notify(key, jid) if key
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
new file mode 100644
index 00000000000..147c8c8d683
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module StageMethods
+ # project_id - The ID of the GitLab project to import the data into.
+ def perform(project_id)
+ return unless (project = find_project(project_id))
+
+ client = GithubImport.new_client_for(project)
+
+ try_import(client, project)
+ end
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def try_import(client, project)
+ import(client, project)
+ rescue RateLimitError
+ self.class.perform_in(client.rate_limit_resets_in, project.id)
+ end
+
+ def find_project(id)
+ # If the project has been marked as failed we want to bail out
+ # automatically.
+ Project.import_started.find_by(id: id)
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
new file mode 100644
index 00000000000..877f88c043f
--- /dev/null
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # AdvanceStageWorker is a worker used by the GitHub importer to wait for a
+ # number of jobs to complete, without blocking a thread. Once all jobs have
+ # been completed this worker will advance the import process to the next
+ # stage.
+ class AdvanceStageWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'github_importer_advance_stage', dead: false
+
+ INTERVAL = 30.seconds.to_i
+
+ # The number of seconds to wait (while blocking the thread) before
+ # continueing to the next waiter.
+ BLOCKING_WAIT_TIME = 5
+
+ # The known importer stages and their corresponding Sidekiq workers.
+ STAGES = {
+ issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker,
+ notes: Stage::ImportNotesWorker,
+ finish: Stage::FinishImportWorker
+ }.freeze
+
+ # project_id - The ID of the project being imported.
+ # waiters - A Hash mapping Gitlab::JobWaiter keys to the number of
+ # remaining jobs.
+ # next_stage - The name of the next stage to start when all jobs have been
+ # completed.
+ def perform(project_id, waiters, next_stage)
+ return unless (project = find_project(project_id))
+
+ new_waiters = wait_for_jobs(waiters)
+
+ if new_waiters.empty?
+ # We refresh the import JID here so workers importing individual
+ # resources (e.g. notes) don't have to do this all the time, reducing
+ # the pressure on Redis. We _only_ do this once all jobs are done so
+ # we don't get stuck forever if one or more jobs failed to notify the
+ # JobWaiter.
+ project.refresh_import_jid_expiration
+
+ STAGES.fetch(next_stage.to_sym).perform_async(project_id)
+ else
+ self.class.perform_in(INTERVAL, project_id, new_waiters, next_stage)
+ end
+ end
+
+ def wait_for_jobs(waiters)
+ waiters.each_with_object({}) do |(key, remaining), new_waiters|
+ waiter = JobWaiter.new(remaining, key)
+
+ # We wait for a brief moment of time so we don't reschedule if we can
+ # complete the work fast enough.
+ waiter.wait(BLOCKING_WAIT_TIME)
+
+ next unless waiter.jobs_remaining.positive?
+
+ new_waiters[waiter.key] = waiter.jobs_remaining
+ end
+ end
+
+ def find_project(id)
+ # We only care about the import JID so we can refresh it. We also only
+ # want the project if it hasn't been marked as failed yet. It's possible
+ # the import gets marked as stuck when jobs of the current stage failed
+ # somehow.
+ Project.select(:import_jid).import_started.find_by(id: id)
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_diff_note_worker.rb b/app/workers/gitlab/github_import/import_diff_note_worker.rb
new file mode 100644
index 00000000000..ef2a74c51c5
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_diff_note_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportDiffNoteWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::DiffNote
+ end
+
+ def importer_class
+ Importer::DiffNoteImporter
+ end
+
+ def counter_name
+ :github_importer_imported_diff_notes
+ end
+
+ def counter_description
+ 'The number of imported GitHub pull request review comments'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_issue_worker.rb b/app/workers/gitlab/github_import/import_issue_worker.rb
new file mode 100644
index 00000000000..1b081ae5966
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_issue_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportIssueWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::Issue
+ end
+
+ def importer_class
+ Importer::IssueAndLabelLinksImporter
+ end
+
+ def counter_name
+ :github_importer_imported_issues
+ end
+
+ def counter_description
+ 'The number of imported GitHub issues'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_note_worker.rb b/app/workers/gitlab/github_import/import_note_worker.rb
new file mode 100644
index 00000000000..d2b4c36a5b9
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_note_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportNoteWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::Note
+ end
+
+ def importer_class
+ Importer::NoteImporter
+ end
+
+ def counter_name
+ :github_importer_imported_notes
+ end
+
+ def counter_description
+ 'The number of imported GitHub comments'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_pull_request_worker.rb b/app/workers/gitlab/github_import/import_pull_request_worker.rb
new file mode 100644
index 00000000000..62a6da152a3
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_pull_request_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportPullRequestWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::PullRequest
+ end
+
+ def importer_class
+ Importer::PullRequestImporter
+ end
+
+ def counter_name
+ :github_importer_imported_pull_requests
+ end
+
+ def counter_description
+ 'The number of imported GitHub pull requests'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
new file mode 100644
index 00000000000..45a38927225
--- /dev/null
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class RefreshImportJidWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+
+ # The interval to schedule new instances of this job at.
+ INTERVAL = 1.minute.to_i
+
+ def self.perform_in_the_future(*args)
+ perform_in(INTERVAL, *args)
+ end
+
+ # project_id - The ID of the project that is being imported.
+ # check_job_id - The ID of the job for which to check the status.
+ def perform(project_id, check_job_id)
+ return unless (project = find_project(project_id))
+
+ if SidekiqStatus.running?(check_job_id)
+ # As long as the repository is being cloned we want to keep refreshing
+ # the import JID status.
+ project.refresh_import_jid_expiration
+ self.class.perform_in_the_future(project_id, check_job_id)
+ end
+
+ # If the job is no longer running there's nothing else we need to do. If
+ # the clone job completed successfully it will have scheduled the next
+ # stage, if it died there's nothing we can do anyway.
+ end
+
+ def find_project(id)
+ Project.select(:import_jid).import_started.find_by(id: id)
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
new file mode 100644
index 00000000000..1a09497780a
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class FinishImportWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # project - An instance of Project.
+ def import(_, project)
+ project.after_import
+ report_import_time(project)
+ end
+
+ def report_import_time(project)
+ duration = Time.zone.now - project.created_at
+ path = project.path_with_namespace
+
+ histogram.observe({ project: path }, duration)
+ counter.increment
+
+ logger.info("GitHub importer finished for #{path} in #{duration.round(2)} seconds")
+ end
+
+ def histogram
+ @histogram ||= Gitlab::Metrics.histogram(
+ :github_importer_total_duration_seconds,
+ 'Total time spent importing GitHub projects, in seconds'
+ )
+ end
+
+ def counter
+ @counter ||= Gitlab::Metrics.counter(
+ :github_importer_imported_projects,
+ 'The number of imported GitHub projects'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
new file mode 100644
index 00000000000..f8a3684c6ba
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportBaseDataWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # These importers are fast enough that we can just run them in the same
+ # thread.
+ IMPORTERS = [
+ Importer::LabelsImporter,
+ Importer::MilestonesImporter,
+ Importer::ReleasesImporter
+ ].freeze
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ IMPORTERS.each do |klass|
+ klass.new(project, client).execute
+ end
+
+ project.refresh_import_jid_expiration
+
+ ImportPullRequestsWorker.perform_async(project.id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
new file mode 100644
index 00000000000..e110b7c1c36
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportIssuesAndDiffNotesWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # The importers to run in this stage. Issues can't be imported earlier
+ # on as we also use these to enrich pull requests with assigned labels.
+ IMPORTERS = [
+ Importer::IssuesImporter,
+ Importer::DiffNotesImporter
+ ].freeze
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ waiters = IMPORTERS.each_with_object({}) do |klass, hash|
+ waiter = klass.new(project, client).execute
+ hash[waiter.key] = waiter.jobs_remaining
+ end
+
+ AdvanceStageWorker.perform_async(project.id, waiters, :notes)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
new file mode 100644
index 00000000000..9810ed25cf9
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportNotesWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ waiter = Importer::NotesImporter
+ .new(project, client)
+ .execute
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :finish
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
new file mode 100644
index 00000000000..c531f26e897
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportPullRequestsWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ waiter = Importer::PullRequestsImporter
+ .new(project, client)
+ .execute
+
+ project.refresh_import_jid_expiration
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :issues_and_diff_notes
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
new file mode 100644
index 00000000000..aa5762e773d
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportRepositoryWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ # In extreme cases it's possible for a clone to take more than the
+ # import job expiration time. To work around this we schedule a
+ # separate job that will periodically run and refresh the import
+ # expiration time.
+ RefreshImportJidWorker.perform_in_the_future(project.id, jid)
+
+ importer = Importer::RepositoryImporter.new(project, client)
+
+ return unless importer.execute
+
+ counter.increment
+
+ ImportBaseDataWorker.perform_async(project.id)
+ end
+
+ def counter
+ Gitlab::Metrics.counter(
+ :github_importer_imported_repositories,
+ 'The number of imported GitHub repositories'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 3dd14466994..311fc187e49 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -104,6 +104,7 @@ class IrkerWorker
parents = commit.parents
# Return old value if there's no new one
return push_data['before'] if parents.empty?
+
# Or return the first parent-commit
parents[0].id
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d7c0043d3b6..4e90b137b26 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -17,11 +17,16 @@ class RepositoryImportWorker
import_url: project.import_url,
path: project.full_path)
- result = Projects::ImportService.new(project, project.creator).execute
+ service = Projects::ImportService.new(project, project.creator)
+ result = service.execute
+
+ # Some importers may perform their work asynchronously. In this case it's up
+ # to those importers to mark the import process as complete.
+ return if service.async?
+
raise ImportError, result[:message] if result[:status] == :error
- project.repository.after_import
- project.import_finish
+ project.after_import
rescue ImportError => ex
fail_import(project, ex.message)
raise
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 269776a1f62..fdbc049c2df 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -39,6 +39,7 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout)
search(status, timeout) do |build|
return unless build.stuck?
+
drop_build :stuck, build, status, timeout
end
end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 89ae17cef37..afc47fc63d6 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -2,6 +2,8 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ LOG_TIME_THRESHOLD = 90 # seconds
+
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
return unless project
@@ -9,6 +11,20 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id)
return unless user
- MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
+ # TODO: remove this benchmarking when we have rich logging
+ time = Benchmark.measure do
+ MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
+ end
+
+ args_log = [
+ "elapsed=#{time.real}",
+ "project_id=#{project_id}",
+ "user_id=#{user_id}",
+ "oldrev=#{oldrev}",
+ "newrev=#{newrev}",
+ "ref=#{ref}"
+ ].join(',')
+
+ Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD
end
end
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 5aa3bbdaa9d..241ed3901dc 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -2,25 +2,10 @@ class WaitForClusterCreationWorker
include Sidekiq::Worker
include ClusterQueue
- INITIAL_INTERVAL = 2.minutes
- EAGER_INTERVAL = 10.seconds
- TIMEOUT = 20.minutes
-
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::FetchGcpOperationService.new.execute(cluster) do |operation|
- case operation.status
- when 'RUNNING'
- if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc
- return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
- end
-
- WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id)
- when 'DONE'
- Ci::FinalizeClusterCreationService.new.execute(cluster)
- else
- return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
- end
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp?
end
end
end
diff --git a/changelogs/unreleased/1312-time-spent-at.yml b/changelogs/unreleased/1312-time-spent-at.yml
deleted file mode 100644
index c029497e9ab..00000000000
--- a/changelogs/unreleased/1312-time-spent-at.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added possibility to enter past date in /spend command to log time in the past
-merge_request: 3044
-author: g3dinua, LockiStrike
-type: changed
diff --git a/changelogs/unreleased/14970-suggest-rename-remote.yml b/changelogs/unreleased/14970-suggest-rename-remote.yml
deleted file mode 100644
index 68a77eb446d..00000000000
--- a/changelogs/unreleased/14970-suggest-rename-remote.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Suggest to rename the remote for existing repository instructions
-merge_request: 14970
-author: helmo42
-type: added
diff --git a/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml
new file mode 100644
index 00000000000..e3c7ffc8046
--- /dev/null
+++ b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Rubocop rule for line break after guard clause
+merge_request: 15188
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml
new file mode 100644
index 00000000000..b217cb44bf7
--- /dev/null
+++ b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml
@@ -0,0 +1,5 @@
+---
+title: Impersonation no longer gets stuck on password change.
+merge_request: 15497
+author:
+type: fixed
diff --git a/changelogs/unreleased/23000-pages-api.yml b/changelogs/unreleased/23000-pages-api.yml
deleted file mode 100644
index 9f6fa13dd07..00000000000
--- a/changelogs/unreleased/23000-pages-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add API endpoints for Pages Domains
-merge_request: 13917
-author: Travis Miller
-type: added
diff --git a/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml b/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml
deleted file mode 100644
index 8918c42e3fb..00000000000
--- a/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Issue JWT token with registry:catalog:* scope when requested by GitLab admin
-merge_request: 14751
-author: Vratislav Kalenda
-type: added
diff --git a/changelogs/unreleased/27654-retry-button.yml b/changelogs/unreleased/27654-retry-button.yml
deleted file mode 100644
index 11f3b5eb779..00000000000
--- a/changelogs/unreleased/27654-retry-button.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move retry button in job page to sidebar
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step5.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step5.yml
deleted file mode 100644
index 1bff4d6930d..00000000000
--- a/changelogs/unreleased/28202_decrease_abc_threshold_step5.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Decrease ABC threshold to 54.28
-merge_request: 14920
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/30140-restore-readme-only-preference.yml b/changelogs/unreleased/30140-restore-readme-only-preference.yml
deleted file mode 100644
index 4b4ee4d5be9..00000000000
--- a/changelogs/unreleased/30140-restore-readme-only-preference.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add readme only option as project view
-merge_request: 14900
-author:
-type: changed
diff --git a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step3.yml b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step3.yml
deleted file mode 100644
index 8ecb832041e..00000000000
--- a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step3.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Decrease Perceived Complexity threshold to 14
-merge_request: 14231
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/32098-pipelines-navigation.yml b/changelogs/unreleased/32098-pipelines-navigation.yml
new file mode 100644
index 00000000000..925c92b6be8
--- /dev/null
+++ b/changelogs/unreleased/32098-pipelines-navigation.yml
@@ -0,0 +1,6 @@
+---
+title: Stop reloading the page when using pagination and tabs - use API calls - in
+ Pipelines table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/32318-filter-icon.yml b/changelogs/unreleased/32318-filter-icon.yml
deleted file mode 100644
index 71e7c2c4dac..00000000000
--- a/changelogs/unreleased/32318-filter-icon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove filter icon from search bar
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml b/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml
new file mode 100644
index 00000000000..d61bbf2e355
--- /dev/null
+++ b/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml
@@ -0,0 +1,5 @@
+---
+title: Add internationalization support for the prometheus integration
+merge_request: 33338
+author:
+type: other
diff --git a/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml b/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml
deleted file mode 100644
index 816e1f83111..00000000000
--- a/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include the changes in issuable webhook payloads
-merge_request: 14308
-author:
-type: added
diff --git a/changelogs/unreleased/34600-performance-wiki-pages.yml b/changelogs/unreleased/34600-performance-wiki-pages.yml
new file mode 100644
index 00000000000..541ae8f8e60
--- /dev/null
+++ b/changelogs/unreleased/34600-performance-wiki-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Performance issues when loading large number of wiki pages
+merge_request: 15276
+author:
+type: performance
diff --git a/changelogs/unreleased/34841-todos.yml b/changelogs/unreleased/34841-todos.yml
deleted file mode 100644
index 37180eefbfc..00000000000
--- a/changelogs/unreleased/34841-todos.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bad type checking to prevent 0 count badge to be shown
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/34897-delete-branch-after-merge.yml b/changelogs/unreleased/34897-delete-branch-after-merge.yml
deleted file mode 100644
index 96631aa95c8..00000000000
--- a/changelogs/unreleased/34897-delete-branch-after-merge.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed 'Removed source branch' checkbox in merge widget being ignored.
-merge_request: 14832
-author:
-type: fixed
diff --git a/changelogs/unreleased/35199-case-insensitive-branches-search.yml b/changelogs/unreleased/35199-case-insensitive-branches-search.yml
deleted file mode 100644
index da2729e9e55..00000000000
--- a/changelogs/unreleased/35199-case-insensitive-branches-search.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Case insensitive search for branches
-merge_request: 14995
-author: George Andrinopoulos
-type: fixed
diff --git a/changelogs/unreleased/35644-refactor-have-http-status-into-have-gitlab-http-status.yml b/changelogs/unreleased/35644-refactor-have-http-status-into-have-gitlab-http-status.yml
deleted file mode 100644
index b03baab4950..00000000000
--- a/changelogs/unreleased/35644-refactor-have-http-status-into-have-gitlab-http-status.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactor have_http_status into have_gitlab_http_status
-merge_request: 14958
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml b/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml
deleted file mode 100644
index 7e2a7222162..00000000000
--- a/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix flash errors showing up on a non configured prometheus integration
-merge_request: 35652
-author:
-type: fixed
diff --git a/changelogs/unreleased/36160-zindex.yml b/changelogs/unreleased/36160-zindex.yml
deleted file mode 100644
index a836744fb41..00000000000
--- a/changelogs/unreleased/36160-zindex.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Decreases z-index of select2 to a lower number of our navigation bar
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37032-get-project-branch-invalid-name-message.yml b/changelogs/unreleased/37032-get-project-branch-invalid-name-message.yml
deleted file mode 100644
index 22651967a40..00000000000
--- a/changelogs/unreleased/37032-get-project-branch-invalid-name-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Get Project Branch API shows an helpful error message on invalid refname
-merge_request: 14884
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml b/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml
deleted file mode 100644
index bc93aa1fca4..00000000000
--- a/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace WikiPage::CreateService calls with wiki_page factory in specs
-merge_request: 14850
-author: Jacopo Beschi @jacopo-beschi
-type: changed
diff --git a/changelogs/unreleased/37660-match-sidebar-colors.yml b/changelogs/unreleased/37660-match-sidebar-colors.yml
deleted file mode 100644
index d5600f453e7..00000000000
--- a/changelogs/unreleased/37660-match-sidebar-colors.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change background color of nav sidebar to match other gl sidebars
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml b/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml
deleted file mode 100644
index 554249a3f88..00000000000
--- a/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Removed extra border radius from .file-editor and .file-holder when editing
- a file
-merge_request: 14803
-author: Rachel Pipkin
-type: fixed
diff --git a/changelogs/unreleased/38075_allow_refernce_integer_labels.yml b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
new file mode 100644
index 00000000000..b5342d4adf8
--- /dev/null
+++ b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors when selecting numeric-only labels in the labels autocomplete selector
+merge_request: 14607
+author: haseebeqx
+type: fixed
diff --git a/changelogs/unreleased/38178-fl-mr-notes-components.yml b/changelogs/unreleased/38178-fl-mr-notes-components.yml
deleted file mode 100644
index 244ccfb3071..00000000000
--- a/changelogs/unreleased/38178-fl-mr-notes-components.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Moves placeholders components into shared folder with documentation. Makes
- them easier to reuse in MR and Snippets comments
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml b/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml
deleted file mode 100644
index 48b92c02505..00000000000
--- a/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't create build failed todos when the job is automatically retried
-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
new file mode 100644
index 00000000000..c73cf8bf60b
--- /dev/null
+++ b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml
@@ -0,0 +1,5 @@
+---
+title: Changed validation error message on wrong milestone dates
+merge_request:
+author: Xurxo Méndez Pérez
+type: fixed
diff --git a/changelogs/unreleased/38720-sort-admin-runners.yml b/changelogs/unreleased/38720-sort-admin-runners.yml
deleted file mode 100644
index b1047644891..00000000000
--- a/changelogs/unreleased/38720-sort-admin-runners.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add sort runners on admin runners
-merge_request: 14661
-author: Takuya Noguchi
-type: added
diff --git a/changelogs/unreleased/38822-oauth-search-case-insensitive.yml b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml
new file mode 100644
index 00000000000..d84360b4c5c
--- /dev/null
+++ b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml
@@ -0,0 +1,5 @@
+---
+title: OAuth identity lookups case-insensitive
+merge_request: 15312
+author:
+type: fixed
diff --git a/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml b/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml
deleted file mode 100644
index 5e142a2b4cf..00000000000
--- a/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cleanup data-page attribute after each Karma test
-merge_request: 14742
-author:
-type: fixed
diff --git a/changelogs/unreleased/38986-due-date.yml b/changelogs/unreleased/38986-due-date.yml
deleted file mode 100644
index 7799b8d297e..00000000000
--- a/changelogs/unreleased/38986-due-date.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix timezone bug in Pikaday and upgrade Pikaday version
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml b/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml
deleted file mode 100644
index d142afa3433..00000000000
--- a/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Removed d3.js from the graph and users bundles and used the common_d3 bundle
- instead
-merge_request: 14826
-author:
-type: other
diff --git a/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml b/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml
deleted file mode 100644
index 4b90d68d80c..00000000000
--- a/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 14830 Move GitLab export option to top of import list when creating a new project
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/39054-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml b/changelogs/unreleased/39054-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml
deleted file mode 100644
index 47bf30ecb5a..00000000000
--- a/changelogs/unreleased/39054-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop merge requests with thousands of commits from timing out
-merge_request: 15063
-author:
-type: performance
diff --git a/changelogs/unreleased/39167-async-boards-sidebar.yml b/changelogs/unreleased/39167-async-boards-sidebar.yml
new file mode 100644
index 00000000000..dc77f1ad451
--- /dev/null
+++ b/changelogs/unreleased/39167-async-boards-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Update Issue Boards to fetch the notification subscription status asynchronously
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml b/changelogs/unreleased/39188-change-default-disabled-merge-message.yml
deleted file mode 100644
index 7de65f5c3f6..00000000000
--- a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update default disabled merge request widget message to reflect a general failure
-merge_request: 14960
-author:
-type: changed
diff --git a/changelogs/unreleased/39297-remove-help-text-group-lists.yml b/changelogs/unreleased/39297-remove-help-text-group-lists.yml
deleted file mode 100644
index 4773d3c5176..00000000000
--- a/changelogs/unreleased/39297-remove-help-text-group-lists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove help text from group issues page and group merge requests page
-merge_request: 14963
-author:
-type: removed
diff --git a/changelogs/unreleased/39335-add-time-spend-to-milestones.yml b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
new file mode 100644
index 00000000000..41a43418cbf
--- /dev/null
+++ b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Add total time spent to milestones
+merge_request: 15116
+author: George Andrinopoulos
+type: added
diff --git a/changelogs/unreleased/39366-email-confirmation-fails.yml b/changelogs/unreleased/39366-email-confirmation-fails.yml
deleted file mode 100644
index a5568670c70..00000000000
--- a/changelogs/unreleased/39366-email-confirmation-fails.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Fix bug preventing secondary emails from being confirmed'
-merge_request: 15010
-author:
-type: fixed
diff --git a/changelogs/unreleased/39419-remove-overzealous-tooltips.yml b/changelogs/unreleased/39419-remove-overzealous-tooltips.yml
deleted file mode 100644
index d6cf60bebfa..00000000000
--- a/changelogs/unreleased/39419-remove-overzealous-tooltips.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove overzealous tooltips in projects page tabs
-merge_request: 15017
-author:
-type: removed
diff --git a/changelogs/unreleased/39436-pages-api-administrative.yml b/changelogs/unreleased/39436-pages-api-administrative.yml
new file mode 100644
index 00000000000..f38bbbd479c
--- /dev/null
+++ b/changelogs/unreleased/39436-pages-api-administrative.yml
@@ -0,0 +1,5 @@
+---
+title: Add administrative endpoint to list all pages domains
+merge_request: 15160
+author: Travis Miller
+type: added
diff --git a/changelogs/unreleased/39441-bring-edit-form-back.yml b/changelogs/unreleased/39441-bring-edit-form-back.yml
deleted file mode 100644
index 025417e4da9..00000000000
--- a/changelogs/unreleased/39441-bring-edit-form-back.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix editing issue description in mobile view
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml
new file mode 100644
index 00000000000..36c2f789eeb
--- /dev/null
+++ b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml
@@ -0,0 +1,5 @@
+---
+title: Label addition/removal are not going to be redacted wrongfully in the API.
+merge_request: 15080
+author:
+type: fixed
diff --git a/changelogs/unreleased/39495-fix-bitbucket-login.yml b/changelogs/unreleased/39495-fix-bitbucket-login.yml
deleted file mode 100644
index b48d557108b..00000000000
--- a/changelogs/unreleased/39495-fix-bitbucket-login.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bitbucket login
-merge_request: 15051
-author:
-type: fixed
diff --git a/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml
new file mode 100644
index 00000000000..fc7c024f95a
--- /dev/null
+++ b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Add inline editing to issues on mobile
+merge_request: 15438
+author:
+type: changed
diff --git a/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml b/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml
deleted file mode 100644
index 66939d89d69..00000000000
--- a/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow to disable the Performance Bar
-merge_request: 15084
-author:
-type: fixed
diff --git a/changelogs/unreleased/39582-nestingdepth-6.yml b/changelogs/unreleased/39582-nestingdepth-6.yml
deleted file mode 100644
index efe15f0a5f3..00000000000
--- a/changelogs/unreleased/39582-nestingdepth-6.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable NestingDepth (level 6) on scss-lint
-merge_request: 15073
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/39583-reopen-issue-count-cache.yml b/changelogs/unreleased/39583-reopen-issue-count-cache.yml
deleted file mode 100644
index ee35bcbcdae..00000000000
--- a/changelogs/unreleased/39583-reopen-issue-count-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refresh open Issue and Merge Request project counter caches when re-opening.
-merge_request: 15085
-author: Rob Ede @robjtede
-type: fixed
diff --git a/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
new file mode 100644
index 00000000000..056afe43010
--- /dev/null
+++ b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Move update_project_counter_caches? out of issue and merge request
+merge_request: 15300
+author: George Andrinopoulos
+type: other
diff --git a/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml
new file mode 100644
index 00000000000..8b27c43d15b
--- /dev/null
+++ b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml
@@ -0,0 +1,5 @@
+---
+title: Fix commits page throwing 500 when the multi-file editor was enabled
+merge_request: 15502
+author:
+type: fixed
diff --git a/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
new file mode 100644
index 00000000000..580b97241e7
--- /dev/null
+++ b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
@@ -0,0 +1,6 @@
+---
+title: Fix pipeline status transition for single manual job. This would also fix pipeline
+ duration becuse it is depending on status transition
+merge_request: 15251
+author:
+type: fixed
diff --git a/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml b/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
new file mode 100644
index 00000000000..358c007387e
--- /dev/null
+++ b/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix acceptance of username for Mattermost service update
+merge_request: 15275
+author:
+type: fixed
diff --git a/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml b/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml
new file mode 100644
index 00000000000..b7a974fd8d9
--- /dev/null
+++ b/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Set the default gitlab-shell timeout to 3 hours
+merge_request: 15292
+author:
+type: fixed
diff --git a/changelogs/unreleased/40016-log-header.yml b/changelogs/unreleased/40016-log-header.yml
new file mode 100644
index 00000000000..f52c2d2a0d5
--- /dev/null
+++ b/changelogs/unreleased/40016-log-header.yml
@@ -0,0 +1,5 @@
+---
+title: Hide log size for mobile screens
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml b/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml
new file mode 100644
index 00000000000..a2ae2059c47
--- /dev/null
+++ b/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml
@@ -0,0 +1,5 @@
+---
+title: Add total_time_spent to the `changes` hash in issuable Webhook payloads
+merge_request: 15381
+author:
+type: changed
diff --git a/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml b/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml
new file mode 100644
index 00000000000..fdaa90f0d5d
--- /dev/null
+++ b/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml
@@ -0,0 +1,5 @@
+---
+title: Remove extra margin from wordmark in header
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml
new file mode 100644
index 00000000000..9c308321a19
--- /dev/null
+++ b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml
@@ -0,0 +1,5 @@
+---
+title: Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive'
+merge_request: 15493
+author:
+type: fixed
diff --git a/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml
new file mode 100644
index 00000000000..e5879f89156
--- /dev/null
+++ b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bitbucket wiki import with hashed storage enabled
+merge_request: 15490
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix_global_board_routes_39073.yml b/changelogs/unreleased/40377-blank-states.yml
index cc9ae8592db..7635602c68c 100644
--- a/changelogs/unreleased/fix_global_board_routes_39073.yml
+++ b/changelogs/unreleased/40377-blank-states.yml
@@ -1,5 +1,5 @@
---
-title: Allow boards as top level route
+title: Fix blank states using old css
merge_request:
author:
type: fixed
diff --git a/changelogs/unreleased/4080-align-retry-btn.yml b/changelogs/unreleased/4080-align-retry-btn.yml
new file mode 100644
index 00000000000..c7d3997839c
--- /dev/null
+++ b/changelogs/unreleased/4080-align-retry-btn.yml
@@ -0,0 +1,5 @@
+---
+title: Align retry button with job title with new grid size
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-lazy-option-to-user-avatar-image-component.yml b/changelogs/unreleased/add-lazy-option-to-user-avatar-image-component.yml
deleted file mode 100644
index eef78cd58f9..00000000000
--- a/changelogs/unreleased/add-lazy-option-to-user-avatar-image-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add lazy option to UserAvatarImage
-merge_request: 14895
-author:
-type: changed
diff --git a/changelogs/unreleased/add-shared-vue-loading-button.yml b/changelogs/unreleased/add-shared-vue-loading-button.yml
deleted file mode 100644
index a8904acc4e7..00000000000
--- a/changelogs/unreleased/add-shared-vue-loading-button.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add loading button for new UX paradigm
-merge_request: 14883
-author:
-type: added
diff --git a/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml b/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml
deleted file mode 100644
index 19d950b48d6..00000000000
--- a/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Avoid fetching all branches for branch existence checks
-merge_request: 14778
-author:
-type: changed
diff --git a/changelogs/unreleased/animate-auto-devops.yml b/changelogs/unreleased/animate-auto-devops.yml
deleted file mode 100644
index c572dbdd093..00000000000
--- a/changelogs/unreleased/animate-auto-devops.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Animate auto devops graphic
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/api-configure-jira.yml b/changelogs/unreleased/api-configure-jira.yml
deleted file mode 100644
index 3ac52d573b0..00000000000
--- a/changelogs/unreleased/api-configure-jira.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Validate username/pw for Jiraservice, require them in the API
-merge_request: 15025
-author: Robert Schilling
-type: fixed
diff --git a/changelogs/unreleased/api-doc-group-statistics.yml b/changelogs/unreleased/api-doc-group-statistics.yml
deleted file mode 100644
index 385ff978024..00000000000
--- a/changelogs/unreleased/api-doc-group-statistics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update the groups API documentation
-merge_request: 15024
-author: Robert Schilling
-type: fixed
diff --git a/changelogs/unreleased/brand_header_change.yml b/changelogs/unreleased/brand_header_change.yml
new file mode 100644
index 00000000000..6ea6e8192a4
--- /dev/null
+++ b/changelogs/unreleased/brand_header_change.yml
@@ -0,0 +1,5 @@
+---
+title: When a custom header logo is present, don't show GitLab type logo
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-circuitbreaker-backoff.yml b/changelogs/unreleased/bvl-circuitbreaker-backoff.yml
deleted file mode 100644
index 5cb90e7c085..00000000000
--- a/changelogs/unreleased/bvl-circuitbreaker-backoff.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Make the circuitbreaker more robust by adding higher thresholds, and multiple
- access attempts.
-merge_request: 14933
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-circuitbreaker-improvements.yml b/changelogs/unreleased/bvl-circuitbreaker-improvements.yml
deleted file mode 100644
index 15cbd5592e9..00000000000
--- a/changelogs/unreleased/bvl-circuitbreaker-improvements.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Store circuitbreaker settings in the database instead of config
-merge_request: 14842
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-delete-empty-fork-networks.yml b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml
new file mode 100644
index 00000000000..3bbb4cf6e3c
--- /dev/null
+++ b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up empty fork networks
+merge_request: 15373
+author:
+type: other
diff --git a/changelogs/unreleased/bvl-do-not-use-redis-keys.yml b/changelogs/unreleased/bvl-do-not-use-redis-keys.yml
deleted file mode 100644
index f703aad2065..00000000000
--- a/changelogs/unreleased/bvl-do-not-use-redis-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Forbid the usage of `Redis#keys`
-merge_request: 14889
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml
new file mode 100644
index 00000000000..e0895cb5d48
--- /dev/null
+++ b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Don't move repositories and attachments for projects using hashed storage
+merge_request: 15479
+author:
+type: other
diff --git a/changelogs/unreleased/bvl-dont-rename-free-names.yml b/changelogs/unreleased/bvl-dont-rename-free-names.yml
deleted file mode 100644
index 60a4ec8afbe..00000000000
--- a/changelogs/unreleased/bvl-dont-rename-free-names.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't rename paths that were freed up when upgrading
-merge_request: 15029
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-fix-group-atom-feed.yml b/changelogs/unreleased/bvl-fix-group-atom-feed.yml
deleted file mode 100644
index 48f67db7799..00000000000
--- a/changelogs/unreleased/bvl-fix-group-atom-feed.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the atom feed for group events
-merge_request: 14974
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml b/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml
deleted file mode 100644
index 2a7d80270ac..00000000000
--- a/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only cache last push event for existing projects when pushing to a fork
-merge_request: 14989
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml b/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml
deleted file mode 100644
index a17ed51c9b8..00000000000
--- a/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use the correct visibility attribute for projects in system hooks
-merge_request: 15065
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-group-trees.yml b/changelogs/unreleased/bvl-group-trees.yml
deleted file mode 100644
index 9f76eb81627..00000000000
--- a/changelogs/unreleased/bvl-group-trees.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show collapsible project lists
-merge_request: 14055
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml b/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml
new file mode 100644
index 00000000000..1114d429dec
--- /dev/null
+++ b/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml
@@ -0,0 +1,5 @@
+---
+title: Make sure a user can add projects to subgroups they have access to
+merge_request: 15294
+author:
+type: fixed
diff --git a/changelogs/unreleased/cleanup-issues-schema.yml b/changelogs/unreleased/cleanup-issues-schema.yml
new file mode 100644
index 00000000000..9f5fb0bdf82
--- /dev/null
+++ b/changelogs/unreleased/cleanup-issues-schema.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up schema of the "issues" table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml
new file mode 100644
index 00000000000..1049e94f312
--- /dev/null
+++ b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml
@@ -0,0 +1,5 @@
+---
+title: Enable UnnecessaryMantissa in scss-lint
+merge_request: 15255
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/es-module-broadcast_message.yml b/changelogs/unreleased/es-module-broadcast_message.yml
deleted file mode 100644
index 031bcc449ae..00000000000
--- a/changelogs/unreleased/es-module-broadcast_message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix unnecessary ajax requests in admin broadcast message form
-merge_request: 14853
-author:
-type: fixed
diff --git a/changelogs/unreleased/feature-reliable-rspec-with-eval-script.yml b/changelogs/unreleased/feature-reliable-rspec-with-eval-script.yml
deleted file mode 100644
index 1f36d84092a..00000000000
--- a/changelogs/unreleased/feature-reliable-rspec-with-eval-script.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Get true failure from evalulate_script by checking for element beforehand
-merge_request: 14898
-author:
-type: fixed
diff --git a/changelogs/unreleased/feature-ssh_host_fingerprint.yml b/changelogs/unreleased/feature-ssh_host_fingerprint.yml
deleted file mode 100644
index 04f9fd1d6ed..00000000000
--- a/changelogs/unreleased/feature-ssh_host_fingerprint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automatic configuration settings page
-merge_request: 13850
-author: Francisco Lopez
-type: added
diff --git a/changelogs/unreleased/feature_add_mermaid.yml b/changelogs/unreleased/feature_add_mermaid.yml
new file mode 100644
index 00000000000..caeb5d3470d
--- /dev/null
+++ b/changelogs/unreleased/feature_add_mermaid.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add support of Mermaid (generation of diagrams and flowcharts from text)'
+merge_request: 15107
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/fix-500-on-old-merge-requests.yml b/changelogs/unreleased/fix-500-on-old-merge-requests.yml
deleted file mode 100644
index 765d7466819..00000000000
--- a/changelogs/unreleased/fix-500-on-old-merge-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 500 errors caused by empty diffs in some discussions
-merge_request: 14945
-author: Alexander Popov
-type: fixed
diff --git a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml b/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml
deleted file mode 100644
index 0847b5f6733..00000000000
--- a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broken wiki pages that link to a wiki file
-merge_request: 15019
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-ci-pipelines-index.yml b/changelogs/unreleased/fix-ci-pipelines-index.yml
new file mode 100644
index 00000000000..772093fbef0
--- /dev/null
+++ b/changelogs/unreleased/fix-ci-pipelines-index.yml
@@ -0,0 +1,5 @@
+---
+title: Update composite pipelines index to include "id"
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-filter-by-my-reaction.yml b/changelogs/unreleased/fix-filter-by-my-reaction.yml
new file mode 100644
index 00000000000..8bf91ddf893
--- /dev/null
+++ b/changelogs/unreleased/fix-filter-by-my-reaction.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filter by my reaction is not working
+merge_request: 15345
+author: Hiroyuki Sato
+type: fixed
diff --git a/changelogs/unreleased/fix-project-select-js-without-button.yml b/changelogs/unreleased/fix-project-select-js-without-button.yml
deleted file mode 100644
index 389ca2394f0..00000000000
--- a/changelogs/unreleased/fix-project-select-js-without-button.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use project select dropdown not only as a combobutton
-merge_request: 15043
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-protected-branches-descriptions.yml b/changelogs/unreleased/fix-protected-branches-descriptions.yml
new file mode 100644
index 00000000000..8e233d9defd
--- /dev/null
+++ b/changelogs/unreleased/fix-protected-branches-descriptions.yml
@@ -0,0 +1,5 @@
+---
+title: Clarify wording of protected branch settings for the default branch
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
new file mode 100644
index 00000000000..198116f34aa
--- /dev/null
+++ b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
@@ -0,0 +1,5 @@
+---
+title: Only owner or master can erase jobs
+merge_request: 15216
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-system-hook-docs.yml b/changelogs/unreleased/fix-system-hook-docs.yml
deleted file mode 100644
index 393c84a2eff..00000000000
--- a/changelogs/unreleased/fix-system-hook-docs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clarify system_hook triggers in documentation
-merge_request: 14957
-author: Joe Marty
-type: other
diff --git a/changelogs/unreleased/fix-todos-last-page.yml b/changelogs/unreleased/fix-todos-last-page.yml
new file mode 100644
index 00000000000..efcdbb75e6e
--- /dev/null
+++ b/changelogs/unreleased/fix-todos-last-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix access to the final page of todos
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix_diff_parsing.yml b/changelogs/unreleased/fix_diff_parsing.yml
deleted file mode 100644
index 7a26b4f9ff5..00000000000
--- a/changelogs/unreleased/fix_diff_parsing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix diff parser so it tolerates to diff special markers in the content
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/hide-pipeline-zero-duration.yml b/changelogs/unreleased/hide-pipeline-zero-duration.yml
deleted file mode 100644
index 5d7a0983537..00000000000
--- a/changelogs/unreleased/hide-pipeline-zero-duration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hides pipeline duration in commit box when it is zero (nil)
-merge_request: 14979
-author: gvieira37
-type: fixed
diff --git a/changelogs/unreleased/improved-changes-dropdown.yml b/changelogs/unreleased/improved-changes-dropdown.yml
new file mode 100644
index 00000000000..f305cbe573b
--- /dev/null
+++ b/changelogs/unreleased/improved-changes-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Improved diff changed files dropdown design
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/issue-36484.yml b/changelogs/unreleased/issue-36484.yml
deleted file mode 100644
index a19126e650f..00000000000
--- a/changelogs/unreleased/issue-36484.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove unnecessary alt-texts from pipeline emails
-merge_request: 14602
-author: gernberg
-type: fixed
diff --git a/changelogs/unreleased/issue_39238.yml b/changelogs/unreleased/issue_39238.yml
new file mode 100644
index 00000000000..75a4969ca9e
--- /dev/null
+++ b/changelogs/unreleased/issue_39238.yml
@@ -0,0 +1,5 @@
+---
+title: Fix image diff notification email from showing wrong content
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_40337.yml b/changelogs/unreleased/issue_40337.yml
new file mode 100644
index 00000000000..0cd6c5f46a9
--- /dev/null
+++ b/changelogs/unreleased/issue_40337.yml
@@ -0,0 +1,5 @@
+---
+title: Fix promoting milestone updating all issuables without milestone
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml b/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml
deleted file mode 100644
index fe945e822fd..00000000000
--- a/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix widget of locked merge requests not being presented
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/merge-requests-schema-cleanup.yml b/changelogs/unreleased/merge-requests-schema-cleanup.yml
new file mode 100644
index 00000000000..ccce9b1436c
--- /dev/null
+++ b/changelogs/unreleased/merge-requests-schema-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up schema of the "merge_requests" table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/mk-add-user-rate-limits.yml b/changelogs/unreleased/mk-add-user-rate-limits.yml
new file mode 100644
index 00000000000..512757da5fc
--- /dev/null
+++ b/changelogs/unreleased/mk-add-user-rate-limits.yml
@@ -0,0 +1,6 @@
+---
+title: Add anonymous rate limit per IP, and authenticated (web or API) rate limits
+ per user
+merge_request: 14708
+author:
+type: added
diff --git a/changelogs/unreleased/move_markdown_preview_to_concern.yml b/changelogs/unreleased/move_markdown_preview_to_concern.yml
deleted file mode 100644
index 036e77610b9..00000000000
--- a/changelogs/unreleased/move_markdown_preview_to_concern.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for markdown preview to group milestones
-merge_request: 14806
-author: Vitaliy @blackst0ne Klachkov
-type: fixed
diff --git a/changelogs/unreleased/mr-14642.yml b/changelogs/unreleased/mr-14642.yml
deleted file mode 100644
index 048cc79e323..00000000000
--- a/changelogs/unreleased/mr-14642.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Auto Devops kubernetes default namespace is now correctly built out of gitlab
- project group-name
-merge_request: 14642
-author: Mircea Danila Dumitrescu
-type: fixed
diff --git a/changelogs/unreleased/multi-file-editor-submodules.yml b/changelogs/unreleased/multi-file-editor-submodules.yml
deleted file mode 100644
index b83a50957c5..00000000000
--- a/changelogs/unreleased/multi-file-editor-submodules.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added submodule support in multi-file editor
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/new-mr-repo-editor.yml b/changelogs/unreleased/new-mr-repo-editor.yml
deleted file mode 100644
index a6c15ee30a9..00000000000
--- a/changelogs/unreleased/new-mr-repo-editor.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Repo Editor: Add option to start a new MR directly from comit section'
-merge_request: 14665
-author:
-type: added
diff --git a/changelogs/unreleased/not-found-in-commits.yml b/changelogs/unreleased/not-found-in-commits.yml
deleted file mode 100644
index d5f9ff15a36..00000000000
--- a/changelogs/unreleased/not-found-in-commits.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Renders 404 in commits controller if no commits are found for a given path
-merge_request: 14610
-author: Guilherme Vieira
-type: fixed
diff --git a/changelogs/unreleased/osw-merge-process-logs.yml b/changelogs/unreleased/osw-merge-process-logs.yml
new file mode 100644
index 00000000000..d2bb0e09834
--- /dev/null
+++ b/changelogs/unreleased/osw-merge-process-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Add logs for monitoring the merge process
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/reduce-queries-for-artifacts-button.yml b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml
new file mode 100644
index 00000000000..f2d469b5a80
--- /dev/null
+++ b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml
@@ -0,0 +1,5 @@
+---
+title: Use arrays in Pipeline#latest_builds_with_artifacts
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/replace_explore_projects-feature.yml b/changelogs/unreleased/replace_explore_projects-feature.yml
deleted file mode 100644
index 85ef045fb4b..00000000000
--- a/changelogs/unreleased/replace_explore_projects-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'features/explore/projects.feature' spinach test with an rspec analog
-merge_request: 14755
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml b/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml
deleted file mode 100644
index 96e5195d247..00000000000
--- a/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broken Members link when relative URL root paths are used
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-container-registry-destroy.yml b/changelogs/unreleased/sh-fix-container-registry-destroy.yml
deleted file mode 100644
index 21a463da62a..00000000000
--- a/changelogs/unreleased/sh-fix-container-registry-destroy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix deletion of container registry or images returning an error
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-environment-write-ref.yml b/changelogs/unreleased/sh-fix-environment-write-ref.yml
deleted file mode 100644
index 8f291843ebe..00000000000
--- a/changelogs/unreleased/sh-fix-environment-write-ref.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the writing of invalid environment refs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-memoize-logger.yml b/changelogs/unreleased/sh-memoize-logger.yml
deleted file mode 100644
index 1b6567ce72f..00000000000
--- a/changelogs/unreleased/sh-memoize-logger.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Memoize GitLab logger to reduce open file descriptors
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sha-handling.yml b/changelogs/unreleased/sha-handling.yml
deleted file mode 100644
index d776edafef5..00000000000
--- a/changelogs/unreleased/sha-handling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 404 errors in API caused when the branch name had a dot
-merge_request: 14462
-author: gvieira37
-type: fixed
diff --git a/changelogs/unreleased/skip_confirmation_user_API.yml b/changelogs/unreleased/skip_confirmation_user_API.yml
new file mode 100644
index 00000000000..144ccd69e68
--- /dev/null
+++ b/changelogs/unreleased/skip_confirmation_user_API.yml
@@ -0,0 +1,7 @@
+---
+title: Add email confirmation parameters for user creation and update via API
+merge_request:
+author: Daniel Juarez
+type: added
+
+
diff --git a/changelogs/unreleased/tc-saml-fix-false-empty.yml b/changelogs/unreleased/tc-saml-fix-false-empty.yml
deleted file mode 100644
index 987f596475b..00000000000
--- a/changelogs/unreleased/tc-saml-fix-false-empty.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix SAML error 500 when no groups are defined for user
-merge_request: 14913
-author:
-type: fixed
diff --git a/changelogs/unreleased/text-utils.yml b/changelogs/unreleased/text-utils.yml
new file mode 100644
index 00000000000..b95bb82fe01
--- /dev/null
+++ b/changelogs/unreleased/text-utils.yml
@@ -0,0 +1,5 @@
+---
+title: Export text utils functions as es6 module and add tests
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml b/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml
new file mode 100644
index 00000000000..e509a8df6bc
--- /dev/null
+++ b/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml
@@ -0,0 +1,6 @@
+---
+title: 'Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma
+ in :cartwheel_tone4:'
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/update-fe-i18n-guide.yml b/changelogs/unreleased/update-fe-i18n-guide.yml
deleted file mode 100644
index 10bcf7836c6..00000000000
--- a/changelogs/unreleased/update-fe-i18n-guide.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update i18n section in FE docs for marking and interpolation
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/update-merge-worker-metrics.yml b/changelogs/unreleased/update-merge-worker-metrics.yml
new file mode 100644
index 00000000000..c733675926a
--- /dev/null
+++ b/changelogs/unreleased/update-merge-worker-metrics.yml
@@ -0,0 +1,5 @@
+---
+title: Add performance logging to UpdateMergeRequestsWorker.
+merge_request: 15360
+author:
+type: performance
diff --git a/changelogs/unreleased/use-git-branch-merged.yml b/changelogs/unreleased/use-git-branch-merged.yml
deleted file mode 100644
index 24ec226250c..00000000000
--- a/changelogs/unreleased/use-git-branch-merged.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve branch listing page performance
-merge_request: 14729
-author:
-type: performance
diff --git a/changelogs/unreleased/use-title.yml b/changelogs/unreleased/use-title.yml
deleted file mode 100644
index 647e282eb69..00000000000
--- a/changelogs/unreleased/use-title.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use title as placeholder instead of issue title for reusability
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/winh-subgroups-api.yml b/changelogs/unreleased/winh-subgroups-api.yml
new file mode 100644
index 00000000000..c49e3621e9c
--- /dev/null
+++ b/changelogs/unreleased/winh-subgroups-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add /groups/:id/subgroups endpoint to API
+merge_request: 15142
+author: marbemac
+type: added
diff --git a/changelogs/unreleased/zj-add-performance-changelog-cat.yml b/changelogs/unreleased/zj-add-performance-changelog-cat.yml
deleted file mode 100644
index 3d58044a254..00000000000
--- a/changelogs/unreleased/zj-add-performance-changelog-cat.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Performance improvement as category on the changelog
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-commit-cache.yml b/changelogs/unreleased/zj-commit-cache.yml
deleted file mode 100644
index e3afe0ea7ef..00000000000
--- a/changelogs/unreleased/zj-commit-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache commits fetched from the repository
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-commit-show-n-1.yml b/changelogs/unreleased/zj-commit-show-n-1.yml
new file mode 100644
index 00000000000..e536434f74a
--- /dev/null
+++ b/changelogs/unreleased/zj-commit-show-n-1.yml
@@ -0,0 +1,5 @@
+---
+title: Fetch blobs in bulk when generating diffs
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-peek-gitaly.yml b/changelogs/unreleased/zj-peek-gitaly.yml
deleted file mode 100644
index bd2f2a07540..00000000000
--- a/changelogs/unreleased/zj-peek-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Gitaly metrics to the performance bar
-merge_request:
-author:
-type: other
diff --git a/config/application.rb b/config/application.rb
index 5100ec5d2b7..6436f887d14 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -113,7 +113,7 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
- config.middleware.insert_before Warden::Manager, Rack::Attack
+ config.middleware.insert_after Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains
config.middleware.insert_before Warden::Manager, Rack::Cors do
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 3af7f7bd5c0..60df92a44fc 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -459,9 +459,9 @@
:versions: []
:when: 2017-09-13 17:31:16.425819400 Z
- - :approve
- - gitlab-svgs
+ - "@gitlab-org/gitlab-svgs"
- :who: Tim Zallmann
- :why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs
+ :why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
:versions: []
:when: 2017-09-19 14:36:32.795496000 Z
- - :license
@@ -471,3 +471,35 @@
:why:
:versions: []
:when: 2017-10-17 17:46:12.367554000 Z
+- - :license
+ - component-emitter
+ - MIT
+ - :who: Winnie Hellmann
+ :why: package.json does not specify the license (README.md does)
+ :versions:
+ - 1.1.2
+ :when: 2017-11-13 12:23:10.502463000 Z
+- - :license
+ - json-schema
+ - BSD
+ - :who: Winnie Hellmann
+ :why: https://github.com/kriszyp/json-schema/blob/v0.2.3/package.json#L18-L19
+ :versions:
+ - 0.2.3
+ :when: 2017-11-16 12:52:18.286091000 Z
+- - :license
+ - node-forge
+ - New BSD
+ - :who: Winnie Hellmann
+ :why: https://github.com/digitalbazaar/forge/blob/0.6.33/LICENSE
+ :versions:
+ - 0.6.33
+ :when: 2017-11-16 12:56:17.974767000 Z
+- - :license
+ - sntp
+ - BSD
+ - :who: Winnie Hellmann
+ :why: https://github.com/hueniverse/sntp/blob/v1.0.9/package.json#L28-L29
+ :versions:
+ - 1.0.9
+ :when: 2017-11-16 13:02:06.765282000 Z
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1edb6fd39b8..d09e51e766a 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,6 +1,7 @@
Rails.application.configure do
# Make sure the middleware is inserted first in middleware chain
config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware')
+ config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestInspectorMiddleware')
# Settings specified here will take precedence over those in config/application.rb
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 4bfa5be0136..7f6e68ceed6 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -501,7 +501,7 @@ production: &base
# Gitaly settings
gitaly:
# Path to the directory containing Gitaly client executables.
- client_path: /home/git/gitaly
+ client_path: /home/git/gitaly/bin
# Default Gitaly authentication token. Can be overriden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly.
@@ -559,8 +559,8 @@ production: &base
upload_pack: true
receive_pack: true
- # Git import/fetch timeout
- # git_timeout: 800
+ # Git import/fetch timeout, in seconds. Defaults to 3 hours.
+ # git_timeout: 10800
# If you use non-standard ssh port you need to specify it
# ssh_port: 22
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 12694f8016f..f7c83f7b0f7 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -429,7 +429,7 @@ Settings.gitlab_shell['ssh_port'] ||= 22
Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user
Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user
Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.__send__(:build_gitlab_shell_ssh_path_prefix)
-Settings.gitlab_shell['git_timeout'] ||= 800
+Settings.gitlab_shell['git_timeout'] ||= 10800
#
# Workhorse
@@ -535,6 +535,7 @@ Settings.webpack.dev_server['port'] ||= 3808
Settings['monitoring'] ||= Settingslogic.new({})
Settings.monitoring['ip_whitelist'] ||= ['127.0.0.1/8']
Settings.monitoring['unicorn_sampler_interval'] ||= 10
+Settings.monitoring['ruby_sampler_interval'] ||= 60
Settings.monitoring['sidekiq_exporter'] ||= Settingslogic.new({})
Settings.monitoring.sidekiq_exporter['enabled'] ||= false
Settings.monitoring.sidekiq_exporter['address'] ||= 'localhost'
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 31839297523..e8f33593fe0 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -11,7 +11,15 @@ Prometheus::Client.configure do |config|
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
end
- config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
+ config.pid_provider = -> do
+ wid = Prometheus::Client::Support::Unicorn.worker_id
+ wid = Process.pid if wid.nil?
+ if wid.nil?
+ "process_pid_#{Process.pid}"
+ else
+ "worker_id_#{wid}"
+ end
+ end
end
Sidekiq.configure_server do |config|
@@ -19,3 +27,11 @@ Sidekiq.configure_server do |config|
Gitlab::Metrics::SidekiqMetricsExporter.instance.start
end
end
+
+if Gitlab::Metrics.prometheus_metrics_enabled?
+ unless Sidekiq.server?
+ Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+ end
+
+ Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
+end
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index e1a59d8c152..7ef594836d6 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -77,7 +77,6 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
instrumentation.instrument_instance_methods(Banzai::Redactor)
- instrumentation.instrument_methods(Banzai::NoteRenderer)
[Issuable, Mentionable, Participable].each do |klass|
instrumentation.instrument_instance_methods(klass)
@@ -116,15 +115,9 @@ def instrument_classes(instrumentation)
# Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159
instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits)
-
- # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/36061
- instrumentation.instrument_instance_method(MergeRequest, :ensure_ref_fetched)
- instrumentation.instrument_instance_method(MergeRequest, :fetch_ref)
end
# rubocop:enable Metrics/AbcSize
-Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
-
Gitlab::Application.configure do |config|
# 0 should be Sentry to catch errors in this middleware
config.middleware.insert(1, Gitlab::Metrics::RequestsRackMiddleware)
@@ -190,7 +183,7 @@ if Gitlab::Metrics.enabled?
GC::Profiler.enable
- Gitlab::Metrics::InfluxSampler.initialize_instance.start
+ Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start
module TrackNewRedisConnections
def connect(*args)
diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb
index 35e8b3808e2..6ebaf8834d2 100644
--- a/config/initializers/ar5_batching.rb
+++ b/config/initializers/ar5_batching.rb
@@ -34,6 +34,7 @@ module ActiveRecord
yield yielded_relation
break if ids.length < of
+
batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
end
end
diff --git a/config/initializers/batch_loader.rb b/config/initializers/batch_loader.rb
new file mode 100644
index 00000000000..2e2256b0eb9
--- /dev/null
+++ b/config/initializers/batch_loader.rb
@@ -0,0 +1 @@
+Rails.application.config.middleware.use(BatchLoader::Middleware)
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index c6ec0aeda7b..051ef93b205 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -195,7 +195,7 @@ Devise.setup do |config|
config.navigational_formats = [:"*/*", "*/*", :html, :zip]
# The default HTTP method used to sign out a resource. Default is :delete.
- config.sign_out_via = :delete
+ config.sign_out_via = :get
# ==> OmniAuth
# To configure a new OmniAuth provider copy and edit omniauth.rb.sample
@@ -236,6 +236,7 @@ Devise.setup do |config|
provider['args'][:on_single_sign_out] = lambda do |request|
ticket = request.params[:session_index]
raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket)
+
Gitlab::OAuth::Session.destroy(:cas3, ticket)
true
end
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 1ebe3c7a742..2fd47a3f4d3 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -10,4 +10,32 @@ module Gollum
index.send(name, *args)
end
end
+
+ class Wiki
+ def pages(treeish = nil, limit: nil)
+ tree_list((treeish || @ref), limit: limit)
+ end
+
+ def tree_list(ref, limit: nil)
+ if (sha = @access.ref_to_sha(ref))
+ commit = @access.commit(sha)
+ tree_map_for(sha).inject([]) do |list, entry|
+ next list unless @page_class.valid_page_name?(entry.name)
+
+ list << entry.page(self, commit)
+ break list if limit && list.size >= limit
+
+ list
+ end
+ else
+ []
+ end
+ end
+ end
+end
+
+Rails.application.configure do
+ config.after_initialize do
+ Gollum::Page.per_page = Kaminari.config.default_per_page
+ end
end
diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb
deleted file mode 100644
index 8a3388a267e..00000000000
--- a/config/initializers/math_lexer.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-# Touch the lexers so it is registered with Rouge
-Rouge::Lexers::Math
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index fddb018e948..e9e1f1c4e9b 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -3,6 +3,7 @@ if Gitlab::LDAP::Config.enabled?
Gitlab::LDAP::Config.available_servers.each do |server|
# do not redeclare LDAP
next if server['provider_name'] == 'ldap'
+
const_set(server['provider_class'], Class.new(LDAP))
end
end
diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb
deleted file mode 100644
index e8a77b146fa..00000000000
--- a/config/initializers/plantuml_lexer.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-# Touch the lexers so it is registered with Rouge
-Rouge::Lexers::Plantuml
diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb
index 7f0df8949db..38a9cd68d57 100644
--- a/config/initializers/postgresql_cte.rb
+++ b/config/initializers/postgresql_cte.rb
@@ -61,11 +61,13 @@ module ActiveRecord
def with_values=(values)
raise ImmutableRelation if @loaded
+
@values[:with] = values
end
def recursive_value=(value)
raise ImmutableRelation if @loaded
+
@values[:recursive] = value
end
diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb
new file mode 100644
index 00000000000..9453df2ec5a
--- /dev/null
+++ b/config/initializers/rack_attack_global.rb
@@ -0,0 +1,61 @@
+module Gitlab::Throttle
+ def self.settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ def self.unauthenticated_options
+ limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period }
+ period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+
+ def self.authenticated_api_options
+ limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period }
+ period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+
+ def self.authenticated_web_options
+ limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period }
+ period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+end
+
+class Rack::Attack
+ throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
+ Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
+ req.unauthenticated? &&
+ req.ip
+ end
+
+ throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
+ Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
+ req.api_request? &&
+ req.authenticated_user_id
+ end
+
+ throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
+ Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
+ req.web_request? &&
+ req.authenticated_user_id
+ end
+
+ class Request
+ def unauthenticated?
+ !authenticated_user_id
+ end
+
+ def authenticated_user_id
+ Gitlab::Auth::RequestAuthenticator.new(self).user&.id
+ end
+
+ def api_request?
+ path.start_with?('/api')
+ end
+
+ def web_request?
+ !api_request?
+ end
+ end
+end
diff --git a/config/karma.config.js b/config/karma.config.js
index e459f5cdac3..9f018d14b8f 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -67,14 +67,5 @@ module.exports = function(config) {
karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1');
}
- if (process.env.DEBUG) {
- karmaConfig.logLevel = config.LOG_DEBUG;
- process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log';
- }
-
- if (process.env.CHROME_LOG_FILE) {
- karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1');
- }
-
config.set(karmaConfig);
};
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 14d49885fb3..b1c71095d4f 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -58,10 +58,19 @@ en:
expired: "The access token expired"
unknown: "The access token is invalid"
scopes:
- api: Access your API
- read_user: Read user information
+ api: Access the authenticated user's API
+ read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect
-
+ sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
+ scope_desc:
+ api:
+ Full access to GitLab as the user, including read/write on all their groups and projects
+ read_user:
+ Read-only access to the user's profile information, like username, public email and full name
+ openid:
+ The ability to authenticate using GitLab, and read-only access to the user's profile information
+ sudo:
+ Access to the Sudo feature, to perform API actions as any user in the system (only available for admins)
flash:
applications:
create:
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 33b897f46e2..601a86490d4 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -145,7 +145,7 @@
- container_memory_usage_bytes
weight: 1
queries:
- - query_range: '(sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024'
+ - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024'
label: Average
unit: MB
- title: "CPU Utilization"
@@ -154,8 +154,6 @@
- container_cpu_usage_seconds_total
weight: 1
queries:
- - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100'
- label: CPU
- unit: "%"
- series:
- - label: cpu
+ - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
+ label: Average
+ unit: "%" \ No newline at end of file
diff --git a/config/routes/group.rb b/config/routes/group.rb
index f4d520a2518..db99e10bb9a 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -8,24 +8,33 @@ constraints(GroupUrlConstrainer.new) do
scope(path: 'groups/*id',
controller: :groups,
constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
- get :edit, as: :edit_group
- get :issues, as: :issues_group
- get :merge_requests, as: :merge_requests_group
- get :projects, as: :projects_group
- get :activity, as: :activity_group
+ scope(path: '-') do
+ get :edit, as: :edit_group
+ get :issues, as: :issues_group
+ get :merge_requests, as: :merge_requests_group
+ get :projects, as: :projects_group
+ get :activity, as: :activity_group
+ end
+
get '/', action: :show, as: :group_canonical
end
- scope(path: 'groups/*group_id',
+ scope(path: 'groups/*group_id/-',
module: :groups,
as: :group,
constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
- resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
- post :resend_invite, on: :member
- delete :leave, on: :collection
+ namespace :settings do
+ resource :ci_cd, only: [:show], controller: 'ci_cd'
+ end
+
+ resources :variables, only: [:index, :show, :update, :create, :destroy]
+
+ resources :children, only: [:index]
+
+ resources :labels, except: [:show] do
+ post :toggle_subscription, on: :member
end
- resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :edit, :update, :new, :create] do
member do
get :merge_requests
@@ -34,18 +43,11 @@ constraints(GroupUrlConstrainer.new) do
end
end
- resources :labels, except: [:show] do
- post :toggle_subscription, on: :member
- end
-
- scope path: '-' do
- namespace :settings do
- resource :ci_cd, only: [:show], controller: 'ci_cd'
- end
-
- resources :variables, only: [:index, :show, :update, :create, :destroy]
+ resource :avatar, only: [:destroy]
- resources :children, only: [:index]
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+ post :resend_invite, on: :member
+ delete :leave, on: :collection
end
end
@@ -58,4 +60,12 @@ constraints(GroupUrlConstrainer.new) do
put '/', action: :update
delete '/', action: :destroy
end
+
+ # Legacy paths should be defined last, so they would be ignored if routes with
+ # one of the previously reserved words exist.
+ scope(path: 'groups/*group_id') do
+ Gitlab::Routing.redirect_legacy_paths(self, :labels, :milestones, :group_members,
+ :edit, :issues, :merge_requests, :projects,
+ :activity)
+ end
end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index ddc852f0132..bcfc17a5f66 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do
get :audit_log
get :applications, to: 'oauth/applications#index'
- put :reset_private_token
put :reset_incoming_email_token
put :reset_rss_token
put :update_username
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 9f553085d50..bdafaba3ab3 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -186,10 +186,15 @@ constraints(ProjectUrlConstrainer.new) do
resources :clusters, except: [:edit] do
collection do
get :login
+ get '/providers/gcp/new', action: :new_gcp
end
member do
get :status, format: :json
+
+ scope :applications do
+ post '/:application', to: 'clusters/applications#create', as: :install_applications
+ end
end
end
@@ -293,6 +298,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :milestones, constraints: { id: /\d+/ } do
member do
+ post :promote
put :sort_issues
put :sort_merge_requests
get :merge_requests
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index e2bb766ee47..a8b918177de 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -40,6 +40,8 @@
- [upload_checksum, 1]
- [repository_fork, 1]
- [repository_import, 1]
+ - [github_importer, 1]
+ - [github_importer_advance_stage, 1]
- [project_service, 1]
- [delete_user, 1]
- [delete_merged_branches, 1]
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 383782112a8..96c6d954ff7 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -4,7 +4,7 @@ require './spec/support/test_env'
class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false)
@project = project
- @user = User.order(:id).last
+ @user = User.admins.first
@issue_count = perf ? 1000 : 5
stub_git_pre_receive!
end
@@ -77,39 +77,41 @@ class Gitlab::Seeder::CycleAnalytics
end
def seed!
- Sidekiq::Testing.inline! do
- issues = create_issues
- puts '.'
-
- # Stage 1
- Timecop.travel 5.days.from_now
- add_milestones_and_list_labels(issues)
- print '.'
-
- # Stage 2
- Timecop.travel 5.days.from_now
- branches = mention_in_commits(issues)
- print '.'
-
- # Stage 3
- Timecop.travel 5.days.from_now
- merge_requests = create_merge_requests_closing_issues(issues, branches)
- print '.'
-
- # Stage 4
- Timecop.travel 5.days.from_now
- run_builds(merge_requests)
- print '.'
-
- # Stage 5
- Timecop.travel 5.days.from_now
- merge_merge_requests(merge_requests)
- print '.'
-
- # Stage 6 / 7
- Timecop.travel 5.days.from_now
- deploy_to_production(merge_requests)
- print '.'
+ Sidekiq::Worker.skipping_transaction_check do
+ Sidekiq::Testing.inline! do
+ issues = create_issues
+ puts '.'
+
+ # Stage 1
+ Timecop.travel 5.days.from_now
+ add_milestones_and_list_labels(issues)
+ print '.'
+
+ # Stage 2
+ Timecop.travel 5.days.from_now
+ branches = mention_in_commits(issues)
+ print '.'
+
+ # Stage 3
+ Timecop.travel 5.days.from_now
+ merge_requests = create_merge_requests_closing_issues(issues, branches)
+ print '.'
+
+ # Stage 4
+ Timecop.travel 5.days.from_now
+ run_builds(merge_requests)
+ print '.'
+
+ # Stage 5
+ Timecop.travel 5.days.from_now
+ merge_merge_requests(merge_requests)
+ print '.'
+
+ # Stage 6 / 7
+ Timecop.travel 5.days.from_now
+ deploy_to_production(merge_requests)
+ print '.'
+ end
end
print '.'
@@ -123,7 +125,7 @@ class Gitlab::Seeder::CycleAnalytics
title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}",
description: FFaker::Lorem.sentence,
state: 'opened',
- assignee: @project.team.users.sample
+ assignees: [@project.team.users.sample]
}
Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute
@@ -155,7 +157,7 @@ class Gitlab::Seeder::CycleAnalytics
issue.project.repository.add_branch(@user, branch_name, 'master')
- commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name)
+ commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for #{issue.to_reference}", branch_name: branch_name)
issue.project.repository.commit(commit_sha)
GitPushService.new(issue.project,
@@ -210,6 +212,8 @@ class Gitlab::Seeder::CycleAnalytics
def deploy_to_production(merge_requests)
merge_requests.each do |merge_request|
+ next unless merge_request.head_pipeline
+
Timecop.travel 12.hours.from_now
job = merge_request.head_pipeline.builds.where.not(environment: nil).last
@@ -223,7 +227,14 @@ Gitlab::Seeder.quiet do
flag = 'SEED_CYCLE_ANALYTICS'
if ENV[flag]
- Project.all.each do |project|
+ Project.find_each do |project|
+ # This seed naively assumes that every project has a repository, and every
+ # repository has a `master` branch, which may be the case for a pristine
+ # GDK seed, but is almost never true for a GDK that's actually had
+ # development performed on it.
+ next unless project.repository_exists?
+ next unless project.repository.commit('master')
+
seeder = Gitlab::Seeder::CycleAnalytics.new(project)
seeder.seed!
end
diff --git a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
index 6f22641077d..35df121519e 100644
--- a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
+++ b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
@@ -8,7 +8,11 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ # We put condition here because of a mistake we made a couple of years ago
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/39382#note_45716103
+ unless column_exists?(:projects, :merge_requests_ff_only_enabled)
+ add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ end
end
def down
diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
index 22bac46e25c..1716b6e8153 100644
--- a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
+++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160608195742_add_repository_storage_to_projects.rb b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
index 0f3664c13ef..e4febd1614d 100644
--- a/db/migrate/20160608195742_add_repository_storage_to_projects.rb
+++ b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddRepositoryStorageToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
index 5336b036bca..c58cb957df4 100644
--- a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
+++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class SetMissingStageOnCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
index 5dc26f8982a..22c925799a3 100644
--- a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
+++ b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddRequestAccessEnabledToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
index 4a317646788..4fcb29e1325 100644
--- a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
+++ b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddRequestAccessEnabledToGroups < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
index abe8e701e23..58f7f2a2841 100644
--- a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
+++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
index 7414a28ac97..aec709aaf59 100644
--- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
+++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
@@ -1,7 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
index 0100e30a733..df7d922b816 100644
--- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
@@ -1,7 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class RemoveProjectsPushesSinceGc < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
index ae37da275fd..27ebe0af33b 100644
--- a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
+++ b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddTwoFactorColumnsToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
index 8d4aefa4365..558a1837c79 100644
--- a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
+++ b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddTwoFactorColumnsToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
index 7ad01a04815..6d43f346d4f 100644
--- a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
+++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
@@ -1,7 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
index f335e77fb5e..3c5cd95726a 100644
--- a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
+++ b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddAutoCancelPendingPipelinesToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
index 6c9fe19ca34..807dfcb385d 100644
--- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb
index 7b61e811317..255b5e9c4db 100644
--- a/db/migrate/20170320173259_migrate_assignees.rb
+++ b/db/migrate/20170320173259_migrate_assignees.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class MigrateAssignees < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170918111708_create_project_custom_attributes.rb b/db/migrate/20170918111708_create_project_custom_attributes.rb
new file mode 100644
index 00000000000..b5bc90ec02e
--- /dev/null
+++ b/db/migrate/20170918111708_create_project_custom_attributes.rb
@@ -0,0 +1,15 @@
+class CreateProjectCustomAttributes < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :project_custom_attributes do |t|
+ t.timestamps_with_timezone null: false
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }
+ t.string :key, null: false
+ t.string :value, null: false
+
+ t.index [:project_id, :key], unique: true
+ t.index [:key, :value]
+ end
+ end
+end
diff --git a/db/migrate/20170918140927_create_group_custom_attributes.rb b/db/migrate/20170918140927_create_group_custom_attributes.rb
new file mode 100644
index 00000000000..3879ea15eb6
--- /dev/null
+++ b/db/migrate/20170918140927_create_group_custom_attributes.rb
@@ -0,0 +1,19 @@
+class CreateGroupCustomAttributes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :group_custom_attributes do |t|
+ t.timestamps_with_timezone null: false
+ t.references :group, null: false
+ t.string :key, null: false
+ t.string :value, null: false
+
+ t.index [:group_id, :key], unique: true
+ t.index [:key, :value]
+ end
+
+ add_foreign_key :group_custom_attributes, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
+ end
+end
diff --git a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
index b2009b282e9..8423bf13fd9 100644
--- a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
+++ b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
@@ -12,6 +12,7 @@ class RemoveTemporaryCiBuildsIndex < ActiveRecord::Migration
def up
return unless index_exists?(:ci_builds, :id, name: 'index_for_ci_builds_retried_migration')
+
remove_concurrent_index(:ci_builds, :id, name: "index_for_ci_builds_retried_migration")
end
diff --git a/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb
new file mode 100644
index 00000000000..55e822752af
--- /dev/null
+++ b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddGlobalRateLimitsToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :throttle_unauthenticated_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :throttle_unauthenticated_requests_per_period, :integer, default: 3600, allow_null: false
+ add_column_with_default :application_settings, :throttle_unauthenticated_period_in_seconds, :integer, default: 3600, allow_null: false
+
+ add_column_with_default :application_settings, :throttle_authenticated_api_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_api_requests_per_period, :integer, default: 7200, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_api_period_in_seconds, :integer, default: 3600, allow_null: false
+
+ add_column_with_default :application_settings, :throttle_authenticated_web_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_web_requests_per_period, :integer, default: 7200, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_web_period_in_seconds, :integer, default: 3600, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :throttle_authenticated_web_period_in_seconds
+ remove_column :application_settings, :throttle_authenticated_web_requests_per_period
+ remove_column :application_settings, :throttle_authenticated_web_enabled
+
+ remove_column :application_settings, :throttle_authenticated_api_period_in_seconds
+ remove_column :application_settings, :throttle_authenticated_api_requests_per_period
+ remove_column :application_settings, :throttle_authenticated_api_enabled
+
+ remove_column :application_settings, :throttle_unauthenticated_period_in_seconds
+ remove_column :application_settings, :throttle_unauthenticated_requests_per_period
+ remove_column :application_settings, :throttle_unauthenticated_enabled
+ end
+end
diff --git a/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb
new file mode 100644
index 00000000000..9a909644a44
--- /dev/null
+++ b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb
@@ -0,0 +1,78 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateUserAuthenticationTokenToPersonalAccessToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # disable_ddl_transaction!
+
+ TOKEN_NAME = 'Private Token'.freeze
+
+ def up
+ execute <<~SQL
+ INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
+ SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api].to_yaml}'
+ FROM users
+ WHERE authentication_token IS NOT NULL
+ AND admin = FALSE
+ AND NOT EXISTS (
+ SELECT true
+ FROM personal_access_tokens
+ WHERE user_id = users.id
+ AND token = users.authentication_token
+ )
+ SQL
+
+ # Admins also need the `sudo` scope
+ execute <<~SQL
+ INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
+ SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api sudo].to_yaml}'
+ FROM users
+ WHERE authentication_token IS NOT NULL
+ AND admin = TRUE
+ AND NOT EXISTS (
+ SELECT true
+ FROM personal_access_tokens
+ WHERE user_id = users.id
+ AND token = users.authentication_token
+ )
+ SQL
+ end
+
+ def down
+ if Gitlab::Database.postgresql?
+ execute <<~SQL
+ UPDATE users
+ SET authentication_token = pats.token
+ FROM (
+ SELECT user_id, token
+ FROM personal_access_tokens
+ WHERE name = '#{TOKEN_NAME}'
+ ) AS pats
+ WHERE id = pats.user_id
+ SQL
+ else
+ execute <<~SQL
+ UPDATE users
+ INNER JOIN personal_access_tokens AS pats
+ ON users.id = pats.user_id
+ SET authentication_token = pats.token
+ WHERE pats.name = '#{TOKEN_NAME}'
+ SQL
+ end
+
+ execute <<~SQL
+ DELETE FROM personal_access_tokens
+ WHERE name = '#{TOKEN_NAME}'
+ AND EXISTS (
+ SELECT true
+ FROM users
+ WHERE id = personal_access_tokens.user_id
+ AND authentication_token = personal_access_tokens.token
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb
new file mode 100644
index 00000000000..dabb3e25e48
--- /dev/null
+++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb
@@ -0,0 +1,68 @@
+class CreateNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :clusters do |t|
+ t.references :user, index: true, foreign_key: { on_delete: :nullify }
+
+ t.integer :provider_type
+ t.integer :platform_type
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.boolean :enabled, index: true, default: true
+
+ t.string :name, null: false # If manual, read-write. If gcp, read-only.
+ end
+
+ create_table :cluster_projects do |t|
+ t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+ end
+
+ create_table :cluster_platforms_kubernetes do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :api_url
+ t.text :ca_cert
+
+ t.string :namespace
+
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ t.text :encrypted_token
+ t.string :encrypted_token_iv
+ end
+
+ create_table :cluster_providers_gcp do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.integer :status
+ t.integer :num_nodes, null: false
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :status_reason
+
+ t.string :gcp_project_id, null: false
+ t.string :zone, null: false
+ t.string :machine_type
+ t.string :operation_id
+
+ t.string :endpoint
+
+ t.text :encrypted_access_token
+ t.string :encrypted_access_token_iv
+ end
+ end
+end
diff --git a/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb
new file mode 100644
index 00000000000..74a2badc130
--- /dev/null
+++ b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb
@@ -0,0 +1,26 @@
+class AddLatestMergeRequestDiffIdToMergeRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :merge_requests, :latest_merge_request_diff_id, :integer
+ add_concurrent_index :merge_requests, :latest_merge_request_diff_id
+
+ add_concurrent_foreign_key :merge_requests, :merge_request_diffs,
+ column: :latest_merge_request_diff_id,
+ on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key :merge_requests, column: :latest_merge_request_diff_id
+
+ if index_exists?(:merge_requests, :latest_merge_request_diff_id)
+ remove_concurrent_index :merge_requests, :latest_merge_request_diff_id
+ end
+
+ remove_column :merge_requests, :latest_merge_request_diff_id
+ end
+end
diff --git a/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb b/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb
new file mode 100644
index 00000000000..a2ce37127ea
--- /dev/null
+++ b/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb
@@ -0,0 +1,18 @@
+class CreateClustersKubernetesHelmApps < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_helm do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.integer :status, null: false
+ t.string :version, null: false
+ t.text :status_reason
+ end
+ end
+end
diff --git a/db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb b/db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb
new file mode 100644
index 00000000000..21f48b1d1b4
--- /dev/null
+++ b/db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb
@@ -0,0 +1,21 @@
+class CreateClustersKubernetesIngressApps < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_ingress do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.integer :status, null: false
+ t.integer :ingress_type, null: false
+
+ t.string :version, null: false
+ t.string :cluster_ip
+ t.text :status_reason
+ end
+ end
+end
diff --git a/db/migrate/20171106132212_issues_confidential_not_null.rb b/db/migrate/20171106132212_issues_confidential_not_null.rb
new file mode 100644
index 00000000000..c959d2dd938
--- /dev/null
+++ b/db/migrate/20171106132212_issues_confidential_not_null.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesConfidentialNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ end
+
+ def up
+ Issue.where('confidential IS NULL').update_all(confidential: false)
+
+ change_column_null :issues, :confidential, false
+ end
+
+ def down
+ # There's no way / point to revert this.
+ end
+end
diff --git a/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
new file mode 100644
index 00000000000..e6a780d0964
--- /dev/null
+++ b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesMilestoneIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_milestones
+ where('NOT EXISTS (SELECT true FROM milestones WHERE milestones.id = issues.milestone_id)')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_milestones.each_batch(of: 100) do |batch|
+ batch.update_all(milestone_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :issues,
+ :milestones,
+ column: :milestone_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :milestone_id)
+ end
+end
diff --git a/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb b/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb
new file mode 100644
index 00000000000..3b8844d7d9f
--- /dev/null
+++ b/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb
@@ -0,0 +1,45 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesUpdatedByIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_updaters
+ where('NOT EXISTS (SELECT true FROM users WHERE users.id = issues.updated_by_id)')
+ .where('updated_by_id IS NOT NULL')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_updaters.each_batch(of: 100) do |batch|
+ batch.update_all(updated_by_id: nil)
+ end
+
+ # This index is only used for foreign keys, and those in turn will always
+ # specify a value. As such we can add a WHERE condition to make the index
+ # smaller.
+ add_concurrent_index(:issues, :updated_by_id, where: 'updated_by_id IS NOT NULL')
+
+ add_concurrent_foreign_key(
+ :issues,
+ :users,
+ column: :updated_by_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :updated_by_id)
+ remove_concurrent_index(:issues, :updated_by_id)
+ end
+end
diff --git a/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
new file mode 100644
index 00000000000..8d2ceb8cc18
--- /dev/null
+++ b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
@@ -0,0 +1,44 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesMovedToIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_moved_to_issues
+ where('NOT EXISTS (SELECT true FROM issues WHERE issues.id = issues.moved_to_id)')
+ .where('moved_to_id IS NOT NULL')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_moved_to_issues.each_batch(of: 100) do |batch|
+ batch.update_all(moved_to_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :issues,
+ :issues,
+ column: :moved_to_id,
+ on_delete: :nullify
+ )
+
+ # We're using a partial index here so we only index the data we actually
+ # care about.
+ add_concurrent_index(:issues, :moved_to_id, where: 'moved_to_id IS NOT NULL')
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :moved_to_id)
+ remove_concurrent_index(:issues, :moved_to_id)
+ end
+end
diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/migrate/20171106154015_remove_issues_branch_name.rb
new file mode 100644
index 00000000000..3d08225c96d
--- /dev/null
+++ b/db/migrate/20171106154015_remove_issues_branch_name.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveIssuesBranchName < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ remove_column :issues, :branch_name, :string
+ end
+end
diff --git a/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb
new file mode 100644
index 00000000000..e4bed778695
--- /dev/null
+++ b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ NEW_INDEX_NAME = 'idx_issues_on_project_id_and_due_date_and_id_and_state_partial'
+ OLD_INDEX_NAME = 'index_issues_on_project_id_and_due_date_and_id_and_state'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(
+ :issues,
+ [:project_id, :due_date, :id, :state],
+ where: 'due_date IS NOT NULL',
+ name: NEW_INDEX_NAME
+ )
+
+ # We set the column name to nil as otherwise Rails will ignore the custom
+ # index name and remove the wrong index.
+ remove_concurrent_index(:issues, nil, name: OLD_INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(
+ :issues,
+ [:project_id, :due_date, :id, :state],
+ name: OLD_INDEX_NAME
+ )
+
+ remove_concurrent_index(:issues, nil, name: NEW_INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb
new file mode 100644
index 00000000000..ad540b1e509
--- /dev/null
+++ b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTimezoneToIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ change_column_type_concurrently(:issues, :closed_at, :datetime_with_timezone)
+ end
+
+ def down
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+end
diff --git a/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb
new file mode 100644
index 00000000000..021eaa04a0c
--- /dev/null
+++ b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb
@@ -0,0 +1,43 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsAuthorIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_authors
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.author_id = users.id)')
+ .where('author_id IS NOT NULL')
+ end
+ end
+
+ def up
+ # Replacing the ghost user ID logic would be too complex, hence we don't
+ # redefine the User model here.
+ ghost_id = User.select(:id).ghost.id
+
+ MergeRequest.with_orphaned_authors.each_batch(of: 100) do |batch|
+ batch.update_all(author_id: ghost_id)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :author_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key(:merge_requests, column: :author_id)
+ end
+end
diff --git a/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb
new file mode 100644
index 00000000000..1a242f01051
--- /dev/null
+++ b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsAssigneeIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_assignees
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.assignee_id = users.id)')
+ .where('assignee_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_assignees.each_batch(of: 100) do |batch|
+ batch.update_all(assignee_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :assignee_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key(:merge_requests, column: :assignee_id)
+ end
+end
diff --git a/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb
new file mode 100644
index 00000000000..eb3872e38da
--- /dev/null
+++ b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb
@@ -0,0 +1,46 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsUpdatedByIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_updaters
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.updated_by_id = users.id)')
+ .where('updated_by_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_updaters.each_batch(of: 100) do |batch|
+ batch.update_all(updated_by_id: nil)
+ end
+
+ add_concurrent_index(
+ :merge_requests,
+ :updated_by_id,
+ where: 'updated_by_id IS NOT NULL'
+ )
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :updated_by_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :updated_by_id)
+ remove_concurrent_index(:merge_requests, :updated_by_id)
+ end
+end
diff --git a/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb
new file mode 100644
index 00000000000..925b3e537d7
--- /dev/null
+++ b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb
@@ -0,0 +1,46 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsMergeUserIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_mergers
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.merge_user_id = users.id)')
+ .where('merge_user_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_mergers.each_batch(of: 100) do |batch|
+ batch.update_all(merge_user_id: nil)
+ end
+
+ add_concurrent_index(
+ :merge_requests,
+ :merge_user_id,
+ where: 'merge_user_id IS NOT NULL'
+ )
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :merge_user_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :merge_user_id)
+ remove_concurrent_index(:merge_requests, :merge_user_id)
+ end
+end
diff --git a/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb
new file mode 100644
index 00000000000..99740f64fe6
--- /dev/null
+++ b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb
@@ -0,0 +1,45 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsSourceProjectIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_source_projects
+ where('NOT EXISTS (SELECT true FROM projects WHERE merge_requests.source_project_id = projects.id)')
+ .where('source_project_id IS NOT NULL')
+ end
+ end
+
+ def up
+ # We need to allow NULL values so we can nullify the column when the source
+ # project is removed. We _don't_ want to remove the merge request, instead
+ # the application will keep them but close them.
+ change_column_null(:merge_requests, :source_project_id, true)
+
+ MergeRequest.with_orphaned_source_projects.each_batch(of: 100) do |batch|
+ batch.update_all(source_project_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :projects,
+ column: :source_project_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :source_project_id)
+ change_column_null(:merge_requests, :source_project_id, false)
+ end
+end
diff --git a/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb
new file mode 100644
index 00000000000..c005cf7d173
--- /dev/null
+++ b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsMilestoneIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_milestones
+ where('NOT EXISTS (SELECT true FROM milestones WHERE merge_requests.milestone_id = milestones.id)')
+ .where('milestone_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_milestones.each_batch(of: 100) do |batch|
+ batch.update_all(milestone_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :milestones,
+ column: :milestone_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :milestone_id)
+ end
+end
diff --git a/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb
new file mode 100644
index 00000000000..5a8ae6e4b57
--- /dev/null
+++ b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb
@@ -0,0 +1,35 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CiPipelinesIndexOnProjectIdRefStatusId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ TABLE = :ci_pipelines
+ OLD_COLUMNS = %i[project_id ref status].freeze
+ NEW_COLUMNS = %i[project_id ref status id].freeze
+
+ def up
+ unless index_exists?(TABLE, NEW_COLUMNS)
+ add_concurrent_index(TABLE, NEW_COLUMNS)
+ end
+
+ if index_exists?(TABLE, OLD_COLUMNS)
+ remove_concurrent_index(TABLE, OLD_COLUMNS)
+ end
+ end
+
+ def down
+ unless index_exists?(TABLE, OLD_COLUMNS)
+ add_concurrent_index(TABLE, OLD_COLUMNS)
+ end
+
+ if index_exists?(TABLE, NEW_COLUMNS)
+ remove_concurrent_index(TABLE, NEW_COLUMNS)
+ end
+ end
+end
diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
index 82f8147547e..f1f81691f81 100644
--- a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
+++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
index 01fff680183..49fd46b0262 100644
--- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
+++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class ResetRelativePositionForIssue < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
index cb1b4f1855d..78413a608f1 100644
--- a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
+++ b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
index f2ce25d4524..0ab3d61730d 100644
--- a/db/post_migrate/20170406111121_clean_upload_symlinks.rb
+++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
@@ -14,6 +14,7 @@ class CleanUploadSymlinks < ActiveRecord::Migration
DIRECTORIES_TO_MOVE.each do |dir|
symlink_location = File.join(old_upload_dir, dir)
next unless File.symlink?(symlink_location)
+
say "removing symlink: #{symlink_location}"
FileUtils.rm(symlink_location)
end
diff --git a/db/post_migrate/20170406142253_migrate_user_project_view.rb b/db/post_migrate/20170406142253_migrate_user_project_view.rb
index c4e910b3b44..d6061dd416d 100644
--- a/db/post_migrate/20170406142253_migrate_user_project_view.rb
+++ b/db/post_migrate/20170406142253_migrate_user_project_view.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
index 765daa0a347..bba37e32c01 100644
--- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
+++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
index 9d9f36550e7..b0b58ab3011 100644
--- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
+++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class UpdateRetriedForCiBuild < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb
index f77078ddd70..81e9d050668 100644
--- a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb
+++ b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class AddHeadPipelineForEachMergeRequest < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb
index c78beda9d21..3e952980866 100644
--- a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb
+++ b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb
index 97cb242415d..31a73bb3b27 100644
--- a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb
+++ b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class MigrateBuildStageReferenceAgain < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170612071012_move_personal_snippets_files.rb b/db/post_migrate/20170612071012_move_personal_snippets_files.rb
index 2b79a87ccd8..c735dc67f44 100644
--- a/db/post_migrate/20170612071012_move_personal_snippets_files.rb
+++ b/db/post_migrate/20170612071012_move_personal_snippets_files.rb
@@ -32,6 +32,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration
file_name = upload['path'].split('/')[1]
next unless move_file(upload['model_id'], secret, file_name)
+
update_markdown(upload['model_id'], secret, file_name, upload['description'])
end
end
diff --git a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb
index acb895e426f..17849b78ceb 100644
--- a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb
+++ b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb
@@ -13,6 +13,7 @@ class CleanAppearanceSymlinks < ActiveRecord::Migration
symlink_location = File.join(old_upload_dir, dir)
return unless File.symlink?(symlink_location)
+
say "removing symlink: #{symlink_location}"
FileUtils.rm(symlink_location)
end
diff --git a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
index a238216253b..b040c81b316 100644
--- a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
+++ b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class UpdateLegacyDiffNotesTypeForImport < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170927112319_update_notes_type_for_import.rb b/db/post_migrate/20170927112319_update_notes_type_for_import.rb
index 1e70acd9868..5a400c71b02 100644
--- a/db/post_migrate/20170927112319_update_notes_type_for_import.rb
+++ b/db/post_migrate/20170927112319_update_notes_type_for_import.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class UpdateNotesTypeForImport < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20171012150314_remove_user_authentication_token.rb b/db/post_migrate/20171012150314_remove_user_authentication_token.rb
new file mode 100644
index 00000000000..d0f3aa06e98
--- /dev/null
+++ b/db/post_migrate/20171012150314_remove_user_authentication_token.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUserAuthenticationToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_column :users, :authentication_token
+ end
+
+ def down
+ add_column :users, :authentication_token, :string
+
+ add_concurrent_index :users, :authentication_token, unique: true
+ end
+end
diff --git a/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
new file mode 100644
index 00000000000..4758c694563
--- /dev/null
+++ b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
@@ -0,0 +1,99 @@
+class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ class GcpCluster < ActiveRecord::Base
+ self.table_name = 'gcp_clusters'
+
+ belongs_to :project, class_name: 'Project'
+
+ include EachBatch
+ end
+
+ class Cluster < ActiveRecord::Base
+ self.table_name = 'clusters'
+
+ has_many :cluster_projects, class_name: 'ClustersProject'
+ has_many :projects, through: :cluster_projects, class_name: 'Project'
+ has_one :provider_gcp, class_name: 'ProvidersGcp'
+ has_one :platform_kubernetes, class_name: 'PlatformsKubernetes'
+
+ accepts_nested_attributes_for :provider_gcp
+ accepts_nested_attributes_for :platform_kubernetes
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+ end
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ has_one :cluster_project, class_name: 'ClustersProject'
+ has_one :cluster, through: :cluster_project, class_name: 'Cluster'
+ end
+
+ class ClustersProject < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Cluster'
+ belongs_to :project, class_name: 'Project'
+ end
+
+ class ProvidersGcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+ end
+
+ class PlatformsKubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+ end
+
+ def up
+ GcpCluster.all.find_each(batch_size: 1) do |gcp_cluster|
+ Cluster.create(
+ enabled: gcp_cluster.enabled,
+ user_id: gcp_cluster.user_id,
+ name: gcp_cluster.gcp_cluster_name,
+ provider_type: Cluster.provider_types[:gcp],
+ platform_type: Cluster.platform_types[:kubernetes],
+ projects: [gcp_cluster.project],
+ provider_gcp_attributes: {
+ status: gcp_cluster.status,
+ status_reason: gcp_cluster.status_reason,
+ gcp_project_id: gcp_cluster.gcp_project_id,
+ zone: gcp_cluster.gcp_cluster_zone,
+ num_nodes: gcp_cluster.gcp_cluster_size,
+ machine_type: gcp_cluster.gcp_machine_type,
+ operation_id: gcp_cluster.gcp_operation_id,
+ endpoint: gcp_cluster.endpoint,
+ encrypted_access_token: gcp_cluster.encrypted_gcp_token,
+ encrypted_access_token_iv: gcp_cluster.encrypted_gcp_token_iv
+ },
+ platform_kubernetes_attributes: {
+ cluster_id: gcp_cluster.id,
+ api_url: api_url(gcp_cluster.endpoint),
+ ca_cert: gcp_cluster.ca_cert,
+ namespace: gcp_cluster.project_namespace,
+ username: gcp_cluster.username,
+ encrypted_password: gcp_cluster.encrypted_password,
+ encrypted_password_iv: gcp_cluster.encrypted_password_iv,
+ encrypted_token: gcp_cluster.encrypted_kubernetes_token,
+ encrypted_token_iv: gcp_cluster.encrypted_kubernetes_token_iv
+ } )
+ end
+ end
+
+ def down
+ execute('DELETE FROM clusters')
+ end
+
+ private
+
+ def api_url(endpoint)
+ endpoint ? 'https://' + endpoint : nil
+ end
+end
diff --git a/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb
new file mode 100644
index 00000000000..7a63382cc6d
--- /dev/null
+++ b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb
@@ -0,0 +1,29 @@
+class ScheduleMergeRequestLatestMergeRequestDiffIdMigrations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 50_000
+ MIGRATION = 'PopulateMergeRequestsLatestMergeRequestDiffId'
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ # On GitLab.com, we saw that we generated about 500,000 dead tuples over 5 minutes.
+ # To keep replication lag from ballooning, we'll aim for 50,000 updates over 5 minutes.
+ #
+ # Assuming that there are 5 million rows affected (which is more than on
+ # GitLab.com), and that each batch of 50,000 rows takes up to 5 minutes, then
+ # we can migrate all the rows in 8.5 hours.
+ def up
+ MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation, index|
+ range = relation.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * 5.minutes, MIGRATION, range)
+ end
+ end
+end
diff --git a/db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb b/db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb
new file mode 100644
index 00000000000..4e8f495d65d
--- /dev/null
+++ b/db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb
@@ -0,0 +1,14 @@
+class RemoveRefFetchedFromMergeRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # We don't need to cache this anymore: the refs are now created
+ # upon save/update and there is no more use for this flag
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/36061
+ def change
+ remove_column :merge_requests, :ref_fetched, :boolean
+ end
+end
diff --git a/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb
new file mode 100644
index 00000000000..88dd8f89ba6
--- /dev/null
+++ b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupAddTimezoneToIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+
+ # rubocop:disable Migration/Datetime
+ def down
+ change_column_type_concurrently(:issues, :closed_at, :datetime)
+ end
+end
diff --git a/db/post_migrate/20171114104051_remove_empty_fork_networks.rb b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb
new file mode 100644
index 00000000000..2fe99a1b9c1
--- /dev/null
+++ b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb
@@ -0,0 +1,36 @@
+class RemoveEmptyForkNetworks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+
+ class MigrationForkNetwork < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'fork_networks'
+ end
+
+ class MigrationForkNetworkMembers < ActiveRecord::Base
+ self.table_name = 'fork_network_members'
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ say 'Deleting empty ForkNetworks in batches'
+
+ has_members = MigrationForkNetworkMembers
+ .where('fork_network_members.fork_network_id = fork_networks.id')
+ .select(1)
+ MigrationForkNetwork.where('NOT EXISTS (?)', has_members)
+ .each_batch(of: BATCH_SIZE) do |networks|
+ deleted = networks.delete_all
+
+ say "Deleted #{deleted} rows in batch"
+ end
+ end
+
+ def down
+ # nothing
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 530f08022be..a82270390f1 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: 20171017145932) do
+ActiveRecord::Schema.define(version: 20171121144800) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -140,6 +140,15 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.integer "circuitbreaker_storage_timeout", default: 30
t.integer "circuitbreaker_access_retries", default: 3
t.integer "circuitbreaker_backoff_threshold", default: 80
+ t.boolean "throttle_unauthenticated_enabled", default: false, null: false
+ t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false
+ t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false
+ t.boolean "throttle_authenticated_api_enabled", default: false, null: false
+ t.integer "throttle_authenticated_api_requests_per_period", default: 7200, null: false
+ t.integer "throttle_authenticated_api_period_in_seconds", default: 3600, null: false
+ t.boolean "throttle_authenticated_web_enabled", default: false, null: false
+ t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false
+ t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -373,7 +382,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
- add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+ add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
@@ -462,6 +471,83 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
+ create_table "cluster_platforms_kubernetes", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "api_url"
+ t.text "ca_cert"
+ t.string "namespace"
+ t.string "username"
+ t.text "encrypted_password"
+ t.string "encrypted_password_iv"
+ t.text "encrypted_token"
+ t.string "encrypted_token_iv"
+ end
+
+ add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree
+
+ create_table "cluster_projects", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
+ add_index "cluster_projects", ["project_id"], name: "index_cluster_projects_on_project_id", using: :btree
+
+ create_table "cluster_providers_gcp", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "status"
+ t.integer "num_nodes", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "status_reason"
+ t.string "gcp_project_id", null: false
+ t.string "zone", null: false
+ t.string "machine_type"
+ t.string "operation_id"
+ t.string "endpoint"
+ t.text "encrypted_access_token"
+ t.string "encrypted_access_token_iv"
+ end
+
+ add_index "cluster_providers_gcp", ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
+
+ create_table "clusters", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "provider_type"
+ t.integer "platform_type"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled", default: true
+ t.string "name", null: false
+ end
+
+ add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
+ add_index "clusters", ["user_id"], name: "index_clusters_on_user_id", using: :btree
+
+ create_table "clusters_applications_helm", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.text "status_reason"
+ end
+
+ create_table "clusters_applications_ingress", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.integer "ingress_type", null: false
+ t.string "version", null: false
+ t.string "cluster_ip"
+ t.text "status_reason"
+ end
+
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -693,6 +779,17 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "gpg_signatures", ["gpg_key_subkey_id"], name: "index_gpg_signatures_on_gpg_key_subkey_id", using: :btree
add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
+ create_table "group_custom_attributes", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "group_id", null: false
+ t.string "key", null: false
+ t.string "value", null: false
+ end
+
+ add_index "group_custom_attributes", ["group_id", "key"], name: "index_group_custom_attributes_on_group_id_and_key", unique: true, using: :btree
+ add_index "group_custom_attributes", ["key", "value"], name: "index_group_custom_attributes_on_key_and_value", using: :btree
+
create_table "identities", force: :cascade do |t|
t.string "extern_uid"
t.string "provider"
@@ -729,13 +826,12 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "branch_name"
t.text "description"
t.integer "milestone_id"
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
- t.boolean "confidential", default: false
+ t.boolean "confidential", default: false, null: false
t.datetime "deleted_at"
t.date "due_date"
t.integer "moved_to_id"
@@ -744,11 +840,11 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.text "description_html"
t.integer "time_estimate"
t.integer "relative_position"
- t.datetime "closed_at"
t.integer "cached_markdown_version"
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
t.boolean "discussion_locked"
+ t.datetime_with_timezone "closed_at"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -757,13 +853,15 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
+ add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)", using: :btree
add_index "issues", ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state", using: :btree
- add_index "issues", ["project_id", "due_date", "id", "state"], name: "index_issues_on_project_id_and_due_date_and_id_and_state", using: :btree
+ add_index "issues", ["project_id", "due_date", "id", "state"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_partial", where: "(due_date IS NOT NULL)", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state", using: :btree
add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ add_index "issues", ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "keys", force: :cascade do |t|
t.integer "user_id"
@@ -942,7 +1040,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
- t.integer "source_project_id", null: false
+ t.integer "source_project_id"
t.integer "author_id"
t.integer "assignee_id"
t.string "title"
@@ -970,9 +1068,9 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
t.integer "head_pipeline_id"
- t.boolean "ref_fetched"
t.string "merge_jid"
t.boolean "discussion_locked"
+ t.integer "latest_merge_request_diff_id"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -981,6 +1079,8 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
+ add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree
+ add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
@@ -989,6 +1089,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ add_index "merge_requests", ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "merge_requests_closing_issues", force: :cascade do |t|
t.integer "merge_request_id", null: false
@@ -1212,6 +1313,17 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
+ create_table "project_custom_attributes", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "project_id", null: false
+ t.string "key", null: false
+ t.string "value", null: false
+ end
+
+ add_index "project_custom_attributes", ["key", "value"], name: "index_project_custom_attributes_on_key_and_value", using: :btree
+ add_index "project_custom_attributes", ["project_id", "key"], name: "index_project_custom_attributes_on_project_id_and_key", unique: true, using: :btree
+
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "merge_requests_access_level"
@@ -1670,7 +1782,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
- t.string "authentication_token"
t.string "bio"
t.integer "failed_attempts", default: 0
t.datetime "locked_at"
@@ -1720,7 +1831,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
- add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
@@ -1810,6 +1920,12 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
+ add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "projects", on_delete: :cascade
+ add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade
+ add_foreign_key "clusters", "users", on_delete: :nullify
+ add_foreign_key "clusters_applications_helm", "clusters", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
@@ -1829,11 +1945,15 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
+ add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
+ add_foreign_key "issues", "issues", column: "moved_to_id", name: "fk_a194299be1", on_delete: :nullify
+ add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
+ add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
@@ -1846,7 +1966,14 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
+ add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify
+ add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify
+ add_foreign_key "merge_requests", "projects", column: "source_project_id", name: "fk_3308fe130c", on_delete: :nullify
add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade
+ add_foreign_key "merge_requests", "users", column: "assignee_id", name: "fk_6149611a04", on_delete: :nullify
+ add_foreign_key "merge_requests", "users", column: "author_id", name: "fk_e719a85f8a", on_delete: :nullify
+ add_foreign_key "merge_requests", "users", column: "merge_user_id", name: "fk_ad525e1f87", on_delete: :nullify
+ add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
@@ -1858,6 +1985,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
+ add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 5eabc126b95..d4119d35162 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,5 +1,6 @@
---
toc: false
+comments: false
---
# GitLab Documentation
@@ -170,6 +171,7 @@ have access to GitLab administration tools and settings.
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics.
- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
+- [Monitoring GitHub imports](administration/monitoring/github_imports.md)
### Performance
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 13bd501e397..ee9b9a9466a 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Authentication and Authorization
GitLab integrates with the following external authentication and authorization
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 4d3be0ab8f6..a88e67bfeb5 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -53,7 +53,9 @@ or in different cloud availability zones.
> **Note:** GitLab recommends against choosing this HA method because of the
complexity of managing DRBD and crafting automatic failover. This is
- *compatible* with GitLab, but not officially *supported*.
+ *compatible* with GitLab, but not officially *supported*. If you are
+ an EE customer, support will help you with GitLab related problems, but if the
+ root cause is identified as DRBD, we will not troubleshoot further.
Components/Servers Required: 2 servers/virtual machines (one active/one passive)
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 652ca9cf454..93c3642a1f1 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -56,29 +56,34 @@ that, login with an Admin account and do following:
With PlantUML integration enabled and configured, we can start adding diagrams to
our AsciiDoc snippets, wikis and repos using delimited blocks:
-```
-[plantuml, format="png", id="myDiagram", width="200px"]
---
-Bob->Alice : hello
-Alice -> Bob : Go Away
---
-```
+- **Markdown**
+
+ ```plantuml
+ Bob -> Alice : hello
+ Alice -> Bob : Go Away
+ ```
-And in Markdown using fenced code blocks:
+- **AsciiDoc**
- ```plantuml
- Bob -> Alice : hello
+ ```
+ [plantuml, format="png", id="myDiagram", width="200px"]
+ --
+ Bob->Alice : hello
Alice -> Bob : Go Away
+ --
```
-And in reStructuredText using a directive:
+- **reStructuredText**
-```
-.. plantuml::
+ ```
+ .. plantuml::
+ :caption: Caption with **bold** and *italic*
- Bob -> Alice: hello
- Alice -> Bob: Go Away
-```
+ Bob -> Alice: hello
+ Alice -> Bob: Go Away
+ ```
+
+ You can also use the `uml::` directive for compatibility with [sphinxcontrib-plantuml](https://pypi.python.org/pypi/sphinxcontrib-plantuml), but please note that we currently only support the `caption` option.
The above blocks will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index c9ed2d84ccb..debaa2330d0 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -192,4 +192,13 @@ installations from source.
It logs information whenever a [repository check is run][repocheck] on a project.
+## Reconfigure Logs
+
+Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab
+packages. Installations from source don't have reconfigure logs. A reconfigure log
+is populated whenever `gitlab-ctl reconfigure` is run manually or as part of an upgrade.
+
+Reconfigure logs files are named according to the UNIX timestamp of when the reconfigure
+was initiated, such as `1509705644.log`
+
[repocheck]: repository_checks.md
diff --git a/doc/administration/monitoring/github_imports.md b/doc/administration/monitoring/github_imports.md
new file mode 100644
index 00000000000..5592e0a9e9a
--- /dev/null
+++ b/doc/administration/monitoring/github_imports.md
@@ -0,0 +1,101 @@
+# Monitoring GitHub imports
+
+>**Note:**
+Available since [GitLab 10.2][14731].
+
+The GitHub importer exposes various Prometheus metrics that you can use to
+monitor the health and progress of the importer.
+
+## Import Duration Times
+
+| Name | Type |
+|------------------------------------------|-----------|
+| `github_importer_total_duration_seconds` | histogram |
+
+This metric tracks the total time spent (in seconds) importing a project (from
+project creation until the import process finishes), for every imported project.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported projects
+
+| Name | Type |
+|-------------------------------------|---------|
+| `github_importer_imported_projects` | counter |
+
+This metric tracks the total number of projects imported over time. This metric
+does not expose any labels.
+
+## Number of GitHub API calls
+
+| Name | Type |
+|---------------------------------|---------|
+| `github_importer_request_count` | counter |
+
+This metric tracks the total number of GitHub API calls performed over time, for
+all projects. This metric does not expose any labels.
+
+## Rate limit errors
+
+| Name | Type |
+|-----------------------------------|---------|
+| `github_importer_rate_limit_hits` | counter |
+
+This metric tracks the number of times we hit the GitHub rate limit, for all
+projects. This metric does not expose any labels.
+
+## Number of imported issues
+
+| Name | Type |
+|-----------------------------------|---------|
+| `github_importer_imported_issues` | counter |
+
+This metric tracks the number of imported issues across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported pull requests
+
+| Name | Type |
+|------------------------------------------|---------|
+| `github_importer_imported_pull_requests` | counter |
+
+This metric tracks the number of imported pull requests across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported comments
+
+| Name | Type |
+|----------------------------------|---------|
+| `github_importer_imported_notes` | counter |
+
+This metric tracks the number of imported comments across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported pull request review comments
+
+| Name | Type |
+|---------------------------------------|---------|
+| `github_importer_imported_diff_notes` | counter |
+
+This metric tracks the number of imported comments across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported repositories
+
+| Name | Type |
+|-----------------------------------------|---------|
+| `github_importer_imported_repositories` | counter |
+
+This metric tracks the number of imported repositories across all projects. This
+metric does not expose any labels.
+
+[14731]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14731
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
index b5e78348989..cbffd883774 100644
--- a/doc/administration/operations/sidekiq_memory_killer.md
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -28,7 +28,7 @@ The MemoryKiller is controlled using environment variables.
delayed shutdown is triggered. The default value for Omnibus packages is set
[in the omnibus-gitlab
repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
+- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults to 900 seconds (15 minutes). When
a shutdown is triggered, the Sidekiq process will keep working normally for
another 15 minutes.
- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
@@ -36,5 +36,3 @@ The MemoryKiller is controlled using environment variables.
Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
restart Sidekiq.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
- the final signal sent to the Sidekiq process when we want it to shut down.
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index fa882bbe28a..21184fed6e9 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -9,7 +9,7 @@ mapping structure from the projects URLs:
* Project's repository: `#{namespace}/#{project_name}.git`
* Project's wiki: `#{namespace}/#{project_name}.wiki.git`
-
+
This structure made simple to migrate from existing solutions to GitLab and easy for Administrators to find where the
repository is stored.
@@ -25,7 +25,10 @@ Any change in the URL will need to be reflected on disk (when groups / users or
of load in big installations, and can be even worst if they are using any type of network based filesystem.
Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct
-order or we may end-up with wrong repository or missing data temporarily.
+order or we may end-up with wrong repository or missing data temporarily.
+
+This pattern also exists in other objects stored in GitLab, like issue Attachments, GitLab Pages artifacts,
+Docker Containers for the integrated Registry, etc.
## Hashed Storage
@@ -59,11 +62,31 @@ you will never mistakenly restore a repository in the wrong project (considering
### How to migrate to Hashed Storage
-In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select
+In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select
"_Create new projects using hashed storage paths_".
-
+
To migrate your existing projects to the new storage type, check the specific [rake tasks].
[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
[storage-paths]: repository_storage_types.md
+
+### Hashed Storage coverage
+
+We are incrementally moving every storable object in GitLab to the Hashed Storage pattern. You can check the current
+coverage status below.
+
+Note that things stored in an S3 compatible endpoint will not have the downsides mentioned earlier, if they are not
+prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects.
+
+| Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version |
+| --------------- | -------------- | -------------- | ------------- | -------------- |
+| Repository | Yes | Yes | - | 10.0 |
+| Attachments | Yes | Yes | - | 10.2 |
+| Avatars | Yes | No | - | - |
+| Pages | Yes | No | - | - |
+| Docker Registry | Yes | No | - | - |
+| CI Build Logs | No | No | - | - |
+| CI Artifacts | No | No | Yes (EEP) | - |
+| CI Cache | No | No | Yes | - |
+| LFS Objects | Yes | No | Yes (EEP) | - |
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index 6f1356ddf8f..83a714810c1 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -141,7 +141,7 @@ separate Rails process to debug the issue:
1. Log in to your GitLab account.
1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC).
-1. Obtain the private token for your user (Profile Settings -> Account).
+1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens).
1. Bring up the GitLab Rails console. For omnibus users, run:
```
@@ -163,6 +163,34 @@ separate Rails process to debug the issue:
1. In a new window, run `top`. It should show this ruby process using 100% CPU. Write down the PID.
1. Follow step 2 from the previous section on using gdb.
+### GitLab: API is not accessible
+
+This often occurs when gitlab-shell attempts to request authorization via the
+internal API (e.g., `http://localhost:8080/api/v4/internal/allowed`), and
+something in the check fails. There are many reasons why this may happen:
+
+1. Timeout connecting to a database (e.g., PostgreSQL or Redis)
+1. Error in Git hooks or push rules
+1. Error accessing the repository (e.g., stale NFS handles)
+
+To diagnose this problem, try to reproduce the problem and then see if there
+is a unicorn worker that is spinning via `top`. Try to use the `gdb`
+techniques above. In addition, using `strace` may help isolate issues:
+
+```shell
+strace -tt -T -f -s 1024 -p <PID of unicorn worker> -o /tmp/unicorn.txt
+```
+
+If you cannot isolate which Unicorn worker is the issue, try to run `strace`
+on all the Unicorn workers to see where the `/internal/allowed` endpoint gets
+stuck:
+
+```shell
+ps auwx | grep unicorn | awk '{ print " -p " $2}' | xargs strace -tt -T -f -s 1024 -o /tmp/unicorn.txt
+```
+
+The output in `/tmp/unicorn.txt` may help diagnose the root cause.
+
# More information
* [Debugging Stuck Ruby Processes](https://blog.newrelic.com/2013/04/29/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9/)
diff --git a/doc/api/README.md b/doc/api/README.md
index 89ffe9d7868..f226716c3b5 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -50,7 +50,6 @@ following locations:
- [Repository Files](repository_files.md)
- [Runners](runners.md)
- [Services](services.md)
-- [Session](session.md)
- [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
@@ -86,27 +85,10 @@ API requests should be prefixed with `api` and the API version. The API version
is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API
is at `/api/v4`.
-For endpoints that require [authentication](#authentication), you need to pass
-a `private_token` parameter via query string or header. If passed as a header,
-the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
-an underscore).
-
-Example of a valid API request:
-
-```
-GET /projects?private_token=9koXpg98eAheJpvBs5tK
-```
-
-Example of a valid API request using cURL and authentication via header:
+Example of a valid API request using cURL:
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
-```
-
-Example of a valid API request using cURL and authentication via a query string:
-
-```shell
-curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK"
+curl "https://gitlab.example.com/api/v4/projects"
```
The API uses JSON to serialize data. You don't need to specify `.json` at the
@@ -114,15 +96,20 @@ end of an API URL.
## Authentication
-Most API requests require authentication via a session cookie or token. For
+Most API requests require authentication, or will only return public data when
+authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation
for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
-There are three types of access tokens available:
+There are three ways to authenticate with the GitLab API:
1. [OAuth2 tokens](#oauth2-tokens)
-1. [Private tokens](#private-tokens)
1. [Personal access tokens](#personal-access-tokens)
+1. [Session cookie](#session-cookie)
+
+For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available:
+1. [Impersonation tokens](#impersonation-tokens)
+2. [Sudo](#sudo)
If authentication information is invalid or omitted, an error message will be
returned with status code `401`:
@@ -133,74 +120,84 @@ returned with status code `401`:
}
```
-### Session cookie
+### OAuth2 tokens
-When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
-set. The API will use this cookie for authentication if it is present, but using
-the API to generate a new session cookie is currently not supported.
+You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing it in either the
+`access_token` parameter or the `Authorization` header.
-### OAuth2 tokens
+Example of using the OAuth2 token in a parameter:
-You can use an OAuth 2 token to authenticate with the API by passing it either in the
-`access_token` parameter or in the `Authorization` header.
+```shell
+curl https://gitlab.example.com/api/v4/projects?access_token=OAUTH-TOKEN
+```
-Example of using the OAuth2 token in the header:
+Example of using the OAuth2 token in a header:
```shell
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects
```
-Read more about [GitLab as an OAuth2 client](oauth2.md).
+Read more about [GitLab as an OAuth2 provider](oauth2.md).
-### Private tokens
+### Personal access tokens
-Private tokens provide full access to the GitLab API. Anyone with access to
-them can interact with GitLab as if they were you. You can find or reset your
-private token in your account page (`/profile/account`).
+You can use a [personal access token][pat] to authenticate with the API by passing it in either the
+`private_token` parameter or the `Private-Token` header.
-For examples of usage, [read the basic usage section](#basic-usage).
+Example of using the personal access token in a parameter:
-### Personal access tokens
+```shell
+curl https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK
+```
+
+Example of using the personal access token in a header:
-Instead of using your private token which grants full access to your account,
-personal access tokens could be a better fit because of their granular
-permissions.
+```shell
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects
+```
-Once you have your token, pass it to the API using either the `private_token`
-parameter or the `PRIVATE-TOKEN` header. For examples of usage,
-[read the basic usage section](#basic-usage).
+Read more about [personal access tokens][pat].
+
+### Session cookie
+
+When signing in to the main GitLab application, a `_gitlab_session` cookie is
+set. The API will use this cookie for authentication if it is present, but using
+the API to generate a new session cookie is currently not supported.
-[Read more about personal access tokens.][pat]
+The primary user of this authentication method is the web frontend of GitLab itself,
+which can use the API as the authenticated user to get a list of their projects,
+for example, without needing to explicitly pass an access token.
### Impersonation tokens
> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
Impersonation tokens are a type of [personal access token][pat]
-that can only be created by an admin for a specific user.
+that can only be created by an admin for a specific user. They are a great fit
+if you want to build applications or scripts that authenticate with the API as a specific user.
-They are a better alternative to using the user's password/private token
-or using the [Sudo](#sudo) feature which also requires the admin's password
-or private token, since the password/token can change over time. Impersonation
-tokens are a great fit if you want to build applications or tools which
-authenticate with the API as a specific user.
+They are an alternative to directly using the user's password or one of their
+personal access tokens, and to using the [Sudo](#sudo) feature, since the user's (or admin's, in the case of Sudo)
+password/token may not be known or may change over time.
For more information, refer to the
[users API](users.md#retrieve-user-impersonation-tokens) docs.
-For examples of usage, [read the basic usage section](#basic-usage).
+Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
+`private_token` parameter or the `Private-Token` header.
### Sudo
> Needs admin permissions.
All API requests support performing an API call as if you were another user,
-provided your private token is from an administrator account. You need to pass
-the `sudo` parameter either via query string or a header with an ID/username of
+provided you are authenticated as an administrator with an OAuth or Personal Access Token that has the `sudo` scope.
+
+You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the
-header name must be `SUDO` (uppercase).
+header name must be `Sudo`.
-If a non administrative `private_token` is provided, then an error message will
+If a non administrative access token is provided, an error message will
be returned with status code `403`:
```json
@@ -209,12 +206,23 @@ be returned with status code `403`:
}
```
+If an access token without the `sudo` scope is provided, an error message will
+be returned with status code `403`:
+
+```json
+{
+ "error": "insufficient_scope",
+ "error_description": "The request requires higher privileges than provided by the access token.",
+ "scope": "sudo"
+}
+```
+
If the sudo user ID or username cannot be found, an error message will be
returned with status code `404`:
```json
{
- "message": "404 Not Found: No user id or username for: <id/username>"
+ "message": "404 User with ID or username '123' Not Found"
}
```
@@ -228,7 +236,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects"
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: username" "https://gitlab.example.com/api/v4/projects"
```
Example of a valid API call and a request using cURL with sudo request,
@@ -239,7 +247,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects"
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: 23" "https://gitlab.example.com/api/v4/projects"
```
## Status codes
diff --git a/doc/api/custom_attributes.md b/doc/api/custom_attributes.md
index 8b26f7093ab..91d1b0e1520 100644
--- a/doc/api/custom_attributes.md
+++ b/doc/api/custom_attributes.md
@@ -2,17 +2,22 @@
Every API call to custom attributes must be authenticated as administrator.
+Custom attributes are currently available on users, groups, and projects,
+which will be referred to as "resource" in this documentation.
+
## List custom attributes
-Get all custom attributes on a user.
+Get all custom attributes on a resource.
```
GET /users/:id/custom_attributes
+GET /groups/:id/custom_attributes
+GET /projects/:id/custom_attributes
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes
@@ -35,15 +40,17 @@ Example response:
## Single custom attribute
-Get a single custom attribute on a user.
+Get a single custom attribute on a resource.
```
GET /users/:id/custom_attributes/:key
+GET /groups/:id/custom_attributes/:key
+GET /projects/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
| `key` | string | yes | The key of the custom attribute |
```bash
@@ -61,16 +68,18 @@ Example response:
## Set custom attribute
-Set a custom attribute on a user. The attribute will be updated if it already exists,
+Set a custom attribute on a resource. The attribute will be updated if it already exists,
or newly created otherwise.
```
PUT /users/:id/custom_attributes/:key
+PUT /groups/:id/custom_attributes/:key
+PUT /projects/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
| `key` | string | yes | The key of the custom attribute |
| `value` | string | yes | The value of the custom attribute |
@@ -89,15 +98,17 @@ Example response:
## Delete custom attribute
-Delete a custom attribute on a user.
+Delete a custom attribute on a resource.
```
DELETE /users/:id/custom_attributes/:key
+DELETE /groups/:id/custom_attributes/:key
+DELETE /projects/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
| `key` | string | yes | The key of the custom attribute |
```bash
diff --git a/doc/api/environments.md b/doc/api/environments.md
index e8deb3e07e9..6e20781f51a 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -36,7 +36,7 @@ Creates a new environment with the given name and external_url.
It returns `201` if the environment was successfully created, `400` for wrong parameters.
```
-POST /projects/:id/environment
+POST /projects/:id/environments
```
| Attribute | Type | Required | Description |
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 99d200c9c93..c1b5737c247 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -9,13 +9,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `skip_groups` | array of integers | no | Skip the group IDs passes |
-| `all_available` | boolean | no | Show all the groups you have access to |
-| `search` | string | no | Return list of authorized groups matching the search criteria |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
-| `owned` | boolean | no | Limit by groups owned by the current user |
+| `owned` | boolean | no | Limit to groups owned by the current user |
```
GET /groups
@@ -74,6 +74,55 @@ GET /groups?statistics=true
You can search for groups by name or path, see below.
+You can filter by [custom attributes](custom_attributes.md) with:
+
+```
+GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_value
+```
+
+## List a groups's subgroups
+
+> [Introduced][ce-15142] in GitLab 10.3.
+
+Get a list of visible direct subgroups in this group.
+When accessed without authentication, only public groups are returned.
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
+| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
+| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
+| `statistics` | boolean | no | Include group statistics (admins only) |
+| `owned` | boolean | no | Limit to groups owned by the current user |
+
+```
+GET /groups/:id/subgroups
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group",
+ "visibility": "public",
+ "lfs_enabled": true,
+ "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg",
+ "web_url": "http://gitlab.example.com/groups/foo-bar",
+ "request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
+ "parent_id": 123
+ }
+]
+```
+
## List a group's projects
Get a list of projects in this group. When accessed without authentication, only
@@ -466,3 +515,5 @@ And to switch pages add:
```
/groups?per_page=100&page=2
```
+
+[ce-15142]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15142
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 50a971102fb..b2e4b6d0955 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -15,6 +15,11 @@ given state (`opened`, `closed`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to
restrict the list of merge requests.
+**Note**: the `changes_count` value in the response is a string, not an
+integer. This is because when an MR has too many changes to display and store,
+it will be capped at 1,000. In that case, the API will return the string
+`"1000+"` for the changes count.
+
```
GET /merge_requests
GET /merge_requests?state=opened
@@ -53,6 +58,8 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "opened",
+ "created_at": "2017-04-29T08:46:00Z",
+ "updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0,
"downvotes": 0,
"author": {
@@ -92,6 +99,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -130,6 +138,11 @@ will be the same. In the case of a merge request from a fork,
`target_project_id` and `project_id` will be the same and
`source_project_id` will be the fork project's ID.
+**Note**: the `changes_count` value in the response is a string, not an
+integer. This is because when an MR has too many changes to display and store,
+it will be capped at 1,000. In that case, the API will return the string
+`"1000+"` for the changes count.
+
Parameters:
| Attribute | Type | Required | Description |
@@ -159,6 +172,8 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "opened",
+ "created_at": "2017-04-29T08:46:00Z",
+ "updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0,
"downvotes": 0,
"author": {
@@ -198,6 +213,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -234,6 +250,8 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "merged",
+ "created_at": "2017-04-29T08:46:00Z",
+ "updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0,
"downvotes": 0,
"author": {
@@ -274,6 +292,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": "9999999999999999999999999999999999999999",
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -386,6 +405,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -480,6 +500,7 @@ POST /projects/:id/merge_requests
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 0,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -565,6 +586,7 @@ Must include at least one non-required attribute from above.
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -670,6 +692,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": "9999999999999999999999999999999999999999",
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -747,6 +770,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -822,7 +846,8 @@ Example response when the GitLab issue tracker is used:
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
"labels" : [],
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "changes_count": "1"
},
]
```
@@ -1077,6 +1102,7 @@ Example response:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 7,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1"
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 51962595e33..50685f335f7 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -4,6 +4,31 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages]
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
+## List all pages domains
+
+Get a list of all pages domains. The user must have admin permissions.
+
+```http
+GET /pages/domains
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/pages/domains
+```
+
+```json
+[
+ {
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "certificate": {
+ "expired": false,
+ "expiration": "2020-04-12T14:32:00.000Z"
+ }
+ }
+]
+```
+
## List pages domains
Get a list of project pages domains. The user must have permissions to view pages domains.
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 890945cfc7e..a6631cab8c3 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -57,7 +57,7 @@ GET /projects/:id/pipelines/:pipeline_id
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline/46"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
```
Example of response
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 07331d05231..5a403f7593a 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -192,6 +192,12 @@ GET /projects
]
```
+You can filter by [custom attributes](custom_attributes.md) with:
+
+```
+GET /projects?custom_attributes[key]=value&custom_attributes[other_key]=other_value
+```
+
## List user projects
Get a list of visible projects for the given user. When accessed without
diff --git a/doc/api/services.md b/doc/api/services.md
index 6c8f196fd5c..08df26db3ec 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -490,6 +490,41 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
+## Kubernetes
+
+Kubernetes / Openshift integration
+
+### Create/Edit Kubernetes service
+
+Set Kubernetes service for a project.
+
+```
+PUT /projects/:id/services/kubernetes
+```
+
+Parameters:
+
+- `namespace` (**required**) - The Kubernetes namespace to use
+- `api_url` (**required**) - The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com
+- `token` (**required**) - The service token to authenticate against the Kubernetes cluster with
+- `ca_pem` (optional) - A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)
+
+### Delete Kubernetes service
+
+Delete Kubernetes service for a project.
+
+```
+DELETE /projects/:id/services/kubernetes
+```
+
+### Get Kubernetes service settings
+
+Get Kubernetes service settings for a project.
+
+```
+GET /projects/:id/services/kubernetes
+```
+
## Slack slash commands
Ability to receive slash commands from a Slack chat instance.
@@ -572,7 +607,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Mattermost token |
-
+| `username` | string | no | The username to use to post the message |
### Delete Mattermost slash command service
@@ -582,6 +617,40 @@ Delete Mattermost slash command service for a project.
DELETE /projects/:id/services/mattermost-slash-commands
```
+## Packagist
+
+Update your project on Packagist, the main Composer repository, when commits or tags are pushed to GitLab.
+
+### Create/Edit Packagist service
+
+Set Packagist service for a project.
+
+```
+PUT /projects/:id/services/packagist
+```
+
+Parameters:
+
+- `username` (**required**)
+- `token` (**required**)
+- `server` (optional)
+
+### Delete Packagist service
+
+Delete Packagist service for a project.
+
+```
+DELETE /projects/:id/services/packagist
+```
+
+### Get Packagist service settings
+
+Get Packagist service settings for a project.
+
+```
+GET /projects/:id/services/packagist
+```
+
## Pipeline-Emails
Get emails for GitLab CI pipelines.
diff --git a/doc/api/session.md b/doc/api/session.md
deleted file mode 100644
index b97e26f34a2..00000000000
--- a/doc/api/session.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Session API
-
->**Deprecation notice:**
-Starting in GitLab 8.11, this feature has been **disabled** for users with
-[two-factor authentication][2fa] turned on. These users can access the API
-using [personal access tokens] instead.
-
-You can login with both GitLab and LDAP credentials in order to obtain the
-private token.
-
-```
-POST /session
-```
-
-| Attribute | Type | Required | Description |
-| ---------- | ------- | -------- | -------- |
-| `login` | string | yes | The username of the user|
-| `email` | string | yes if login is not provided | The email of the user |
-| `password` | string | yes | The password of the user |
-
-```bash
-curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd"
-```
-
-Example response:
-
-```json
-{
- "name": "John Smith",
- "username": "john_smith",
- "id": 32,
- "state": "active",
- "avatar_url": null,
- "created_at": "2015-01-29T21:07:19.440Z",
- "is_admin": true,
- "bio": null,
- "skype": "",
- "linkedin": "",
- "twitter": "",
- "website_url": "",
- "email": "john@example.com",
- "theme_id": 1,
- "color_scheme_id": 1,
- "projects_limit": 10,
- "current_sign_in_at": "2015-07-07T07:10:58.392Z",
- "identities": [],
- "can_create_group": true,
- "can_create_project": true,
- "two_factor_enabled": false,
- "private_token": "9koXpg98eAheJpvBs5tK"
-}
-```
-
-[2fa]: ../user/profile/account/two_factor_authentication.md
-[personal access tokens]: ../user/profile/personal_access_tokens.md
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 4e24e4bbfc3..b27220f57f4 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -79,7 +79,7 @@ PUT /application/settings
| `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
| `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts |
-| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. |
+| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and masters can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but masters can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. |
| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` |
diff --git a/doc/api/users.md b/doc/api/users.md
index 1643c584244..478d747a50d 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -297,6 +297,7 @@ Parameters:
- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
+- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default)
- `external` (optional) - Flags the user as external - true or false(default)
- `avatar` (optional) - Image file for user's avatar
@@ -410,8 +411,7 @@ GET /user
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
- "external": false,
- "private_token": "dd34asd13as"
+ "external": false
}
```
diff --git a/doc/ci/README.md b/doc/ci/README.md
index ec0ddfbea75..12404eddbe2 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Continuous Integration (GitLab CI)
![Pipeline graph](img/cicd_pipeline_infograph.png)
diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md
index 99669a9272a..b0e01d74f7e 100644
--- a/doc/ci/docker/README.md
+++ b/doc/ci/docker/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Docker integration
- [Using Docker Images](using_docker_images.md)
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 4586caa457d..0a2419b7ed2 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -31,12 +31,12 @@ There are three methods to enable the use of `docker build` and `docker run` dur
The simplest approach is to install GitLab Runner in `shell` execution mode.
GitLab Runner then executes job scripts as the `gitlab-runner` user.
-1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/#installation).
1. During GitLab Runner installation select `shell` as method of executing job scripts or use command:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor shell \
@@ -93,7 +93,7 @@ In order to do that, follow the steps:
mode:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
@@ -178,7 +178,7 @@ In order to do that, follow the steps:
1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index f7493794b6a..ecb8f15c851 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -501,8 +501,8 @@ First start with creating a file named `build_script`:
```bash
cat <<EOF > build_script
-git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner
-cd /builds/gitlab-org/gitlab-ci-multi-runner
+git clone https://gitlab.com/gitlab-org/gitlab-runner.git /builds/gitlab-org/gitlab-runner
+cd /builds/gitlab-org/gitlab-runner
make
EOF
```
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index b8f9988e3ef..7aa7de97c43 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -1,4 +1,4 @@
-## Enable or disable GitLab CI/CD
+# How to enable or disable GitLab CI/CD
To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
file present at the root directory of your project and a
@@ -21,7 +21,7 @@ individually under each project's settings, or site-wide by modifying the
settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
respectively.
-### Per-project user setting
+## Per-project user setting
The setting to enable or disable GitLab CI/CD can be found under your project's
**Settings > General > Permissions**. Choose one of "Disabled", "Only team members"
@@ -29,7 +29,7 @@ or "Everyone with access" and hit **Save changes** for the settings to take effe
![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png)
-### Site-wide admin setting
+## Site-wide admin setting
You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml`
and `gitlab.rb` for source and Omnibus installations respectively.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index f094546c3bd..d05b4db953a 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab CI Examples
A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md
index b9f0485290e..bed379b0254 100644
--- a/doc/ci/examples/deployment/composer-npm-deploy.md
+++ b/doc/ci/examples/deployment/composer-npm-deploy.md
@@ -1,4 +1,4 @@
-## Running Composer and NPM scripts with deployment via SCP
+# Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD
This guide covers the building dependencies of a PHP project while compiling assets via an NPM script.
@@ -39,13 +39,13 @@ In this particular case, the `npm deploy` script is a Gulp script that does the
All these operations will put all files into a `build` folder, which is ready to be deployed to a live server.
-### How to transfer files to a live server?
+## How to transfer files to a live server
You have multiple options: rsync, scp, sftp and so on. For now, we will use scp.
To make this work, you need to add a GitLab Secret Variable (accessible on _gitlab.example/your-project-name/variables_). That variable will be called `STAGING_PRIVATE_KEY` and it's the **private** ssh key of your server.
-#### Security tip
+### Security tip
Create a user that has access **only** to the folder that needs to be updated!
@@ -69,7 +69,7 @@ In order, this means that:
And this is basically all you need in the `before_script` section.
-## How to deploy things?
+## How to deploy things
As we stated above, we need to deploy the `build` folder from the docker image to our server. To do so, we create a new job:
@@ -88,7 +88,7 @@ stage_deploy:
- ssh -p22 server_user@server_host "rm -rf htdocs/wp-content/themes/_old"
```
-### What's going on here?
+Here's the breakdown:
1. `only:dev` means that this build will run only when something is pushed to the `dev` branch. You can remove this block completely and have everything be ran on every push (but probably this is something you don't want)
2. `ssh-add ...` we will add that private key you added on the web UI to the docker container
@@ -99,7 +99,7 @@ stage_deploy:
What's the deal with the artifacts? We just tell GitLab CI to keep the `build` directory (later on, you can download that as needed).
-#### Why we do it this way?
+### Why we do it this way
If you're using this only for stage server, you could do this in two steps:
@@ -112,7 +112,7 @@ The problem is that there will be a small period of time when you won't have the
So we use so many steps because we want to make sure that at any given time we have a functional app in place.
-## Where to go next?
+## Where to go next
Since this was a WordPress project, I gave real life code snippets. Some ideas you can pursuit:
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index f2dd12b67d3..6768a2e012f 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -267,10 +267,10 @@ terminal execute:
```bash
# Check using docker executor
-gitlab-ci-multi-runner exec docker test:app
+gitlab-runner exec docker test:app
# Check using shell executor
-gitlab-ci-multi-runner exec shell test:app
+gitlab-runner exec shell test:app
```
## Example project
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 73aebaf6d7f..a6ed1c54e16 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -1,10 +1,13 @@
-## Test and Deploy a python application
+# Test and Deploy a python application with GitLab CI/CD
+
This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application.
-You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://gitlab.com/ayufan/python-getting-started/builds?scope=all).
+You can checkout the [example source](https://gitlab.com/ayufan/python-getting-started).
+
+## Configure project
-### Configure project
This is what the `.gitlab-ci.yml` file looks like for this project:
+
```yaml
test:
script:
@@ -41,23 +44,27 @@ This project has three jobs:
2. `staging` - used to automatically deploy staging environment every push to `master` branch
3. `production` - used to automatically deploy production environmnet for every created tag
-### Store API keys
+## Store API keys
+
You'll need to create two variables in `Project > Variables`:
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
-### Create Heroku application
+## Create Heroku application
+
For each of your environments, you'll need to create a new Heroku application.
You can do this through the [Dashboard](https://dashboard.heroku.com/).
-### Create runner
+## Create Runner
+
First install [Docker Engine](https://docs.docker.com/installation/).
-To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
+To build this project you also need to have [GitLab Runner](https://docs.gitlab.com/runner).
You can use public runners available on `gitlab.com`, but you can register your own:
+
```
-gitlab-ci-multi-runner register \
+gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 6fa64a67e82..10fd2616fab 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -1,10 +1,13 @@
-## Test and Deploy a ruby application
+# Test and Deploy a ruby application with GitLab CI/CD
+
This example will guide you how to run tests in your Ruby on Rails application and deploy it automatically as Heroku application.
You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
-### Configure project
+## Configure the project
+
This is what the `.gitlab-ci.yml` file looks like for this project:
+
```yaml
test:
script:
@@ -36,23 +39,28 @@ This project has three jobs:
2. `staging` - used to automatically deploy staging environment every push to `master` branch
3. `production` - used to automatically deploy production environment for every created tag
-### Store API keys
-You'll need to create two variables in `Project > Variables`:
+## Store API keys
+
+You'll need to create two variables in your project's **Settings > CI/CD > Variables**:
+
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
-### Create Heroku application
+## Create Heroku application
+
For each of your environments, you'll need to create a new Heroku application.
You can do this through the [Dashboard](https://dashboard.heroku.com/).
-### Create runner
+## Create Runner
+
First install [Docker Engine](https://docs.docker.com/installation/).
To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
You can use public runners available on `gitlab.com`, but you can register your own:
+
```
-gitlab-ci-multi-runner register \
+gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
@@ -62,6 +70,6 @@ gitlab-ci-multi-runner register \
--docker-postgres latest
```
-With the command above, you create a runner that uses [ruby:2.2](https://hub.docker.com/r/_/ruby/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database.
+With the command above, you create a Runner that uses [ruby:2.2](https://hub.docker.com/r/_/ruby/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database.
To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md
index 56b746ce025..3b1026d174f 100644
--- a/doc/ci/examples/test-clojure-application.md
+++ b/doc/ci/examples/test-clojure-application.md
@@ -1,10 +1,10 @@
-## Test a Clojure application
+# Test a Clojure application with GitLab CI/CD
This example will guide you how to run tests in your Clojure application.
You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://gitlab.com/dzaporozhets/clojure-web-application/builds?scope=all).
-### Configure project
+## Configure the project
This is what the `.gitlab-ci.yml` file looks like for this project:
@@ -23,13 +23,13 @@ before_script:
- lein deps
- lein migratus migrate
-test:
- script:
+test:
+ script:
- lein test
```
-In before script we install JRE and [Leiningen](http://leiningen.org/).
-Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations.
+In before script we install JRE and [Leiningen](http://leiningen.org/).
+Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations.
So we added database migration as last step of `before_script` section
You can use public runners available on `gitlab.com` for testing your application with such configuration.
diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md
index 150698ca04b..f6c81b076bc 100644
--- a/doc/ci/examples/test-phoenix-application.md
+++ b/doc/ci/examples/test-phoenix-application.md
@@ -1,9 +1,9 @@
-## Test a Phoenix application
+# Test a Phoenix application with GitLab CI/CD
This example demonstrates the integration of Gitlab CI with Phoenix, Elixir and
Postgres.
-### Add `.gitlab-ci.yml` file to project
+## Add `.gitlab-ci.yml` to project
The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
@@ -36,7 +36,7 @@ run your migrations.
Finally, the test `script` will run your tests.
-### Update the Config Settings
+## Update the Config Settings
In `config/test.exs`, update the database hostname:
@@ -45,12 +45,12 @@ config :my_app, MyApp.Repo,
hostname: if(System.get_env("CI"), do: "postgres", else: "localhost"),
```
-### Add the Migrations Folder
+## Add the Migrations Folder
If you do not have any migrations yet, you will need to create an empty
`.gitkeep` file in `priv/repo/migrations`.
-### Sources
+## Sources
- https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142
- https://davejlong.com/ci-with-phoenix-and-gitlab/
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 36c6e153d95..286f3dee665 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -8,7 +8,7 @@
with the use of [SSH keys](ssh_keys/README.md).
- With GitLab 8.12 onward, your permissions are used to evaluate what a CI job
can access. More information about how this system works can be found in the
- [Jobs permissions model](../user/permissions.md#jobs-permissions).
+ [Jobs permissions model](../user/permissions.md#job-permissions).
- The HTTP(S) Git protocol [must be enabled][gitpro] in your GitLab instance.
## Configuring the `.gitmodules` file
@@ -61,7 +61,7 @@ correctly with your CI jobs:
1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file)
for the submodules located in the same GitLab server.
-1. Next, if you are using `gitlab-ci-multi-runner` v1.10+, you can set the
+1. Next, if you are using `gitlab-runner` v1.10+, you can set the
`GIT_SUBMODULE_STRATEGY` variable to either `normal` or `recursive` to tell
the runner to fetch your submodules before the job:
```yaml
@@ -71,7 +71,7 @@ correctly with your CI jobs:
See the [`.gitlab-ci.yml` reference](yaml/README.md#git-submodule-strategy)
for more details about `GIT_SUBMODULE_STRATEGY`.
-1. If you are using an older version of `gitlab-ci-multi-runner`, then use
+1. If you are using an older version of `gitlab-runner`, then use
`git submodule sync/update` in `before_script`:
```yaml
diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md
index 42eb59f84c8..80d8e46f29c 100644
--- a/doc/ci/permissions/README.md
+++ b/doc/ci/permissions/README.md
@@ -1,3 +1 @@
-# Users Permissions
-
This document was moved to [user/permissions.md](../../user/permissions.md#gitlab-ci).
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 2d56b2540ef..f621bf07251 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,4 +1,4 @@
-# Getting started with GitLab CI
+# Getting started with GitLab CI/CD
>**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
is fully integrated into GitLab itself and is [enabled] by default on all
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 8b51d112a2c..df66810a838 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -1,4 +1,4 @@
-# Runners
+# Configuring GitLab Runners
In GitLab CI, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md).
They are isolated (virtual) machines that pick up jobs through the coordinator
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index 4b79461d55c..d94b472b768 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -1,4 +1,8 @@
-## GitLab CI Services
+---
+comments: false
+---
+
+# GitLab CI Services
GitLab CI uses the `services` keyword to define what docker containers should
be linked with your base image. Below is a list of examples you may use.
diff --git a/doc/ci/services/docker-services.md b/doc/ci/services/docker-services.md
index df36ebaf7d4..787c5e462e4 100644
--- a/doc/ci/services/docker-services.md
+++ b/doc/ci/services/docker-services.md
@@ -1,5 +1,9 @@
-## GitLab CI Services
+---
+comments: false
+---
-+ [Using MySQL](mysql.md)
-+ [Using PostgreSQL](postgres.md)
-+ [Using Redis](redis.md)
+# GitLab CI Services
+
+- [Using MySQL](mysql.md)
+- [Using PostgreSQL](postgres.md)
+- [Using Redis](redis.md)
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 73568757aaa..a9e6bda9916 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -1,4 +1,4 @@
-# Variables
+# GitLab CI/CD Variables
When receiving a job from GitLab CI, the [Runner] prepares the build environment.
It starts by setting a list of **predefined variables** (environment variables)
@@ -43,7 +43,7 @@ future GitLab releases.**
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
-| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Mark that job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
+| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
@@ -66,6 +66,7 @@ future GitLab releases.**
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
+| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
@@ -74,7 +75,7 @@ future GitLab releases.**
| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
-| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Mark that job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
+| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 6ad70707594..f40d2c5e347 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -124,7 +124,7 @@ stages:
1. First, all jobs of `build` are executed in parallel.
1. If all jobs of `build` succeed, the `test` jobs are executed in parallel.
1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel.
-1. If all jobs of `deploy` succeed, the commit is marked as `success`.
+1. If all jobs of `deploy` succeed, the commit is marked as `passed`.
1. If any of the previous jobs fails, the commit is marked as `failed` and no
jobs of further stage are executed.
diff --git a/doc/development/README.md b/doc/development/README.md
index 36096842344..6892838be7f 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab development guides
## Get started!
@@ -18,6 +22,7 @@
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](fe_guide/index.md)
+- [Emoji guide](fe_guide/emojis.md)
## Backend guides
@@ -33,6 +38,7 @@
- [Gotchas](gotchas.md) to avoid
- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md)
+- [Working with the GitHub importer](github_importer.md)
## Performance guides
@@ -65,6 +71,7 @@
- [Iterating tables in batches](iterating_tables_in_batches.md)
- [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md)
+- [Database Debugging and Troubleshooting](database_debugging.md)
## Testing guides
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
new file mode 100644
index 00000000000..50eb8005b44
--- /dev/null
+++ b/doc/development/database_debugging.md
@@ -0,0 +1,55 @@
+# Database Debugging and Troubleshooting
+
+This section is to help give some copy-pasta you can use as a reference when you
+run into some head-banging database problems.
+
+An easy first step is to search for your error in Slack or google "GitLab <my error>".
+
+---
+
+Available `RAILS_ENV`
+
+ - `production` (generally not for your main GDK db, but you may need this for e.g. omnibus)
+ - `development` (this is your main GDK db)
+ - `test` (used for tests like rspec and spinach)
+
+
+## Nuke everything and start over
+
+If you just want to delete everything and start over with an empty DB (~1 minute):
+
+ - `bundle exec rake db:reset RAILS_ENV=development`
+
+If you just want to delete everything and start over with dummy data (~40 minutes). This also does `db:reset` and runs DB-specific migrations:
+
+ - `bundle exec rake dev:setup RAILS_ENV=development`
+
+If your test DB is giving you problems, it is safe to nuke it because it doesn't contain important data:
+
+ - `bundle exec rake db:reset RAILS_ENV=test`
+
+## Migration wrangling
+
+ - `bundle exec rake db:migrate RAILS_ENV=development`: Execute any pending migrations that you may have picked up from a MR
+ - `bundle exec rake db:migrate:status RAILS_ENV=development`: Check if all migrations are `up` or `down`
+ - `bundle exec rake db:migrate:down VERSION=20170926203418 RAILS_ENV=development`: Tear down a migration
+ - `bundle exec rake db:migrate:up VERSION=20170926203418 RAILS_ENV=development`: Setup a migration
+ - `bundle exec rake db:migrate:redo VERSION=20170926203418 RAILS_ENV=development`: Re-run a specific migration
+
+
+## Manually access the database
+
+Access the database via one of these commands (they all get you to the same place)
+
+```
+gdk psql -d gitlabhq_development
+bundle exec rails dbconsole RAILS_ENV=development
+bundle exec rails db RAILS_ENV=development
+```
+
+ - `\q`: Quit/exit
+ - `\dt`: List all tables
+ - `\d+ issues`: List columns for `issues` table
+ - `CREATE TABLE board_labels();`: Create a table called `board_labels`
+ - `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run
+ - `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 798f40eef3d..0e4ffbd7910 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -459,11 +459,11 @@ Rendered example:
### cURL commands
- Use `https://gitlab.example.com/api/v4/` as an endpoint.
-- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`.
+- Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`.
- Always put the request first. `GET` is the default so you don't have to
include it.
- Use double quotes to the URL when it includes additional parameters.
-- Prefer to use examples using the private token and don't pass data of
+- Prefer to use examples using the personal access token and don't pass data of
username and password.
| Methods | Description |
diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md
new file mode 100644
index 00000000000..962fe3dcec9
--- /dev/null
+++ b/doc/development/fe_guide/axios.md
@@ -0,0 +1,68 @@
+# Axios
+We use [axios][axios] to communicate with the server in Vue applications and most new code.
+
+In order to guarantee all defaults are set you *should not use `axios` directly*, you should import `axios` from `axios_utils`.
+
+## CSRF token
+All our request require a CSRF token.
+To guarantee this token is set, we are importing [axios][axios], setting the token, and exporting `axios` .
+
+This exported module should be used instead of directly using `axios` to ensure the token is set.
+
+## Usage
+```javascript
+ import axios from '~/lib/utils/axios_utils';
+
+ axios.get(url)
+ .then((response) => {
+ // `data` is the response that was provided by the server
+ const data = response.data;
+
+ // `headers` the headers that the server responded with
+ // All header names are lower cased
+ const paginationData = response.headers;
+ })
+ .catch(() => {
+ //handle the error
+ });
+```
+
+## Mock axios response on tests
+
+To help us mock the responses we need we use [axios-mock-adapter][axios-mock-adapter]
+
+
+```javascript
+ import axios from '~/lib/utils/axios_utils';
+ import MockAdapter from 'axios-mock-adapter';
+
+ let mock;
+ beforeEach(() => {
+ // This sets the mock adapter on the default instance
+ mock = new MockAdapter(axios);
+ // Mock any GET request to /users
+ // arguments for reply are (status, data, headers)
+ mock.onGet('/users').reply(200, {
+ users: [
+ { id: 1, name: 'John Smith' }
+ ]
+ });
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+```
+
+### Mock poll requests on tests with axios
+
+Because polling function requires an header object, we need to always include an object as the third argument:
+
+```javascript
+ mock.onGet('/users').reply(200, { foo: 'bar' }, {});
+```
+
+[axios]: https://github.com/axios/axios
+[axios-instance]: #creating-an-instance
+[axios-interceptors]: https://github.com/axios/axios#interceptors
+[axios-mock-adapter]: https://github.com/ctimmerm/axios-mock-adapter
diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md
new file mode 100644
index 00000000000..e1660ac5caa
--- /dev/null
+++ b/doc/development/fe_guide/dropdowns.md
@@ -0,0 +1,38 @@
+# Dropdowns
+
+
+## How to style a bootstrap dropdown
+1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
+1. Add a specific class to the top level `.dropdown` element
+
+
+ ```Haml
+ .dropdown.my-dropdown
+ %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
+ %span.dropdown-toggle-text
+ Toggle Dropdown
+ = icon('chevron-down')
+
+ %ul.dropdown-menu
+ %li
+ %a
+ item!
+ ```
+
+ Or use the helpers
+ ```Haml
+ .dropdown.my-dropdown
+ = dropdown_toggle('Toogle!', { toggle: 'dropdown' })
+ = dropdown_content
+ %li
+ %a
+ item!
+ ```
+
+1. Include the mixin in CSS
+
+ ```SCSS
+ @include new-style-dropdown('.my-dropdown ');
+ ```
+
+[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
diff --git a/doc/development/fe_guide/emojis.md b/doc/development/fe_guide/emojis.md
new file mode 100644
index 00000000000..38794c47965
--- /dev/null
+++ b/doc/development/fe_guide/emojis.md
@@ -0,0 +1,27 @@
+# Emojis
+
+GitLab supports native unicode emojis and fallsback to image-based emojis selectively
+when your platform does not support it.
+
+# How to update Emojis
+
+ 1. Update the `gemojione` gem
+ 1. Update `fixtures/emojis/index.json` from [Gemojione](https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json).
+ In the future, we could grab the file directly from the gem.
+ We should probably make a PR on the Gemojione project to get access to
+ all emojis after being parsed or just a raw path to the `json` file itself.
+ 1. Ensure [`emoji-unicode-version`](https://www.npmjs.com/package/emoji-unicode-version)
+ is up to date with the latest version.
+ 1. Run `bundle exec rake gemojione:aliases`
+ 1. Run `bundle exec rake gemojione:digests`
+ 1. Run `bundle exec rake gemojione:sprite`
+ 1. Ensure new sprite sheets generated for 1x and 2x
+ - `app/assets/images/emoji.png`
+ - `app/assets/images/emoji@2x.png`
+ 1. Ensure you see new individual images copied into `app/assets/images/emoji/`
+ 1. Ensure you can see the new emojis and their aliases in the GFM Autocomplete
+ 1. Ensure you can see the new emojis and their aliases in the award emoji menu
+ 1. You might need to add new emoji unicode support checks and rules for platforms
+ that do not support a certain emoji and we need to fallback to an image.
+ See `app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js`
+ and `app/assets/javascripts/emoji/support/unicode_support_map.js`
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index a76e978bd26..b288ee95722 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on
### Usage in HAML/Rails
-To use a sprite Icon in HAML or Rails we use a specific helper function :
+To use a sprite Icon in HAML or Rails we use a specific helper function :
`sprite_icon(icon_name, size: nil, css_class: '')`
-**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`).
+**icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
+
**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
+
**css_class (optional)** If you want to add additional css classes
-**Example**
+**Example**
`= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
@@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function :
`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
+### Usage in Vue
+
+We have a special Vue component for our sprite icons in `\vue_shared\components\icon.vue`.
+
+Sample usage :
+
+`<icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />`
+
+**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
+
+**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes)
+
+**css-classes (optional)** Additional CSS Classes to add to the svg tag.
+
### Usage in HTML/JS
-Please use the following function inside JS to render an icon :
+Please use the following function inside JS to render an icon :
`gl.utils.spriteIcon(iconName)`
## Adding a new icon to the sprite
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
-To upgrade to a new SVG Sprite version run `yarn upgrade https://gitlab.com/gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders.
+To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
# SVG Illustrations
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 8f956681693..72cb557d054 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -71,12 +71,14 @@ Vue specific design patterns and practices.
---
-## [Vue Resource](vue_resource.md)
-Vue resource specific practices and gotchas.
+## [Axios](axios.md)
+Axios specific practices and gotchas.
## [Icons](icons.md)
How we use SVG for our Icons.
+## [Dropdowns](dropdowns.md)
+How we use dropdowns.
---
## Style Guides
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index f88f0753687..6e9f18dd1c3 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -178,16 +178,13 @@ itself, please read this guide: [State Management][state-management]
The Service is a class used only to communicate with the server.
It does not store or manipulate any data. It is not aware of the store or the components.
-We use [vue-resource][vue-resource-repo] to communicate with the server.
-Refer to [vue resource](vue_resource.md) for more details.
+We use [axios][axios] to communicate with the server.
+Refer to [axios](axios.md) for more details.
-Vue Resource should only be imported in the service file.
+Axios instance should only be imported in the service file.
```javascript
- import Vue from 'vue';
- import VueResource from 'vue-resource';
-
- Vue.use(VueResource);
+ import axios from 'javascripts/lib/utils/axios_utils';
```
### End Result
@@ -230,15 +227,14 @@ export default class Store {
}
// service.js
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import 'vue_shared/vue_resource_interceptor';
-
-Vue.use(VueResource);
+import axios from 'javascripts/lib/utils/axios_utils'
export default class Service {
constructor(options) {
- this.todos = Vue.resource(endpoint.todosEndpoint);
+ this.todos = axios.create({
+ baseURL: endpoint.todosEndpoint
+ });
+
}
getTodos() {
@@ -477,50 +473,8 @@ The main return value of a Vue component is the rendered output. In order to tes
need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
### Stubbing API responses
-[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with
-the response we need:
-
- ```javascript
- // Mock the service to return data
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([{
- title: 'This is a todo',
- body: 'This is the text'
- }]), {
- status: 200,
- }));
- };
+Refer to [mock axios](axios.md#mock-axios-response-on-tests)
- beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
- });
-
- it('should do something', (done) => {
- setTimeout(() => {
- // Test received data
- done();
- }, 0);
- });
- ```
-
-1. Headers interceptor
-Refer to [this section](vue.md#headers)
-
-1. Use `$.mount()` to mount the component
-
-```javascript
-// bad
-new Component({
- el: document.createElement('div')
-});
-
-// good
-new Component().$mount();
-```
## Vuex
To manage the state of an application you may use [Vuex][vuex-docs].
@@ -721,7 +675,6 @@ describe('component', () => {
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
-[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[flux]: https://facebook.github.io/flux
@@ -729,3 +682,6 @@ describe('component', () => {
[vuex-structure]: https://vuex.vuejs.org/en/structure.html
[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
[vuex-testing]: https://vuex.vuejs.org/en/testing.html
+[axios]: https://github.com/axios/axios
+[axios-interceptors]: https://github.com/axios/axios#interceptors
+
diff --git a/doc/development/fe_guide/vue_resource.md b/doc/development/fe_guide/vue_resource.md
deleted file mode 100644
index c376c5c32bf..00000000000
--- a/doc/development/fe_guide/vue_resource.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Vue Resouce
-In Vue applications we use [vue-resource][vue-resource-repo] to communicate with the server.
-
-## HTTP Status Codes
-
-### `.json()`
-When making a request to the server, you will most likely need to access the body of the response.
-Use `.json()` to convert. Because `.json()` returns a Promise the follwoing structure should be used:
-
- ```javascript
- service.get('url')
- .then(resp => resp.json())
- .then((data) => {
- this.store.storeData(data);
- })
- .catch(() => new Flash('Something went wrong'));
- ```
-
-
-When using `Poll` (`app/assets/javascripts/lib/utils/poll.js`), the `successCallback` needs to handle `.json()` as a Promise:
- ```javascript
- successCallback: (response) => {
- return response.json().then((data) => {
- // handle the response
- });
- }
- ```
-
-### 204
-Some endpoints - usually `delete` endpoints - return `204` as the success response.
-When handling `204 - No Content` responses, we cannot use `.json()` since it tries to parse the non-existant body content.
-
-When handling `204` responses, do not use `.json`, otherwise the promise will throw an error and will enter the `catch` statement:
-
-```javascript
- Vue.http.delete('path')
- .then(() => {
- // success!
- })
- .catch(() => {
- // handle error
- })
-```
-
-## Headers
-Headers are being parsed into a plain object in an interceptor.
-In Vue-resource 1.x `headers` object was changed into an `Headers` object. In order to not change all old code, an interceptor was added.
-
-If you need to write a unit test that takes the headers in consideration, you need to include an interceptor to parse the headers after your test interceptor.
-You can see an example in `spec/javascripts/environments/environment_spec.js`:
- ```javascript
- import { headersInterceptor } from './helpers/vue_resource_helper';
-
- beforeEach(() => {
- Vue.http.interceptors.push(myInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, myInterceptor);
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
- ```
-
-## CSRF token
-We use a Vue Resource interceptor to manage the CSRF token.
-`app/assets/javascripts/vue_shared/vue_resource_interceptor.js` holds all our common interceptors.
-Note: You don't need to load `app/assets/javascripts/vue_shared/vue_resource_interceptor.js`
-since it's already being loaded by `common_vue.js`.
-
-
-[vue-resource-repo]: https://github.com/pagekit/vue-resource
diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md
new file mode 100644
index 00000000000..cf00e24e11a
--- /dev/null
+++ b/doc/development/file_storage.md
@@ -0,0 +1,49 @@
+# File Storage in GitLab
+
+We use the [CarrierWave] gem to handle file upload, store and retrieval.
+
+There are many places where file uploading is used, according to contexts:
+
+* System
+ - Instance Logo (logo visible in sign in/sign up pages)
+ - Header Logo (one displayed in the navigation bar)
+* Group
+ - Group avatars
+* User
+ - User avatars
+ - User snippet attachments
+* Project
+ - Project avatars
+ - Issues/MR Markdown attachments
+ - Issues/MR Legacy Markdown attachments
+ - CI Build Artifacts
+ - LFS Objects
+
+
+## Disk storage
+
+GitLab started saving everything on local disk. While directory location changed from previous versions,
+they are still not 100% standardized. You can see them below:
+
+| Description | In DB? | Relative path | Uploader class | model_type |
+| ------------------------------------- | ------ | ----------------------------------------------------------- | ---------------------- | ---------- |
+| Instance logo | yes | uploads/-/system/appearance/logo/:id/:filename | `AttachmentUploader` | Appearance |
+| Header logo | yes | uploads/-/system/appearance/header_logo/:id/:filename | `AttachmentUploader` | Appearance |
+| Group avatars | yes | uploads/-/system/group/avatar/:id/:filename | `AvatarUploader` | Group |
+| User avatars | yes | uploads/-/system/user/avatar/:id/:filename | `AvatarUploader` | User |
+| User snippet attachments | yes | uploads/-/system/personal_snippet/:id/:random_hex/:filename | `PersonalFileUploader` | Snippet |
+| Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project |
+| Issues/MR Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
+| Issues/MR Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
+| CI Artifacts (CE) | yes | shared/artifacts/:year_:month/:project_id/:id | `ArtifactUploader` | Ci::Build |
+| LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject |
+
+CI Artifacts and LFS Objects behave differently in CE and EE. In CE they inherit the `GitlabUploader`
+while in EE they inherit the `ObjectStoreUploader` and store files in and S3 API compatible object store.
+
+In the case of Issues/MR Markdown attachments, there is a different approach using the [Hashed Storage] layout,
+instead of basing the path into a mutable variable `:project_path_with_namespace`, it's possible to use the
+hash of the project ID instead, if project migrates to the new approach (introduced in 10.2).
+
+[CarrierWave]: https://github.com/carrierwaveuploader/carrierwave
+[Hashed Storage]: ../administration/repository_storage_types.md
diff --git a/doc/development/github_importer.md b/doc/development/github_importer.md
new file mode 100644
index 00000000000..0d558583bb8
--- /dev/null
+++ b/doc/development/github_importer.md
@@ -0,0 +1,209 @@
+# Working with the GitHub importer
+
+In GitLab 10.2 a new version of the GitHub importer was introduced. This new
+importer performs its work in parallel using Sidekiq, greatly reducing the time
+necessary to import GitHub projects into a GitLab instance.
+
+The GitHub importer offers two different types of importers: a sequential
+importer and a parallel importer. The Rake task `import:github` uses the
+sequential importer, while everything else uses the parallel importer. The
+difference between these two importers is quite simple: the sequential importer
+does all work in a single thread, making it more useful for debugging purposes
+or Rake tasks. The parallel importer on the other hand uses Sidekiq.
+
+## Requirements
+
+* GitLab CE 10.2.0 or newer.
+* Sidekiq workers that process the `github_importer` and
+ `github_importer_advance_stage` queues (this is enabled by default).
+* Octokit (used for interacting with the GitHub API)
+
+## Code structure
+
+The importer's codebase is broken up into the following directories:
+
+* `lib/gitlab/github_import`: this directory contains most of the code such as
+ the classes used for importing resources.
+* `app/workers/gitlab/github_import`: this directory contains the Sidekiq
+ workers.
+* `app/workers/concerns/gitlab/github_import`: this directory contains a few
+ modules reused by the various Sidekiq workers.
+
+## Architecture overview
+
+When a GitHub project is imported we schedule and execute a job for the
+`RepositoryImportworker` worker as all other importers. However, unlike other
+importers we don't immediately perform the work necessary. Instead work is
+divided into separate stages, with each stage consisting out of a set of Sidekiq
+jobs that are executed. Between every stage a job is scheduled that periodically
+checks if all work of the current stage is completed, advancing the import
+process to the next stage when this is the case. The worker handling this is
+called `Gitlab::GithubImport::AdvanceStageWorker`.
+
+## Stages
+
+### 1. RepositoryImportWorker
+
+This worker will kick off the import process by simply scheduling a job for the
+next worker.
+
+### 2. Stage::ImportRepositoryWorker
+
+This worker will import the repository and wiki, scheduling the next stage when
+done.
+
+### 3. Stage::ImportBaseDataWorker
+
+This worker will import base data such as labels, milestones, and releases. This
+work is done in a single thread since it can be performed fast enough that we
+don't need to perform this work in parallel.
+
+### 4. Stage::ImportPullRequestsWorker
+
+This worker will import all pull requests. For every pull request a job for the
+`Gitlab::GithubImport::ImportPullRequestWorker` worker is scheduled.
+
+### 5. Stage::ImportIssuesAndDiffNotesWorker
+
+This worker will import all issues and pull request comments. For every issue we
+schedule a job for the `Gitlab::GithubImport::ImportIssueWorker` worker. For
+pull request comments we instead schedule jobs for the
+`Gitlab::GithubImport::DiffNoteImporter` worker.
+
+This worker processes both issues and diff notes in parallel so we don't need to
+schedule a separate stage and wait for the previous one to complete.
+
+Issues are imported separately from pull requests because only the "issues" API
+includes labels for both issue and pull requests. Importing issues and setting
+label links in the same worker removes the need for performing a separate crawl
+through the API data, reducing the number of API calls necessary to import a
+project.
+
+### 6. Stage::ImportNotesWorker
+
+This worker imports regular comments for both issues and pull requests. For
+every comment we schedule a job for the
+`Gitlab::GithubImport::ImportNoteWorker` worker.
+
+Regular comments have to be imported at the end since the GitHub API used
+returns comments for both issues and pull requests. This means we have to wait
+for all issues and pull requests to be imported before we can import regular
+comments.
+
+### 7. Stage::FinishImportWorker
+
+This worker will wrap up the import process by performing some housekeeping
+(such as flushing any caches) and by marking the import as completed.
+
+## Advancing stages
+
+Advancing stages is done in one of two ways:
+
+1. Scheduling the worker for the next stage directly.
+2. Scheduling a job for `Gitlab::GithubImport::AdvanceStageWorker` which will
+ advance the stage when all work of the current stage has been completed.
+
+The first approach should only be used by workers that perform all their work in
+a single thread, while `AdvanceStageWorker` should be used for everything else.
+
+The way `AdvanceStageWorker` works is fairly simple. When scheduling a job it
+will be given a project ID, a list of Redis keys, and the name of the next
+stage. The Redis keys (produced by `Gitlab::JobWaiter`) are used to check if the
+currently running stage has been completed or not. If the stage has not yet been
+completed `AdvanceStageWorker` will reschedule itself. Once a stage finishes
+`AdvanceStageworker` will refresh the import JID (more on this below) and
+schedule the worker of the next stage.
+
+To reduce the number of `AdvanceStageWorker` jobs scheduled this worker will
+briefly wait for jobs to complete before deciding what the next action should
+be. For small projects this may slow down the import process a bit, but it will
+also reduce pressure on the system as a whole.
+
+## Refreshing import JIDs
+
+GitLab includes a worker called `StuckImportJobsWorker` that will periodically
+run and mark project imports as failed if they have been running for more than
+15 hours. For GitHub projects this poses a bit of a problem: importing large
+projects could take several hours depending on how often we hit the GitHub rate
+limit (more on this below), but we don't want `StuckImportJobsWorker` to mark
+our import as failed because of this.
+
+To prevent this from happening we periodically refresh the expiration time of
+the import process. This works by storing the JID of the import job in the
+database, then refreshing this JID's TTL at various stages throughout the import
+process. This is done by calling `Project#refresh_import_jid_expiration`. By
+refreshing this TTL we can ensure our import does not get marked as failed so
+long we're still performing work.
+
+## GitHub rate limit
+
+GitHub has a rate limit of 5 000 API calls per hour. The number of requests
+necessary to import a project is largely dominated by the number of unique users
+involved in a project (e.g. issue authors). Other data such as issue pages
+and comments typically only requires a few dozen requests to import. This is
+because we need the Email address of users in order to map them to GitLab users.
+
+We handle this by doing the following:
+
+1. Once we hit the rate limit all jobs will automatically reschedule themselves
+ in such a way that they are not executed until the rate limit has been reset.
+2. We cache the mapping of GitHub users to GitLab users in Redis.
+
+More information on user caching can be found below.
+
+## Caching user lookups
+
+When mapping GitHub users to GitLab users we need to (in the worst case)
+perform:
+
+1. One API call to get the user's Email address.
+2. Two database queries to see if a corresponding GitLab user exists. One query
+ will try to find the user based on the GitHub user ID, while the second query
+ is used to find the user using their GitHub Email address.
+
+Because this process is quite expensive we cache the result of these lookups in
+Redis. For every user looked up we store three keys:
+
+1. A Redis key mapping GitHub usernames to their Email addresses.
+2. A Redis key mapping a GitHub Email addresses to a GitLab user ID.
+3. A Redis key mapping a GitHub user ID to GitLab user ID.
+
+There are two types of lookups we cache:
+
+1. A positive lookup, meaning we found a GitLab user ID.
+2. A negative lookup, meaning we didn't find a GitLab user ID. Caching this
+ prevents us from performing the same work for users that we know don't exist
+ in our GitLab database.
+
+The expiration time of these keys is 24 hours. When retrieving the cache of a
+positive lookups we refresh the TTL automatically. The TTL of false lookups is
+never refreshed.
+
+Because of this caching layer it's possible newly registered GitLab accounts
+won't be linked to their corresponding GitHub accounts. This however will sort
+itself out once the cached keys expire.
+
+The user cache lookup is shared across projects. This means that the more
+projects get imported the fewer GitHub API calls will be needed.
+
+The code for this resides in:
+
+* `lib/gitlab/github_import/user_finder.rb`
+* `lib/gitlab/github_import/caching.rb`
+
+## Mapping labels and milestones
+
+To reduce pressure on the database we do not query it when setting labels and
+milestones on issues and merge requests. Instead we cache this data when we
+import labels and milestones, then we reuse this cache when assigning them to
+issues/merge requests. Similar to the user lookups these cache keys are expired
+automatically after 24 hours of not being used.
+
+Unlike the user lookup caches these label and milestone caches are scoped to the
+project that is being imported.
+
+The code for this resides in:
+
+* `lib/gitlab/github_import/label_finder.rb`
+* `lib/gitlab/github_import/milestone_finder.rb`
+* `lib/gitlab/github_import/caching.rb`
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 7c38260406d..4b65a0f4a35 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -110,7 +110,7 @@ You can mark that content for translation with:
In JavaScript we added the `__()` (double underscore parenthesis) function
for translations.
-### Updating the PO files with the new content
+## Updating the PO files with the new content
Now that the new content is marked for translation, we need to update the PO
files with the following command:
@@ -119,23 +119,20 @@ files with the following command:
bundle exec rake gettext:find
```
-This command will update the `locale/**/gitlab.edit.po` file with the
-new content that the parser has found.
+This command will update the `locale/gitlab.pot` file with the newly externalized
+strings and remove any strings that aren't used anymore. You should check this
+file in. Once the changes are on master, they will be picked up by
+[Crowdin](http://translate.gitlab.com) and be presented for translation.
-New translations will be added with their default content and will be marked
-fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
-and remove it.
+The command also updates the translation files for each language: `locale/*/gitlab.po`
+These changes can be discarded, the languange files will be updated by Crowdin
+automatically.
-We need to make sure we remove the `fuzzy` translations before generating the
-`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
-be treated as a binary file which could overwrite translations that were merged
-before the new translations.
+Discard all of them at once like this:
-When we are just preparing a page to be translated, but not actually adding any
-translations. There's no need to generate `.po` files.
-
-Translations that aren't used in the source code anymore will be marked with
-`~#`; these can be removed to keep our translation files clutter-free.
+```sh
+git checkout locale/*/gitlab.po
+```
### Validating PO files
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 902b1c74a42..274923c2d43 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -4,11 +4,11 @@ GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed
## Automated Testing
-In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
+In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems and node modules in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
-There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
+There are some limitations with the automated testing, however. CSS, JavaScript, or Ruby libraries which are not included by way of Bundler, NPM, or Yarn (for instance those manually copied into our source tree in the `vendor` directory), must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
-Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually.
+Some gems may not include their license information in their `gemspec` file, and some node modules may not include their license information in their `package.json` file. These won't be detected by License Finder, and will have to be verified manually.
### License Finder commands
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
index 899be9eae4b..ba82babb38a 100644
--- a/doc/development/limit_ee_conflicts.md
+++ b/doc/development/limit_ee_conflicts.md
@@ -336,6 +336,12 @@ Blocks of code that are EE-specific should be moved to partials as much as
possible to avoid conflicts with big chunks of HAML code that that are not fun
to resolve when you add the indentation in the equation.
+### Assets
+
+#### gitlab-svgs
+
+Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
+
---
[Return to Development documentation](README.md)
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 9b8ab5da74e..a235dd74909 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -198,7 +198,43 @@ end
Keep in mind that this operation can easily take 10-15 minutes to complete on
larger installations (e.g. GitLab.com). As a result you should only add default
-values if absolutely necessary.
+values if absolutely necessary. There is a RuboCop cop that will fail if this
+method is used on some tables that are very large on GitLab.com, which would
+cause other issues.
+
+## Updating an existing column
+
+To update an existing column to a particular value, you can use
+`update_column_in_batches` (`add_column_with_default` uses this internally to
+fill in the default value). This will split the updates into batches, so we
+don't update too many rows at in a single statement.
+
+This updates the column `foo` in the `projects` table to 10, where `some_column`
+is `'hello'`:
+
+```ruby
+update_column_in_batches(:projects, :foo, 10) do |table, query|
+ query.where(table[:some_column].eq('hello'))
+end
+```
+
+To perform a computed update, the value can be wrapped in `Arel.sql`, so Arel
+treats it as an SQL literal. The below example is the same as the one above, but
+the value is set to the product of the `bar` and `baz` columns:
+
+```ruby
+update_value = Arel.sql('bar * baz')
+
+update_column_in_batches(:projects, :foo, update_value) do |table, query|
+ query.where(table[:some_column].eq('hello'))
+end
+```
+
+Like `add_column_with_default`, there is a RuboCop cop to detect usage of this
+on large tables. In the case of `update_column_in_batches`, it may be acceptable
+to run on a large table, as long as it is only updating a small subset of the
+rows in the table, but do not ignore that without validating on the GitLab.com
+staging environment - or asking someone else to do so for you - beforehand.
## Integer column type
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index e0127aaed4c..12e90101139 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -22,6 +22,52 @@ As an example you might create 5 issues in between counts, which would cause the
> **Note:** In some cases the query count might change slightly between runs for unrelated reasons. In this case you might need to test `exceed_query_limit(control_count + acceptable_change)`, but this should be avoided if possible.
+## Finding the source of the query
+
+It may be useful to identify the source of the queries by looking at the call backtrace.
+To enable this, run the specs with the `QUERY_RECORDER_DEBUG` environment variable set. For example:
+
+```
+QUERY_RECORDER_DEBUG=1 bundle exec rspec spec/requests/api/projects_spec.rb
+```
+
+This will log calls to QueryRecorder into the `test.log`. For example:
+
+```
+QueryRecorder SQL: SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('opened')) AND "issues"."confidential" = $2
+ --> /home/user/gitlab/gdk/gitlab/spec/support/query_recorder.rb:19:in `callback'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:127:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `block in finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `each'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:36:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:25:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract_adapter.rb:478:in `log'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:601:in `exec_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:585:in `execute_and_clear'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:160:in `exec_query'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:356:in `select'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:32:in `select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `block in select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:83:in `cache_sql'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:270:in `execute_simple_calculation'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:227:in `perform_calculation'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:133:in `calculate'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:48:in `count'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:20:in `uncached_count'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `block in count'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `block in fetch'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:585:in `block in save_block_result_to_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `block in instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications.rb:166:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:584:in `save_block_result_to_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `fetch'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `count'
+ --> /home/user/gitlab/gdk/gitlab/app/models/project.rb:1296:in `open_issues_count'
+```
+
## See also
- [Bullet](profiling.md#Bullet) For finding `N+1` query problems
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index bfd80aab6a4..4773b6773e8 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -122,6 +122,15 @@ they can be easily inspected.
bundle exec rake services:doc
```
+## Updating Emoji Aliases
+
+To update the Emoji aliases file (used for Emoji autocomplete) you must run the
+following:
+
+```
+bundle exec rake gemojione:aliases
+```
+
## Updating Emoji Digests
To update the Emoji digests file (used for Emoji autocomplete) you must run the
@@ -131,6 +140,7 @@ following:
bundle exec rake gemojione:digests
```
+
This will update the file `fixtures/emojis/digests.json` based on the currently
available Emoji.
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 7ddd02e6c73..8b7b015427f 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -60,6 +60,35 @@ writing one](testing_levels.md#consider-not-writing-a-system-test)!
- It's ok to look for DOM elements but don't abuse it since it makes the tests
more brittle
+#### Debugging Capybara
+
+Sometimes you may need to debug Capybara tests by observing browser behavior.
+
+You can pause Capybara and view the website on the browser by using the
+`live_debug` method in your spec. The current page will be automatically opened
+in your default browser.
+You may need to sign in first (the current user's credentials are displayed in
+the terminal).
+
+To resume the test run, press any key.
+
+For example:
+
+```
+$ bin/rspec spec/features/auto_deploy_spec.rb:34
+Running via Spring preloader in process 8999
+Run options: include {:locations=>{"./spec/features/auto_deploy_spec.rb"=>[34]}}
+
+Current example is paused for live debugging
+The current user credentials are: user2 / 12345678
+Press any key to resume the execution of the example!
+Back to the example!
+.
+
+Finished in 34.51 seconds (files took 0.76702 seconds to load)
+1 example, 0 failures
+```
+
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 9b9ba0baa71..1cbd4350284 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -126,7 +126,7 @@ always in-sync with the codebase.
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
[Gitaly]: https://gitlab.com/gitlab-org/gitaly
[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
-[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
+[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
diff --git a/doc/development/testing_guide/testing_rake_tasks.md b/doc/development/testing_guide/testing_rake_tasks.md
index 5bf185dd7b5..60163f1a230 100644
--- a/doc/development/testing_guide/testing_rake_tasks.md
+++ b/doc/development/testing_guide/testing_rake_tasks.md
@@ -1,4 +1,4 @@
-## Testing Rake tasks
+# Testing Rake tasks
To make testing Rake tasks a little easier, there is a helper that can be included
in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index fa31c496b30..16a811dbc74 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -10,6 +10,7 @@
* [Tables](#tables)
* [Blocks](#blocks)
* [Panels](#panels)
+* [Dialog modals](#dialog-modals)
* [Alerts](#alerts)
* [Forms](#forms)
* [Search box](#search-box)
@@ -254,6 +255,38 @@ Skeleton loading can replace any existing UI elements for the period in which th
---
+## Dialog modals
+
+Dialog modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal.
+
+### Usage
+
+* When the action is irreversible, dialog modals provide the details and confirm with the user before they take an advanced action.
+* When the action will affect privacy or authorization, dialog modals provide advanced information and confirm with the user.
+
+### Style
+
+* Dialog modals contain the header, body, and actions.
+ * **Header(1):** The header title is a question instead of a descriptive phrase.
+ * **Body(2):** The content in body should never be ambiguous and unclear. It provides specific information.
+ * **Actions(3):** Contains a affirmative action, a dismissive action, and an extra action. The order of actions from left to right: Dismissive action → Extra action → Affirmative action
+* Confirmations regarding labels should keep labeling styling.
+* References to commits, branches, and tags should be **monospaced**.
+
+![layout-modal](img/modals-layout-for-modals.png)
+
+### Placement
+
+* Dialog modals should always be the center of the screen horizontally and be positioned **72px** from the top.
+
+| Dialog with 2 actions | Dialog with 3 actions | Special confirmation |
+| --------------------- | --------------------- | -------------------- |
+| ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![spcial-confirmation](img/modals-special-confimation-dialog.png) |
+
+> TODO: Special case for dialog modal.
+
+---
+
## Panels
> TODO: Catalog how we are currently using panels and rationalize how they relate to alerts
diff --git a/doc/development/ux_guide/img/modals-general-confimation-dialog.png b/doc/development/ux_guide/img/modals-general-confimation-dialog.png
new file mode 100644
index 00000000000..00a17374a0b
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-general-confimation-dialog.png
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-layout-for-modals.png b/doc/development/ux_guide/img/modals-layout-for-modals.png
new file mode 100644
index 00000000000..6c7bc09e750
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-layout-for-modals.png
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-special-confimation-dialog.png b/doc/development/ux_guide/img/modals-special-confimation-dialog.png
new file mode 100644
index 00000000000..bf1e56326c5
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-special-confimation-dialog.png
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-three-buttons.png b/doc/development/ux_guide/img/modals-three-buttons.png
new file mode 100644
index 00000000000..519439e64e4
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-three-buttons.png
Binary files differ
diff --git a/doc/development/ux_guide/resources.md b/doc/development/ux_guide/resources.md
index 2f760c94414..db57258237f 100644
--- a/doc/development/ux_guide/resources.md
+++ b/doc/development/ux_guide/resources.md
@@ -10,4 +10,8 @@ you can use during GitLab development.
## Design repository
All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design)
-repository and maintained by GitLab UX designers. \ No newline at end of file
+repository and maintained by GitLab UX designers.
+
+## [brand.ai](https://brand.ai/git-lab/primary-brand)
+
+We are in the process of capturing our UI paradigms on brand.ai, see https://brand.ai/git-lab/primary-brand
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index cbd7c17de41..fce882a45f1 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -1,4 +1,5 @@
-## UX Personas
+# UX Personas
+
* [Nazim Ramesh](#nazim-ramesh)
- Small to medium size organisations using GitLab CE
* [James Mackey](#james-mackey)
@@ -7,16 +8,16 @@
* [Karolina Plaskaty](#karolina-plaskaty)
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
- - Working for a medium to large size organisation
+ - Working for a medium to large size organisation
-<hr>
+---
-### Nazim Ramesh
+## Nazim Ramesh
- Small to medium size organisations using GitLab CE
<img src="img/nazim-ramesh.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>32 years old
- **Location**<br>Germany
@@ -26,7 +27,7 @@
- **Frequently used programming languages**<br>JavaScript, SQL, PHP
- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
-#### Motivations
+### Motivations
Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management†returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
>
@@ -39,48 +40,48 @@ In his role as a full-stack web developer, Nazim could recommend products that h
“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.â€
>
-Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
+Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
>
“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.â€
>
-Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
+Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
-The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
+The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
-#### Frustrations
-##### Adoption to GitLab has been slow
+### Frustrations
+#### Adoption to GitLab has been slow
Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
-##### Missing Features
+#### Missing Features
Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
-##### Regressions and bugs
+#### Regressions and bugs
Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks somethingâ€. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.†Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
-##### Uses too much RAM and CPU
+#### Uses too much RAM and CPU
>
“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHubâ€
>
-##### UI/UX
+#### UI/UX
GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.â€
-#### Goals
+### Goals
* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc.
-<hr>
+---
-### James Mackey
+## James Mackey
- Medium to large size organisations using CE or EE
- Small organisations using EE
<img src="img/james-mackey.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>36 years old
- **Location**<br>US
@@ -90,7 +91,7 @@ GitLab’s interface initially attracted Nazim when he was comparing version con
- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python
- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics.
-#### Motivations
+### Motivations
James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and useâ€. After using GitLab for over a year, he began to wonder about using it at work. James explains:
>
@@ -99,7 +100,7 @@ James works for a research company which currently hires around 800 staff. He be
James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHubâ€.
-The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
+The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
James feels partially responsible for his organisation’s decision to start using GitLab.
@@ -107,33 +108,33 @@ James feels partially responsible for his organisation’s decision to start usi
“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.â€
>
-#### Frustrations
-##### Third Party Integration
+### Frustrations
+#### Third Party Integration
Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do.
-##### UX/UI
+#### UX/UI
James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised.
-##### Permissions
+#### Permissions
>
“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.â€
>
-#### Goals
+### Goals
* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily.
* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James.
-<hr>
+---
-### Karolina Plaskaty
+## Karolina Plaskaty
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
-- Working for a medium to large size organisation
+- Working for a medium to large size organisation
<img src="img/karolina-plaskaty.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>26 years old
- **Location**<br>UK
@@ -143,22 +144,22 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
- **Frequently used programming languages**<br>JavaScript and SQL
- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
-#### Motivations
+### Motivations
Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a companyâ€. She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.†She’s also an avid reader of GitLab’s blog.
Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned†and explains that it’s “less of a technical issue and more of a cultural issue†to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms.
-#### Frustrations
-##### Unable to use GitLab at work
+### Frustrations
+#### Unable to use GitLab at work
Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab.
-##### Performance
+#### Performance
GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog†which has deterred her from running GitLab on her own machine for just hobby / personal projects.
-##### UX/UI
+#### UX/UI
Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons†and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.†As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this.
-#### Goals
+### Goals
* To develop her programming experience and to learn from other developers.
* To contribute to both her own and other open source projects.
-* To use a fast and intuitive version control platform. \ No newline at end of file
+* To use a fast and intuitive version control platform.
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 3d893ba53dd..4e15f7cfd49 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab basics
Step-by-step guides on the basics of working with Git and GitLab.
diff --git a/doc/install/README.md b/doc/install/README.md
index 656f8720361..540cb0d3f38 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Installation
GitLab can be installed via various ways. Check the [installation methods][methods]
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2a004152d5e..4efe911b778 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -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-1-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-2-stable gitlab
-**Note:** You can change `10-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 713d11b75e4..2f5d4142d04 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -1,11 +1,11 @@
-## Install GitLab under a relative URL
+# Install GitLab under a relative URL
-_**Note:**
+NOTE: **Note:**
This document describes how to run GitLab under a relative URL for installations
from source. If you are using an Omnibus package,
[the steps are different][omnibus-rel]. Use this guide along with the
[installation guide](installation.md) if you are installing GitLab for the
-first time._
+first time.
---
@@ -33,7 +33,7 @@ serve GitLab under a relative URL is:
After all the changes you need to recompile the assets and [restart GitLab].
-### Relative URL requirements
+## Relative URL requirements
If you configure GitLab with a relative URL, the assets (JavaScript, CSS, fonts,
images, etc.) will need to be recompiled, which is a task which consumes a lot
@@ -43,11 +43,11 @@ least 2GB of RAM available on your system, while we recommend 4GB RAM, and 4 or
See the [requirements](requirements.md) document for more information.
-### Enable relative URL in GitLab
+## Enable relative URL in GitLab
-_**Note:**
+NOTE: **Note:**
Do not make any changes to your web server configuration file regarding
-relative URL. The relative URL support is implemented by GitLab Workhorse._
+relative URL. The relative URL support is implemented by GitLab Workhorse.
---
@@ -115,7 +115,7 @@ Make sure to follow all steps below:
1. [Restart GitLab][] for the changes to take effect.
-### Disable relative URL in GitLab
+## Disable relative URL in GitLab
To disable the relative URL:
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 3d7becd18fc..baecf9455b0 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -80,13 +80,13 @@ errors during usage.
- 256GB RAM supports up to 32,000 users
- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/)
-We recommend having at least 2GB of swap on your server, even if you currently have
+We recommend having at least [2GB of swap on your server](https://askubuntu.com/a/505344/310789), even if you currently have
enough available RAM. Having swap will help reduce the chance of errors occurring
if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916)
to a low value like `10` to make the most of your RAM while still having the swap
available when needed.
-Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
+Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
## Database
@@ -121,7 +121,7 @@ Existing users using GitLab with MySQL/MariaDB are advised to
### PostgreSQL Requirements
-As of GitLab 10.0, PostgreSQL 9.6 or newer is required, and earlier versions are
+As of GitLab 10.0, PostgreSQL 9.6 or newer (but less than 10) is required, and earlier versions are
not supported. We highly recommend users to use PostgreSQL 9.6 as this
is the PostgreSQL version used for development and testing.
@@ -146,7 +146,7 @@ So for a machine with 2 cores, 3 unicorn workers is ideal.
For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
-To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
+To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
## Redis and Sidekiq
@@ -184,7 +184,7 @@ Runner.
We recommend using a separate machine for each GitLab Runner, if you plan to
use the CI features.
-[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md
+[security reasons]: https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/security/index.md
## Supported web browsers
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 09d96bdd338..54e78bdef54 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Integration
GitLab integrates with multiple third-party services to allow external issue
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 372e1909330..075feaeead9 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -22,6 +22,7 @@ Visit the links below for details:
- [Redmine](../user/project/integrations/redmine.md)
- [Jira](../user/project/integrations/jira.md)
- [Bugzilla](../user/project/integrations/bugzilla.md)
+- [Custom Issue Tracker](../user/project/integrations/custom_issue_tracker.md)
### Service Template
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 7485912d1a2..d9acc5bdeac 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Get started with GitLab
## Organize
diff --git a/doc/legal/README.md b/doc/legal/README.md
index 56d72ae3859..6413f1d645f 100644
--- a/doc/legal/README.md
+++ b/doc/legal/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Legal
- [Corporate contributor license agreement](corporate_contributor_license_agreement.md)
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index 7f08188bd65..ebb24ba0a7f 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -1,29 +1,2 @@
-# Corporate contributor license agreement
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.
-
- "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License.
-
-Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License.
-
-Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
-
-8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
-
-This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This document has been replaced by a Developer Certificate of Origin and License,
+as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index 59803aea080..ebb24ba0a7f 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -1,25 +1,2 @@
-# Individual contributor license agreement
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.
-
- "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to GitLab B.V., or that your employer has executed a separate Corporate CLA with GitLab B.V..
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]".
-
-8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
-
-This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This document has been replaced by a Developer Certificate of Origin and License,
+as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 2e7782736ff..9347a834510 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -372,8 +372,10 @@ CREATE TABLE
```
To fix that you need to apply this SQL statement before doing final backup:
-```
-# Omnibus
+
+```sql
+## Omnibus GitLab
+
gitlab-ci-rails dbconsole <<EOF
-- ALTER TABLES - DROP DEFAULTS
ALTER TABLE ONLY ci_application_settings ALTER COLUMN id DROP DEFAULT;
@@ -427,7 +429,8 @@ ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_
ALTER TABLE ONLY ci_web_hooks ALTER COLUMN id SET DEFAULT nextval('ci_web_hooks_id_seq'::regclass);
EOF
-# Source
+## Source installations
+
cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rails dbconsole production <<EOF
... COPY SQL STATEMENTS FROM ABOVE ...
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 7ab56c89014..8d0afa9e692 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -19,24 +19,30 @@ For example, for GitLab version 10.5.7:
* `5` represents minor version
* `7` represents patch number
-## Security releases
+## Patch releases
-The current stable release will receive security patches and bug fixes
-(eg. `8.9.0` -> `8.9.1`).
+Patch releases usually only include bug fixes and are only done for the current
+stable release. That said, in some cases, we may backport it to previous stable
+release, depending on the severity of the bug.
-Feature releases will mark the next supported stable
-release where the minor version is increased numerically by increments of one
-(eg. `8.9 -> 8.10`).
+For instance, if we release `10.1.1` with a fix for a severe bug introduced in
+`10.0.0`, we could backport the fix to a new `10.0.x` patch release.
-Our current policy is to support one stable release at any given time.
-For medium-level security issues, we may consider backporting to the previous two
+### Security releases
+
+Security releases are a special kind of patch release that only include security
+fixes and patches (see below).
+
+Our current policy is to support one stable release at any given time, but for
+medium-level security issues, we may backport security fixes to the previous two
monthly releases.
-For very serious security issues, there is [precedent](https://about.gitlab.com/2016/05/02/cve-2016-4340-patches/)
-to backport security fixes to even more monthly releases of GitLab. This decision
-is made on a case-by-case basis.
+For very serious security issues, there is
+[precedent](https://about.gitlab.com/2016/05/02/cve-2016-4340-patches/)
+to backport security fixes to even more monthly releases of GitLab.
+This decision is made on a case-by-case basis.
-## Version support
+## Upgrade recommendations
We encourage everyone to run the latest stable release to ensure that you can
easily upgrade to the most secure and feature-rich GitLab experience. In order
@@ -70,7 +76,6 @@ Please see the table below for some examples:
| -------------- | ------------ | ------------------------ | ---------------- |
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
| 10.1.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.8` -> `10.1.4` | `8.17.7` is the last version in version `8`, `9.5.8` is the last version in version `9` |
-|
More information about the release procedures can be found in our
[release-tools documentation][rel]. You may also want to read our
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index 2b81ebc9c59..2f916f5dea7 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Rake tasks
- [Backup restore](backup_restore.md)
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index 2b305cb5c99..97e9b36d1a6 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -3,49 +3,47 @@
## Notes
- The owner of the project will be the first admin
-- The groups will be created as needed
+- The groups will be created as needed, including subgroups
- The owner of the group will be the first admin
- Existing projects will be skipped
+- The existing Git repos will be moved from disk (removed from the original path)
## How to use
-### Create a new folder inside the git repositories path. This will be the name of the new group.
+### Create a new folder to import your Git repositories from.
-- For omnibus-gitlab, it is located at: `/var/opt/gitlab/git-data/repositories` by default, unless you changed
-it in the `/etc/gitlab/gitlab.rb` file.
-- For installations from source, it is usually located at: `/home/git/repositories` or you can see where
-your repositories are located by looking at `config/gitlab.yml` under the `repositories => storages` entries
-(you'll usually use the `default` storage path to start).
-
-New folder needs to have git user ownership and read/write/execute access for git user and its group:
+The new folder needs to have git user ownership and read/write/execute access for git user and its group:
```
-sudo -u git mkdir /var/opt/gitlab/git-data/repositories/new_group
+sudo -u git mkdir /var/opt/gitlab/git-data/repository-import-<date>/new_group
```
-If you are using an installation from source, replace `/var/opt/gitlab/git-data`
-with `/home/git`.
-
### Copy your bare repositories inside this newly created folder:
+- Any .git repositories found on any of the subfolders will be imported as projects
+- Groups will be created as needed, these could be nested folders. Example:
+
+If we copy the repos to `/var/opt/gitlab/git-data/repository-import-<date>`, and repo A needs to be under the groups G1 and G2, it will
+have to be created under those folders: `/var/opt/gitlab/git-data/repository-import-<date>/G1/G2/A.git`.
+
+
```
-sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repositories/new_group/
+sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repository-import-<date>/new_group/
# Do this once when you are done copying git repositories
-sudo chown -R git:git /var/opt/gitlab/git-data/repositories/new_group/
+sudo chown -R git:git /var/opt/gitlab/git-data/repository-import-<date>
```
`foo.git` needs to be owned by the git user and git users group.
-If you are using an installation from source, replace `/var/opt/gitlab/git-data`
-with `/home/git`.
+If you are using an installation from source, replace `/var/opt/gitlab/` with `/home/git`.
### Run the command below depending on your type of installation:
#### Omnibus Installation
```
-$ sudo gitlab-rake gitlab:import:repos
+$ sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
```
#### Installation from source
@@ -54,16 +52,21 @@ Before running this command you need to change the directory to where your GitLa
```
$ cd /home/git/gitlab
-$ sudo -u git -H bundle exec rake gitlab:import:repos RAILS_ENV=production
+$ sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
```
#### Example output
```
-Processing abcd.git
+Processing /var/opt/gitlab/git-data/repository-import-1/a/b/c/blah.git
+ * Using namespace: a/b/c
+ * Created blah (a/b/c/blah)
+ * Skipping repo /var/opt/gitlab/git-data/repository-import-1/a/b/c/blah.wiki.git
+Processing /var/opt/gitlab/git-data/repository-import-1/abcd.git
* Created abcd (abcd.git)
-Processing group/xyz.git
- * Created Group group (2)
+Processing /var/opt/gitlab/git-data/repository-import-1/group/xyz.git
+ * Using namespace: group (2)
* Created xyz (group/xyz.git)
+ * Skipping repo /var/opt/gitlab/git-data/repository-import-1/@shared/a/b/abcd.git
[...]
```
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 3ae46019daf..5554a0c8b78 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -149,18 +149,3 @@ cp config/secrets.yml.bak config/secrets.yml
sudo /etc/init.d/gitlab start
```
-
-## Clear authentication tokens for all users. Important! Data loss!
-
-Clear authentication tokens for all users in the GitLab database. This
-task is useful if your users' authentication tokens might have been exposed in
-any way. All the existing tokens will become invalid, and new tokens are
-automatically generated upon sign-in or user modification.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
-
-# installation from source
-bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
-```
diff --git a/doc/security/README.md b/doc/security/README.md
index 0fea6be8b55..d397ff104ab 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Security
- [Password length limits](password_length_limits.md)
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 793de9d777c..33a2d7a88a7 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -1,4 +1,4 @@
-# SSH
+# GitLab and SSH keys
Git is a distributed version control system, which means you can work locally
but you can also share or "push" your changes to other servers.
@@ -114,7 +114,7 @@ custom name continue onto the next step.
If you manually copied your public SSH key make sure you copied the entire
key starting with `ssh-rsa` and ending with your email.
-
+
1. Optionally you can test your setup by running `ssh -T git@example.com`
(replacing `example.com` with your GitLab domain) and verifying that you
receive a `Welcome to GitLab` message.
@@ -172,7 +172,7 @@ dummy user account.
If you are a project master or owner, you can add a deploy key in the
project settings under the section 'Repository'. Specify a title for the new
deploy key and paste a public SSH key. After this, the machine that uses
-the corresponding private SSH key has read-only or read-write (if enabled)
+the corresponding private SSH key has read-only or read-write (if enabled)
access to the project.
You can't add the same deploy key twice using the form.
@@ -232,7 +232,7 @@ something is wrong with your SSH setup.
- Ensure that you generated your SSH key pair correctly and added the public SSH
key to your GitLab profile
-- Try manually registering your private SSH key using `ssh-agent` as documented
+- Try manually registering your private SSH key using `ssh-agent` as documented
earlier in this document
- Try to debug the connection by running `ssh -Tv git@example.com`
(replacing `example.com` with your GitLab domain)
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index a45a4eb9e49..f2a9b1d769b 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,24 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `project_update`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events:
+
+- `project_create`
+- `project_destroy`
+- `project_rename`
+- `project_transfer`
+- `project_update`
+- `user_add_to_team`
+- `user_remove_from_team`
+- `user_create`
+- `user_destroy`
+- `user_rename`
+- `key_create`
+- `key_destroy`
+- `group_create`
+- `group_destroy`
+- `group_rename`
+- `user_add_to_group`
+- `user_remove_from_group`
The triggers for most of these are self-explanatory, but `project_update` and `project_rename` deserve some clarification: `project_update` is fired any time an attribute of a project is changed (name, description, tags, etc.) *unless* the `path` attribute is also changed. In that case, a `project_rename` is triggered instead (so that, for instance, if all you care about is the repo URL, you can just listen for `project_rename`).
@@ -72,6 +90,9 @@ X-Gitlab-Event: System Hook
}
```
+Note that `project_rename` is not triggered if the namespace changes.
+Please refer to `group_rename` and `user_rename` for that case.
+
**Project transferred:**
```json
@@ -175,6 +196,21 @@ X-Gitlab-Event: System Hook
}
```
+**User renamed:**
+
+```json
+{
+ "event_name": "user_rename",
+ "created_at": "2017-11-01T11:21:04Z",
+ "updated_at": "2017-11-01T14:04:47Z",
+ "name": "new-name",
+ "email": "best-email@example.tld",
+ "user_id": 58,
+ "username": "new-exciting-name",
+ "old_username": "old-boring-name"
+}
+```
+
**Key added**
```json
@@ -209,13 +245,15 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_create",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
- "owner_name": "John Smith",
+ "owner_email": null,
+ "owner_name": null,
"path": "storecloud",
"group_id": 78
}
```
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
**Group removed:**
```json
@@ -224,13 +262,35 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_destroy",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
- "owner_name": "John Smith",
+ "owner_email": null,
+ "owner_name": null,
"path": "storecloud",
"group_id": 78
}
```
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
+**Group renamed:**
+
+```json
+{
+ "event_name": "group_rename",
+ "created_at": "2017-10-30T15:09:00Z",
+ "updated_at": "2017-11-01T10:23:52Z",
+ "name": "Better Name",
+ "path": "better-name",
+ "full_path": "parent-group/better-name",
+ "group_id": 64,
+ "owner_name": null,
+ "owner_email": null,
+ "old_path": "old-name",
+ "old_full_path": "parent-group/old-name"
+}
+```
+
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
**New Group Member:**
```json
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 5561784ed0b..28308fc905c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -144,6 +144,12 @@ has a `.gitlab-ci.yml` or not:
All you need to do is remove your existing `.gitlab-ci.yml`, and you can even
do that in a branch to test Auto DevOps before committing to `master`.
+NOTE: **Note:**
+If you are a GitLab Administrator, you can enable Auto DevOps instance wide
+in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that,
+all the projects that haven't explicitly set an option will have Auto DevOps
+enabled by default.
+
## Stages of Auto DevOps
The following sections describe the stages of Auto DevOps. Read them carefully
@@ -315,7 +321,7 @@ Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernet
You can override the Helm chart used by bundling up a chart into your project
repo or by specifying a project variable:
-- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml`
+- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml`
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
@@ -517,7 +523,7 @@ Feature.get(:auto_devops_banner_disabled).enable
Or through the HTTP API with an admin access token:
```sh
-curl --data "value=true" --header "PRIVATE-TOKEN: private_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
+curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
```
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
diff --git a/doc/university/README.md b/doc/university/README.md
index 170582bcd0c..55865ac23e8 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab University
GitLab University is the best place to learn about **Version Control with Git and GitLab**.
@@ -51,10 +55,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 1.5. Migrating from other Source Control
-1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
-1. [Migrating from GitHub](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
-1. [Migrating from SVN](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
-1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
+1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/user/project/import/bitbucket.html)
+1. [Migrating from GitHub](https://docs.gitlab.com/ee/user/project/import/github.html)
+1. [Migrating from SVN](https://docs.gitlab.com/ee/user/project/import/svn.html)
+1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/user/project/import/fogbugz.html)
#### 1.6. GitLab Inc.
@@ -76,13 +80,13 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
- Being part of our Great Community and Contributing to GitLab
1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
-1. [GitLab Training Workshops](https://about.gitlab.com/training)
+1. [GitLab Training Workshops](https://docs.gitlab.com/ce/university/training/end-user/)
+1. [GitLab Professional Services](https://about.gitlab.com/services/)
#### 1.8 GitLab Training Material
1. [Git and GitLab Terminology](glossary/README.md)
1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web)
-1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user)
---
diff --git a/doc/university/bookclub/booklist.md b/doc/university/bookclub/booklist.md
index c4229832e9f..26c3851276b 100644
--- a/doc/university/bookclub/booklist.md
+++ b/doc/university/bookclub/booklist.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Books
List of books and resources, that may be worth reading.
diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md
index 022a61f4429..63238685b2b 100644
--- a/doc/university/bookclub/index.md
+++ b/doc/university/bookclub/index.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# The GitLab Book Club
The Book Club is a casual meet-up to read and discuss books we like.
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 9544de41b9a..076fbf6f710 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -1,4 +1,8 @@
-## What is the Glossary
+---
+comments: false
+---
+
+# What is the Glossary
This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab.
Please add any terms that you discover that you think would be useful for others.
@@ -19,6 +23,10 @@ A Microsoft-based [directory service](https://msdn.microsoft.com/en-us/library/b
Building and [delivering software](http://agilemethodology.org/) in phases/parts rather than trying to build everything at once then delivering to the user/client. The latter is known as the WaterFall model.
+### Amazon RDS
+
+External reference: <http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html>
+
### Application Lifecycle Management (ALM)
The entire product lifecycle management process for an application, from requirements management, development, and testing until deployment. GitLab has [advantages](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit#slide=id.g72f2e4906_2_288) over both legacy and modern ALM tools.
@@ -64,7 +72,7 @@ A branch is a parallel version of a repository. This allows you to work on the r
Having your own logo on [your GitLab instance login page](https://docs.gitlab.com/ee/customization/branded_login_page.html) instead of the GitLab logo.
-### Job triggers
+### Job triggers (Build Triggers)
These protect your code base against breaks, for instance when a team is working on the same project. Learn about [setting up](https://docs.gitlab.com/ce/ci/triggers/README.html) job triggers.
### CEPH
@@ -105,15 +113,15 @@ Atlassian's product for collaboration on documents and projects.
### Continuous Delivery
-A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually.
+A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually. [Amazon moves toward continuous delivery](https://www.youtube.com/watch?v=esEFaY0FDKc)
### Continuous Deployment
-A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all. [The difference between Continuous Delivery and Continuous Integration.](https://www.youtube.com/watch?v=igwFj8PPSnw)
### Continuous Integration
-A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day. [Thoughtworks discusses continuous integration.](https://www.thoughtworks.com/continuous-integration)
### Contributor
@@ -123,6 +131,10 @@ Term used for a person contributing to an open source project.
A [natural evolution](https://about.gitlab.com/2016/09/14/gitlab-live-event-recap/) of software development that carries a conversation across functional groups throughout the development process, enabling developers to track the full path of development in a cohesive and intuitive way. ConvDev accelerates the development lifecycle by fostering collaboration and knowledge sharing from idea to production.
+### Cycle Analytics
+
+See <https://gitlab.com/gitlab-org/gitlab-ce/issues/22458>
+
### Cycle Time
The time it takes to move from [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab).
@@ -131,6 +143,10 @@ The time it takes to move from [idea to production](https://about.gitlab.com/201
Atlassian product for High Availability.
+### Dependencies
+
+As in "specify [dependencies](https://gitlab.com/gitlab-org/gitlab-ce/issues/14728) between stages."
+
### Deploy Keys
A [SSH key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html)stored on your server that grants access to a single GitLab repository. This is used by a GitLab runner to clone a project's code so that tests can be run against the checked out code.
@@ -147,15 +163,17 @@ The intersection of software engineering, quality assurance, and technology oper
The difference between two commits, or saved changes. This will also be shown visually after the changes.
-#### Directory
+### Directory
A folder used for storing multiple files.
### Docker Container Registry
-A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of GitLab projects. Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
+A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of [GitLab projects](https://about.gitlab.com/2016/05/23/gitlab-container-registry/). Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
+
+### Dynamic Environment (review apps)
-### Dynamic Environment
+### EC2 Instance
### ElasticSearch
@@ -163,10 +181,26 @@ Elasticsearch is a flexible, scalable and powerful search service. When [enabled
### Emacs
+External reference: <https://www.masteringemacs.org/article/mastering-key-bindings-emacs>
+
+### First Byte
+
+External reference: <https://en.wikipedia.org/wiki/Time_To_First_Byte>
+
+First Byte (sometimes referred to as time to first byte or [TTFB](https://en.wikipedia.org/wiki/Time_To_First_Byte)) measures the time between making a request and receiving the first byte of information in return. As a result, First Byte encompasses everything that is the backend as well as network transit issues. It differs from [_Speed Index_](#speed-index) mostly by frontend related issues which are included in Speed Index such as javascript loading, page rendering, and so on.
+
### Fork
Your [own copy](https://docs.gitlab.com/ce/workflow/forking_workflow.html) of a repository that allows you to make changes to the repository without affecting the original.
+### Funnel, or: TOFU, MOFU, BOFU
+
+External reference: [Blog post](https://www.weidert.com/whole_brain_marketing_blog/bid/113688/ToFu-MoFu-BoFu-Serving-Up-The-Right-Content-for-Lead-Nurturing)
+
+TOFU: top of funnel
+MOFU: middle of funnel
+BOFU: bottom of funnel
+
### Gerrit
A code review [tool](https://www.gerritcodereview.com/) built on top of Git.
@@ -179,6 +213,8 @@ A [git attributes file](https://git-scm.com/docs/gitattributes) is a simple text
[Scripts](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) you can use to trigger actions at certain points.
+Difference between a [webhook](#webhooks) and a git hook: a git hook is local to its repo (usually) while a webhook is not (it can make API or http calls). So for example if you want your linter to fire before you commit, you can set that up with a git hook. If the linter fails, the commit does not go through. A git hook _can_ be configured to go beyond its repo, e.g. by having it make an API call.
+
### GitHost.io
A single-tenant solution that provides GitLab CE or EE as a managed service. GitLab Inc. is responsible for installing, updating, hosting, and backing up customers' own private and secure GitLab instance.
@@ -207,9 +243,20 @@ Our free SaaS for public and private repositories.
Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/gitlab-geo/README.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
+### GitLab High Availability
+
+### GitLab Master Plan
+
+Related blog post: <https://about.gitlab.com/2016/09/13/gitlab-master-plan/>.
+
### GitLab Pages
+
These allow you to [create websites](https://gitlab.com/help/pages/README.md) for your GitLab projects, groups, or user account.
+### GitLab Runner
+
+Related project: <https://gitlab.com/gitlab-org/gitlab-runner>
+
### Gitolite
An [access layer](https://git-scm.com/book/en/v1/Git-on-the-Server-Gitolite) that sits on top of Git. Users are granted access to repos via a simple config file. As an admin, you only need the users' public SSH key and a username.
@@ -222,6 +269,10 @@ A web-based hosting service for projects using Git. It was acquired by GitLab an
An open source programming [language](https://golang.org/).
+### Gogs
+
+External reference: <https://gogs.io/>
+
### GUI/ Git GUI
A portable [graphical interface](https://git-scm.com/docs/git-gui) to Git that allows users to make changes to their repository by making new commits, amending existing ones, creating branches, performing local merges, and fetching/pushing to remote repositories.
@@ -252,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular.
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
### Jira
@@ -286,6 +337,10 @@ GitLab [integrates](https://docs.gitlab.com/ce/administration/auth/ldap.html) wi
Allows you to synchronize the members of a GitLab group with one or more LDAP groups.
+### Lint
+
+Static code analysis for our various file types. For example, we use [scss-lint](https://github.com/brigade/scss-lint) to ensure that a consistent code styling is respected. Similar tools: rubocop / eslint.
+
### Load Balancer
A [device](https://en.wikipedia.org/wiki/Load_balancing_(computing)) that distributes network or application traffic across multiple servers.
@@ -326,6 +381,10 @@ Takes changes from one branch, and [applies them](https://git-scm.com/docs/git-m
[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file.
+#### Merge Request
+
+[Takes changes](https://docs.gitlab.com/ce/gitlab-basics/add-merge-request.html) from one branch, and applies them into another branch.
+
### Meteor
A [platform](https://www.meteor.com) for building javascript apps.
@@ -346,6 +405,14 @@ A type of software license. It lets people do anything with your code with prope
A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi).
+#### Mount
+
+External reference:
+
+As stated on the [wikipedia page](https://en.wikipedia.org/wiki/Mount_(Unix)), "Mounting makes file systems, files, directories, devices and special files available for use and available to the user."
+
+For example, we have NFS servers where the _git files_ reside. In order for a worker node to "see" or "use" the git files, the NFS server needs to be _mounted_ on the worker; that is, the worker needs to know that the NFS server exists and how to connect to it. Think of it as getting a shared drive to show up in your Finder (on Mac) or Explorer (on Windows).
+
### MySQL
A relational [database](http://www.mysql.com/) owned by Oracle. Currently only supported if you are using EE.
@@ -356,7 +423,7 @@ A set of symbols that are used to organize objects of various kinds so that thes
### Nginx
-A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). It can act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
+A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). [It can act]((https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md) as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
### OAuth
@@ -376,7 +443,11 @@ GitLab's [business model](https://about.gitlab.com/2016/07/20/gitlab-is-open-cor
### Open Source Software
-Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
+Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/). Including to providing access to the source code, open source software must comply with a number of criteria, among them free distribution and no discrimination against persons, groups, or fields of endeavor.
+
+#### Open Source Stewardship
+
+[Related blog post](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
### Owner
@@ -426,6 +497,8 @@ A popular DevOps [automation tool](https://puppet.com/product/how-puppet-works).
Git [command](https://git-scm.com/docs/git-push) to send commits from the local repository to the remote repository. Read about [advanced push rules](https://gitlab.com/help/pages/README.md) in GitLab.
+### Raketasks
+
### RE Read Only
Permissions to see a file and its contents, but not change it.
@@ -434,10 +507,24 @@ Permissions to see a file and its contents, but not change it.
In addition to the merge, the [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is a main way to integrate changes from one branch into another.
+### Regression
+
+A regression is something that used to work one way in the last release and then we made a **breaking change** and it no longer works the same way.
+
+_or_
+
+A regression is defined as a change that results in a negative impact on the functionality of an existing feature due to recent changes, i.e. the latest release.
+
+### Remote mirroring
+
### (Git) Repository
A directory where Git [has been initiatlized](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) to start version controlling your files. The history of your work is stored here. A remote repository is not on your machine, but usually online (like on GitLab.com, for instance). The main remote repository is usually called "Origin."
+##### Remote repository
+
+A [repository](https://about.gitlab.com/2015/05/18/simple-words-for-a-gitlab-newbie/) that is not-on-your-machine, so it's anything that is not your computer. Usually, it is online, GitLab.com for instance. The main remote repository is usually called “Originâ€.
+
### Requirements management
Gives your distributed teams a single shared repository to collaborate and share requirements, understand their relationship to tests, and evaluate linked defects. It includes multiple, preconfigured requirement types.
@@ -456,7 +543,7 @@ A route table contains rules (called routes) that determine where network traffi
### Runners
-Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner) you have specified to be run on GitLab CI.
+Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-runner) you have specified to be run on GitLab CI.
### Sidekiq
@@ -470,7 +557,11 @@ Software that is hosted centrally and accessed on-demand (i.e. whenever you want
This term is often used by people when they mean "Version Control."
-## Scrum
+#### SCLAU
+
+Abbreviation for SQO Count [Large And Up](https://about.gitlab.com/handbook/sales/#market-segmentation). This is the number of opportunities in large and strategic organizations passed from marketing to sales.
+
+### Scrum
An Agile [framework](https://www.scrum.org/Resources/What-is-Scrum) designed to typically help complete complex software projects. It's made up of several parts: product requirements backlog, sprint planning, sprint (development), sprint review, and retrospec (analyzing the sprint). The goal is to end up with potentially shippable products.
@@ -480,7 +571,9 @@ The board used to track the status and progress of each of the sprint backlog it
### Shell
-Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git]() and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+
+### Shell command runner
### Single-tenant
@@ -490,6 +583,8 @@ The tenant purchases their own copy of the software and the software can be cust
Real time messaging app for teams that is used internally by GitLab team members. GitLab users can enable [Slack integration](https://docs.gitlab.com/ce/project_services/slack.html) to trigger push, issue, and merge request events among others.
+### Slash commands
+
### Slave Servers
Also known as secondary servers, these help to spread the load over multiple machines. They also provide backups when the master/primary server crashes.
@@ -498,6 +593,10 @@ Also known as secondary servers, these help to spread the load over multiple mac
Program code as typed by a computer programmer (i.e. it has not yet been compiled/translated by the computer to machine language).
+### Speed Index
+
+[Speed Index](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index) is "the average time at which visible parts of the page are displayed".
+
### SSH Key
A unique identifier of a computer. It is used to identify computers without the need for a password (e.g., On GitLab I have [added the ssh key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html) of all my work machines so that the GitLab instance knows that it can accept code pushes and pulls from this trusted machines whose keys are I have added.)
@@ -538,6 +637,16 @@ An open source version control system. Read about [migrating from SVN](https://d
[Represents](https://docs.gitlab.com/ce/api/tags.html) a version of a particular branch at a moment in time.
+### Tenancy
+
+#### Multi-tenant
+
+A [multi-tenant](http://whatis.techtarget.com/definition/multi-tenancy) GitLab instance can have any number of customers - such as companies or groups of users using it. GitLab.com is an example of a multi-tenant GitLab instance.
+
+#### Single-tenant
+
+A [single-tenant](http://searchcloudapplications.techtarget.com/definition/single-tenancy) GitLab instance has only one customer - such as a company - using it. On premise GitLab instances are almost exclusively single-tenant.
+
### Tool Stack
The set of tools used in a process to achieve a common outcome (e.g. set of tools used in Application Lifecycle Management).
@@ -546,9 +655,17 @@ The set of tools used in a process to achieve a common outcome (e.g. set of tool
An open source project management and bug tracking web [application](https://trac.edgewall.org/).
+### True-Up licensing model
+
+### Ubuntu
+
### Untracked files
-New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously.
+New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously. Add them by using the command "git add [file path]"
+
+### Upstream repository vs. GitLab repository
+
+[External conversation](https://news.ycombinator.com/item?id=12487112)
### User
@@ -556,11 +673,11 @@ Anyone interacting with the software.
### Version Control Software (VCS)
-Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present distributed version control systems like Git, Mercurial, Bazaar, and Darcs.
+Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present [distributed version control systems](https://en.wikipedia.org/wiki/Distributed_version_control) like Git, Mercurial, Bazaar, and Darcs. If any server dies, and these systems were collaborating via it, any of the client repositories can be copied back up to the server to restore it.
### Virtual Private Cloud (VPC)
-An on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
+A [VPC](https://docs.gitlab.com/ce/university/glossary/README.html#virtual-private-cloud-vpc) is an on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
### Virtual private server (VPS)
@@ -568,7 +685,7 @@ A [virtual machine](https://en.wikipedia.org/wiki/Virtual_private_server) sold a
### VM Instance
-In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any object. An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
+In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any [object](https://cloud.google.com/compute/docs/instances/). An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
### Waterfall
@@ -582,6 +699,10 @@ A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integr
A [website/system](http://www.wiki.com/) that allows for collaborative editing of its content by the users. In programming, wikis usually contain documentation of how to use the software.
+### Working area
+
+Files that have been modified but are not committed. Check them by using the command "git status".
+
### Working Tree
[Consists of files](http://stackoverflow.com/questions/3689838/difference-between-head-working-tree-index-in-git) that you are currently working on.
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index 6b8f3cd3d1d..54625996dff 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# High Availability on AWS
diff --git a/doc/university/process/README.md b/doc/university/process/README.md
index 04f2d52514f..fdf6224f7f6 100644
--- a/doc/university/process/README.md
+++ b/doc/university/process/README.md
@@ -1,8 +1,12 @@
---
+comments: false
+---
+
+---
title: University | Process
---
-## Suggesting improvements
+# Suggesting improvements
If you would like to teach a class or participate or help in any way please
submit a merge request and assign it to [Job](https://gitlab.com/u/JobV).
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 567dadb3b47..25d5fe351ca 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -1,5 +1,9 @@
+---
+comments: false
+---
-## Support Boot Camp
+
+# Support Boot Camp
**Goal:** Prepare new Service Engineers at GitLab
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index 03c62a81b10..a882bf0eb48 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Training
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
index a7db1f2e069..02a6ad48a38 100644
--- a/doc/university/training/gitlab_flow.md
+++ b/doc/university/training/gitlab_flow.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Flow
- A simplified branching strategy
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
index 03179ff5a77..14f096b130f 100644
--- a/doc/university/training/index.md
+++ b/doc/university/training/index.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Training Material
All GitLab training material is stored in markdown format. Slides are
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index 3ed601625cf..d01634df744 100644
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -1,4 +1,8 @@
-## Additional Resources
+---
+comments: false
+---
+
+# Additional Resources
1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
index e6e4fea9b51..251af99bed7 100644
--- a/doc/university/training/topics/agile_git.md
+++ b/doc/university/training/topics/agile_git.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Agile and Git
----------
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
index a60c4365e0c..2d5ab107fe6 100644
--- a/doc/university/training/topics/bisect.md
+++ b/doc/university/training/topics/bisect.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Bisect
----------
diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md
index af7a70a2818..df23024b6ee 100644
--- a/doc/university/training/topics/cherry_picking.md
+++ b/doc/university/training/topics/cherry_picking.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Cherry Pick
----------
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
index 8149379b36f..b7bec83ed8a 100644
--- a/doc/university/training/topics/env_setup.md
+++ b/doc/university/training/topics/env_setup.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Configure your environment
----------
diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md
index b65457728c0..84a1879cd92 100644
--- a/doc/university/training/topics/explore_gitlab.md
+++ b/doc/university/training/topics/explore_gitlab.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Explore GitLab projects
----------
diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md
index 4b34406ea75..0df5f26dbea 100644
--- a/doc/university/training/topics/feature_branching.md
+++ b/doc/university/training/topics/feature_branching.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Feature branching
----------
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
index ec7bb2631aa..153b45fb4da 100644
--- a/doc/university/training/topics/getting_started.md
+++ b/doc/university/training/topics/getting_started.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Getting Started
----------
diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md
index 9ffb4b9c859..651366e0d49 100644
--- a/doc/university/training/topics/git_add.md
+++ b/doc/university/training/topics/git_add.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Add
----------
diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md
index ca1ff29d93b..7e502d6dad4 100644
--- a/doc/university/training/topics/git_intro.md
+++ b/doc/university/training/topics/git_intro.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git introduction
----------
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
index 32ebceff491..f2709ae3890 100644
--- a/doc/university/training/topics/git_log.md
+++ b/doc/university/training/topics/git_log.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Log
----------
@@ -49,8 +53,8 @@ git log --since=1.month.ago --until=3.weeks.ago
```
cd ~/workspace
-git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git
-cd gitlab-ci-multi-runner
+git clone git@gitlab.com:gitlab-org/gitlab-runner.git
+cd gitlab-runner
git log --author="Travis"
git log --since=1.month.ago --until=3.weeks.ago
git log --since=1.month.ago --until=1.day.ago --author="Travis"
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
index 8e5d3baf959..b8049b5c80e 100644
--- a/doc/university/training/topics/gitlab_flow.md
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Flow
----------
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
index 77807b3e7ef..9a1ce550868 100644
--- a/doc/university/training/topics/merge_conflicts.md
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Merge conflicts
----------
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
index 5b446f02f63..4e8c9de85a1 100644
--- a/doc/university/training/topics/merge_requests.md
+++ b/doc/university/training/topics/merge_requests.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Merge requests
----------
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
index cf647284604..0db1d93d1dc 100644
--- a/doc/university/training/topics/rollback_commits.md
+++ b/doc/university/training/topics/rollback_commits.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Rollback Commits
----------
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
index c1bdda32645..5b27ac12f77 100644
--- a/doc/university/training/topics/stash.md
+++ b/doc/university/training/topics/stash.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Stash
----------
diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md
index 5d869af64c1..b5a892dc17b 100644
--- a/doc/university/training/topics/subtree.md
+++ b/doc/university/training/topics/subtree.md
@@ -1,8 +1,8 @@
-## Subtree
+---
+comments: false
+---
-----------
-
-## Subtree
+# Subtree
* Used when there are nested repositories.
* Not recommended when the amount of dependencies is too large
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index e9607b5a875..ab48d52d3c3 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Tags
----------
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
index 17dbb64b9e6..fc72949ade9 100644
--- a/doc/university/training/topics/unstage.md
+++ b/doc/university/training/topics/unstage.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Unstage
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 9e38df26b6a..90e1d2ba5e8 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Git Workshop
---
diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md
index dc14c779026..af815d26a74 100644
--- a/doc/update/10.0-to-10.1.md
+++ b/doc/update/10.0-to-10.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 10.0 to 10.1
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/10.1-to-10.2.md b/doc/update/10.1-to-10.2.md
new file mode 100644
index 00000000000..9e0d8f79522
--- /dev/null
+++ b/doc/update/10.1-to-10.2.md
@@ -0,0 +1,360 @@
+---
+comments: false
+---
+
+# From 10.1 to 10.2
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+cd ruby-2.3.5
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-2-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-2-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-1-stable:config/gitlab.yml.example origin/10-2-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/10-1-stable:lib/support/nginx/gitlab-ssl origin/10-2-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-1-stable:lib/support/nginx/gitlab origin/10-2-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-1-stable:lib/support/init.d/gitlab.default.example origin/10-2-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (10.0)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md
index 97cd277b424..8f18bd93cea 100644
--- a/doc/update/2.6-to-3.0.md
+++ b/doc/update/2.6-to-3.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 2.6 to 3.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md
index a890aa885d5..6a3c2387683 100644
--- a/doc/update/2.9-to-3.0.md
+++ b/doc/update/2.9-to-3.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 2.9 to 3.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md
index e32508745a2..1f25b8265c9 100644
--- a/doc/update/3.0-to-3.1.md
+++ b/doc/update/3.0-to-3.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 3.0 to 3.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md
index b370464390e..1a53ddeb4bd 100644
--- a/doc/update/3.1-to-4.0.md
+++ b/doc/update/3.1-to-4.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 3.1 to 4.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index 7124424bb60..40a133e796e 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.0 to 4.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md
index 8ed5b333a2e..1fd6c58bda7 100644
--- a/doc/update/4.1-to-4.2.md
+++ b/doc/update/4.1-to-4.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.1 to 4.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index 1ec39218ba8..311664b2bc1 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.2 to 5.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index 9c9950fb2c6..7067ea4c40c 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.0 to 5.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md
index 2aab47d2d7c..4faf5fa549e 100644
--- a/doc/update/5.1-to-5.2.md
+++ b/doc/update/5.1-to-5.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 5.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md
index e80f1b89c63..212343bac3f 100644
--- a/doc/update/5.1-to-5.4.md
+++ b/doc/update/5.1-to-5.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 5.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md
index 1ee175383da..865d38e0ca4 100644
--- a/doc/update/5.1-to-6.0.md
+++ b/doc/update/5.1-to-6.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 6.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index 2ae50510f63..ed4f3ebdd53 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.2 to 5.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index 842e3bb6791..7277250eb32 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.3 to 5.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md
index 44715984f0c..dacdf05cc9c 100644
--- a/doc/update/5.4-to-6.0.md
+++ b/doc/update/5.4-to-6.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.4 to 6.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md
index 0c672abeb05..a3c52a1cfb4 100644
--- a/doc/update/6.0-to-6.1.md
+++ b/doc/update/6.0-to-6.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.0 to 6.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md
index d3760cf0619..36a395bf01e 100644
--- a/doc/update/6.1-to-6.2.md
+++ b/doc/update/6.1-to-6.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.1 to 6.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md
index 91105de2e29..02e87a08b8f 100644
--- a/doc/update/6.2-to-6.3.md
+++ b/doc/update/6.2-to-6.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.2 to 6.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md
index 20b58ed8b25..285ed06bdad 100644
--- a/doc/update/6.3-to-6.4.md
+++ b/doc/update/6.3-to-6.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.3 to 6.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md
index 5ee0f040b5d..e07c98a5ad4 100644
--- a/doc/update/6.4-to-6.5.md
+++ b/doc/update/6.4-to-6.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.4 to 6.5
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md
index fa3712f83ad..3f79b19644e 100644
--- a/doc/update/6.5-to-6.6.md
+++ b/doc/update/6.5-to-6.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.5 to 6.6
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 9c85ed091c5..a0542d20d49 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.6 to 6.7
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md
index 687c1265d9b..acf004577f1 100644
--- a/doc/update/6.7-to-6.8.md
+++ b/doc/update/6.7-to-6.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.7 to 6.8
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md
index 0205b0c896a..3d7b1e5346b 100644
--- a/doc/update/6.8-to-6.9.md
+++ b/doc/update/6.8-to-6.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.8 to 6.9
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 4b6e3989893..27063948028 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.9 to 7.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index 1e39fe47ef9..41d0e78b7d8 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.x or 7.x to 7.14
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index 2e9457aa142..308e8aeb985 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.0 to 7.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md
index e5045b5570f..07f92ac3af6 100644
--- a/doc/update/7.1-to-7.2.md
+++ b/doc/update/7.1-to-7.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.1 to 7.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
diff --git a/doc/update/7.10-to-7.11.md b/doc/update/7.10-to-7.11.md
index 89213ba7178..39eeefc0e32 100644
--- a/doc/update/7.10-to-7.11.md
+++ b/doc/update/7.10-to-7.11.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.10 to 7.11
### 0. Stop server
diff --git a/doc/update/7.11-to-7.12.md b/doc/update/7.11-to-7.12.md
index 3865186918c..530066e5fdb 100644
--- a/doc/update/7.11-to-7.12.md
+++ b/doc/update/7.11-to-7.12.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.11 to 7.12
### 0. Double-check your Git version
diff --git a/doc/update/7.12-to-7.13.md b/doc/update/7.12-to-7.13.md
index 4c8d8f1f741..8f413a2079a 100644
--- a/doc/update/7.12-to-7.13.md
+++ b/doc/update/7.12-to-7.13.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.12 to 7.13
### 0. Double-check your Git version
diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md
index 934898da5a1..a8980662855 100644
--- a/doc/update/7.13-to-7.14.md
+++ b/doc/update/7.13-to-7.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.13 to 7.14
### 0. Double-check your Git version
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 25fa6d93f06..513afccff50 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.14 to 8.0
### 0. Double-check your Git version
diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md
index d3391ddd225..a16f9de54e4 100644
--- a/doc/update/7.2-to-7.3.md
+++ b/doc/update/7.2-to-7.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.2 to 7.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md
index 6d632dc3c8e..734c655f1d1 100644
--- a/doc/update/7.3-to-7.4.md
+++ b/doc/update/7.3-to-7.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.3 to 7.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
diff --git a/doc/update/7.4-to-7.5.md b/doc/update/7.4-to-7.5.md
index ec50706d421..7a3a49ff948 100644
--- a/doc/update/7.4-to-7.5.md
+++ b/doc/update/7.4-to-7.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.4 to 7.5
### 0. Stop server
diff --git a/doc/update/7.5-to-7.6.md b/doc/update/7.5-to-7.6.md
index 331f5de080e..f0dfb177b79 100644
--- a/doc/update/7.5-to-7.6.md
+++ b/doc/update/7.5-to-7.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.5 to 7.6
### 0. Stop server
diff --git a/doc/update/7.6-to-7.7.md b/doc/update/7.6-to-7.7.md
index 918b10fbd95..85de6b0c546 100644
--- a/doc/update/7.6-to-7.7.md
+++ b/doc/update/7.6-to-7.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.6 to 7.7
### 0. Stop server
diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md
index 84e0464a824..7cee5f79a13 100644
--- a/doc/update/7.7-to-7.8.md
+++ b/doc/update/7.7-to-7.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.7 to 7.8
### 0. Stop server
diff --git a/doc/update/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md
index b0dc2ba1dbb..5a8b689dbc1 100644
--- a/doc/update/7.8-to-7.9.md
+++ b/doc/update/7.8-to-7.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.8 to 7.9
### 0. Stop server
diff --git a/doc/update/7.9-to-7.10.md b/doc/update/7.9-to-7.10.md
index 8f7f84b41ba..99df51dbb99 100644
--- a/doc/update/7.9-to-7.10.md
+++ b/doc/update/7.9-to-7.10.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.9 to 7.10
### 0. Stop server
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index 6ee0c0656ee..f612606af68 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.0 to 8.1
**NOTE:** GitLab 8.0 introduced several significant changes related to
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 4c9ff5c5c0a..2d0b19abd74 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.1 to 8.2
**NOTE:** GitLab 8.0 introduced several significant changes related to
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index e538983e603..df3e34f5cc6 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.10 to 8.11
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 604166beb56..9d6a1f42375 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.11 to 8.12
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index d83965131f5..6225dee9802 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.12 to 8.13
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index aaadcec8ac0..d2508e3f980 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.13 to 8.14
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index a68fe3bb605..daf8d0f2ca6 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.14 to 8.15
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md
index 9f8f0f714d4..3668142edd2 100644
--- a/doc/update/8.15-to-8.16.md
+++ b/doc/update/8.15-to-8.16.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.15 to 8.16
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
index 74ffe0bc846..ee2e31c2aec 100644
--- a/doc/update/8.16-to-8.17.md
+++ b/doc/update/8.16-to-8.17.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.16 to 8.17
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index baab217b6b7..2e0c26a9092 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.17 to 9.0
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 4b3c5bf6d64..3a0d647cbfe 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.2 to 8.3
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 8b89455ca87..f5162dd5ff5 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.3 to 8.4
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0eedfaee2db..9e2f98add8d 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.4 to 8.5
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 851056161bb..55d8178c407 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.5 to 8.6
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index 34c727260aa..49db6f2967c 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.6 to 8.7
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 6feeb1919de..ee7ec6f7614 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.7 to 8.8
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index 61cdf8854d4..7508443c30a 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.8 to 8.9
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index 42132f690d8..915e7db819a 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.9 to 8.10
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index 6f1870a1366..f60bd92e236 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.0 to 9.1
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index ce72b313031..2fff6544797 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.1 to 9.2
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md
index 779ced0cf75..1b36cf53f4c 100644
--- a/doc/update/9.2-to-9.3.md
+++ b/doc/update/9.2-to-9.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.2 to 9.3
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md
index 78d8a6c7de5..210b6eb607d 100644
--- a/doc/update/9.3-to-9.4.md
+++ b/doc/update/9.3-to-9.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.3 to 9.4
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md
index a7255142ef5..1bfc1167c36 100644
--- a/doc/update/9.4-to-9.5.md
+++ b/doc/update/9.4-to-9.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.4 to 9.5
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.5-to-10.0.md b/doc/update/9.5-to-10.0.md
index 8581e6511f2..8d1cf0f737b 100644
--- a/doc/update/9.5-to-10.0.md
+++ b/doc/update/9.5-to-10.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.5 to 10.0
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index b2679d1ff22..e1857ce99c6 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Universal update guide for patch versions
## Select Version to Install
diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md
index eb7f14a96d5..746d6bf93e7 100644
--- a/doc/update/upgrader.md
+++ b/doc/update/upgrader.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Upgrader (deprecated)
*DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script.
diff --git a/doc/user/discussions/img/image_resolved_discussion.png b/doc/user/discussions/img/image_resolved_discussion.png
index ed00b5c77fe..ed00b5c77fe 100755..100644
--- a/doc/user/discussions/img/image_resolved_discussion.png
+++ b/doc/user/discussions/img/image_resolved_discussion.png
Binary files differ
diff --git a/doc/user/discussions/img/onion_skin_view.png b/doc/user/discussions/img/onion_skin_view.png
index 91c3b396844..91c3b396844 100755..100644
--- a/doc/user/discussions/img/onion_skin_view.png
+++ b/doc/user/discussions/img/onion_skin_view.png
Binary files differ
diff --git a/doc/user/discussions/img/swipe_view.png b/doc/user/discussions/img/swipe_view.png
index 82d6e52173c..82d6e52173c 100755..100644
--- a/doc/user/discussions/img/swipe_view.png
+++ b/doc/user/discussions/img/swipe_view.png
Binary files differ
diff --git a/doc/user/discussions/img/two_up_view.png b/doc/user/discussions/img/two_up_view.png
index d9e90708e87..d9e90708e87 100755..100644
--- a/doc/user/discussions/img/two_up_view.png
+++ b/doc/user/discussions/img/two_up_view.png
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 454988b9b80..fb61e360996 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -368,6 +368,37 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
>**Note:**
This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
+### Mermaid
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid
+
+It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid].
+
+In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block.
+
+Example:
+
+ ```mermaid
+ graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+ ```
+
+Becomes:
+
+```mermaid
+graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+```
+
+For details see the [Mermaid official page][mermaid].
+
## Standard Markdown
### Headers
@@ -814,6 +845,7 @@ A link starting with a `/` is relative to the wiki root.
[^2]: This is my awesome footnote.
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
+[mermaid]: https://mermaidjs.github.io/ "Mermaid website"
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index c03700a3501..b9532bf897f 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -197,6 +197,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
+| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
@@ -261,5 +262,6 @@ only.
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one.
[^6]: Only if user is a member of the project.
+[^7]: Only if the build was triggered by the user
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 5ebb88bf324..5fcc0501dc1 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -52,7 +52,7 @@ You can edit your account settings by navigating from the up-right corner menu b
From there, you can:
- Update your personal information
-- Manage [private tokens](../../api/README.md#private-tokens), email tokens, [2FA](account/two_factor_authentication.md)
+- Manage [2FA](account/two_factor_authentication.md)
- Change your username and [delete your account](account/delete_account.md)
- Manage applications that can
[use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth)
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index f28c034e74c..9b4fdd65e2f 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -2,17 +2,15 @@
> [Introduced][ce-3749] in GitLab 8.8.
-Personal access tokens are useful if you need access to the [GitLab API][api].
-Instead of using your private token which grants full access to your account,
-personal access tokens could be a better fit because of their
-[granular permissions](#limiting-scopes-of-a-personal-access-token).
+Personal access tokens are the preferred way for third party applications and scripts to
+authenticate with the [GitLab API][api], if using [OAuth2](../../api/oauth2.md) is not practical.
You can also use them to authenticate against Git over HTTP. They are the only
accepted method of authentication when you have
[Two-Factor Authentication (2FA)][2fa] enabled.
Once you have your token, [pass it to the API][usage] using either the
-`private_token` parameter or the `PRIVATE-TOKEN` header.
+`private_token` parameter or the `Private-Token` header.
The expiration of personal access tokens happens on the date you define,
at midnight UTC.
@@ -49,12 +47,14 @@ the following table.
|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). |
| `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. |
| `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). |
+| `sudo` | Allows performing API actions as any user in the system (if the authenticated user is an admin) ([introduced][ce-14838] in GitLab 10.2). |
[2fa]: ../account/two_factor_authentication.md
[api]: ../../api/README.md
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
+[ce-14838]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838
[container registry]: ../project/container_registry.md
[users]: ../../api/users.md
-[usage]: ../../api/README.md#basic-usage
+[usage]: ../../api/README.md#personal-access-tokens
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index f2ad42f21fd..022d6317555 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -55,9 +55,10 @@ You have 6 options here that you can use for your default dashboard view:
The project home page content setting allows you to choose what content you want to
see on a project’s home page.
-You can choose between 2 options:
+You can choose between 3 options:
- Show the files and the readme (default)
+- Show the readme
- Show the project’s activity
[rouge]: http://rouge.jneen.net/ "Rouge website"
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 7d9e771f570..cf0c7c109a8 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -1,14 +1,15 @@
-# Connecting GitLab with GKE
+# Connecting GitLab with a Kubernetes cluster
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1.
CAUTION: **Warning:**
The Cluster integration is currently in **Beta**.
-Connect your project to Google Container Engine (GKE) in a few steps.
-
With a cluster associated to your project, you can use Review Apps, deploy your
-applications, run your pipelines, and much more in an easy way.
+applications, run your pipelines, and much more, in an easy way.
+
+Connect your project to Google Kubernetes Engine (GKE) or your own Kubernetes
+cluster in a few steps.
NOTE: **Note:**
The Cluster integration will eventually supersede the
@@ -30,36 +31,58 @@ prerequisites must be met:
- You must have Master [permissions] in order to be able to access the **Cluster**
page.
-If all of the above requirements are met, you can proceed to add a new cluster.
+If all of the above requirements are met, you can proceed to add a new GKE
+cluster.
## Adding a cluster
NOTE: **Note:**
You need Master [permissions] and above to add a cluster.
+There are two options when adding a new cluster; either use Google Kubernetes
+Engine (GKE) or provide the credentials to your own Kubernetes cluster.
+
To add a new cluster:
-1. Navigate to your project's **CI/CD > Cluster** page.
-1. Connect your Google account if you haven't done already by clicking the
- "Sign-in with Google" button.
-1. Fill in the requested values:
- - **Cluster name** (required) - The name you wish to give the cluster.
- - **GCP project ID** (required) - The ID of the project you created in your GCP
- console that will host the Kubernetes cluster. This must **not** be confused
- with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
- - **Zone** - The zone under which the cluster will be created. Read more about
- [the available zones](https://cloud.google.com/compute/docs/regions-zones/).
- - **Number of nodes** - The number of nodes you wish the cluster to have.
- - **Machine type** - The machine type of the Virtual Machine instance that
- the cluster will be based on. Read more about [the available machine types](https://cloud.google.com/compute/docs/machine-types).
- - **Project namespace** - The unique namespace for this project. By default you
- don't have to fill it in; by leaving it blank, GitLab will create one for you.
-1. Click the **Create cluster** button.
-
-After a few moments your cluster should be created. If something goes wrong,
+1. Navigate to your project's **CI/CD > Cluster** page
+1. If you want to let GitLab create a cluster on GKE for you, go through the
+ following steps, otherwise skip to the next one.
+ 1. Click on **Create with GKE**
+ 1. Connect your Google account if you haven't done already by clicking the
+ **Sign in with Google** button
+ 1. Fill in the requested values:
+ - **Cluster name** (required) - The name you wish to give the cluster.
+ - **GCP project ID** (required) - The ID of the project you created in your GCP
+ console that will host the Kubernetes cluster. This must **not** be confused
+ with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
+ - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/)
+ under which the cluster will be created.
+ - **Number of nodes** - The number of nodes you wish the cluster to have.
+ - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
+ of the Virtual Machine instance that the cluster will be based on.
+ - **Project namespace** - The unique namespace for this project. By default you
+ don't have to fill it in; by leaving it blank, GitLab will create one for you.
+1. If you want to use your own existing Kubernetes cluster, click on
+ **Add an existing cluster** and fill in the details as described in the
+ [Kubernetes integration](../integrations/kubernetes.md) documentation.
+1. Finally, click the **Create cluster** button
+
+After a few moments, your cluster should be created. If something goes wrong,
you will be notified.
-Now, you can proceed to [enable the Cluster integration](#enabling-or-disabling-the-cluster-integration).
+You can now proceed to install some pre-defined applications and then
+enable the Cluster integration.
+
+## Installing applications
+
+GitLab provides a one-click install for various applications which will be
+added directly to your configured cluster. Those applications are needed for
+[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md).
+
+| Application | GitLab version | Description |
+| ----------- | :------------: | ----------- |
+| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
+| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
## Enabling or disabling the Cluster integration
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 2c4dfcff4a6..394aa9209e4 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -27,7 +27,8 @@ to enable it.
1. First, ask your system administrator to enable GitLab Container Registry
following the [administration documentation](../../administration/container_registry.md).
If you are using GitLab.com, this is enabled by default so you can start using
- the Registry immediately.
+ the Registry immediately. Currently there is a soft (10GB) size restriction for
+ registry on GitLab.com, as part of the [repository size limit](repository/index.html#repository-size).
1. Go to your [project's General settings](settings/index.md#sharing-and-permissions)
and enable the **Container Registry** feature on your project. For new
projects this might be enabled by default. For existing projects
diff --git a/doc/user/project/img/label_priority_sort_order.png b/doc/user/project/img/label_priority_sort_order.png
new file mode 100644
index 00000000000..21c7a76a322
--- /dev/null
+++ b/doc/user/project/img/label_priority_sort_order.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png
deleted file mode 100644
index 419e555e709..00000000000
--- a/doc/user/project/img/labels_filter_by_priority.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/priority_sort_order.png b/doc/user/project/img/priority_sort_order.png
new file mode 100644
index 00000000000..c558ec23b0e
--- /dev/null
+++ b/doc/user/project/img/priority_sort_order.png
Binary files differ
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 6423beefc77..72def9d1d1d 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -24,6 +24,8 @@ constrains of a Sidekiq worker.
- the milestones (GitLab 8.7+)
- the labels (GitLab 8.7+)
- the release note descriptions (GitLab 8.12+)
+ - the pull request review comments (GitLab 10.2+)
+ - the regular issue and pull request comments
- References to pull requests and issues are preserved (GitLab 8.7+)
- Repository public access is retained. If a repository is private in GitHub
it will be created as private in GitLab as well.
@@ -43,10 +45,13 @@ the case the namespace is taken, the repository will be imported under the user'
namespace that started the import process.
The importer will also import branches on forks of projects related to open pull
-requests. These branches will be imported with a naming scheume similar to
+requests. These branches will be imported with a naming scheme similar to
GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency
in branches compared to the GitHub Repository.
+For a more technical description and an overview of the architecture you can
+refer to [Working with the GitHub importer][gh-import-dev-docs].
+
## Importing your GitHub repositories
The importer page is visible when you create a new project.
@@ -121,7 +126,29 @@ If you want, you can import all your GitHub projects in one go by hitting
You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
+## Making the import process go faster
+
+For large projects it may take a while to import all data. To reduce the time
+necessary you can increase the number of Sidekiq workers that process the
+following queues:
+
+* `github_importer`
+* `github_importer_advance_stage`
+
+For an optimal experience we recommend having at least 4 Sidekiq processes (each
+running a number of threads equal to the number of CPU cores) that _only_
+process these queues. We also recommend that these processes run on separate
+servers. For 4 servers with 8 cores this means you can import up to 32 objects
+(e.g. issues) in parallel.
+
+Reducing the time spent in cloning a repository can be done by increasing
+network throughput, CPU capacity, and disk performance (e.g. by using high
+performance SSDs) of the disks that store the Git repositories (for your GitLab
+instance). Increasing the number of Sidekiq workers will _not_ reduce the time
+spent cloning repositories.
+
[gh-import]: ../../../integration/github.md "GitHub integration"
[gh-rake]: ../../../administration/raketasks/github_import.md "GitHub rake task"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
+[gh-import-dev-docs]: ../../../development/github_importer.md "Working with the GitHub importer"
diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md
new file mode 100644
index 00000000000..757522c2ae3
--- /dev/null
+++ b/doc/user/project/integrations/custom_issue_tracker.md
@@ -0,0 +1,20 @@
+# Custom Issue Tracker Service
+
+To enable the Custom Issue Tracker integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **Customer Issue Tracker** service, and fill in the required details on the page as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `title` | A title for the issue tracker (to differentiate between instances, for example) |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | Currently unused. Will be changed in a future release. |
+| `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. |
+| `new_issue_url` | Currently unused. Will be changed in a future release. |
+
+
+## Referencing issues
+
+Issues are referenced with `#<ID>`, where `<ID>` is a number (example `#143`).
+So with the example above, `#143` would refer to `https://customissuetracker.com/project-name/143`. \ No newline at end of file
diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png
index 917068d9398..803678db6b6 100644
--- a/doc/user/project/integrations/img/webhook_logs.png
+++ b/doc/user/project/integrations/img/webhook_logs.png
Binary files differ
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 51989ccaaea..a0405161495 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -43,6 +43,7 @@ Click on the service links to see further configuration instructions and details
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
+| Packagist | Update your project on Packagist, the main Composer repository |
| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications |
| [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab |
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 518683965e8..a6673fa2a00 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -13,8 +13,8 @@ integration services must be enabled.
| Name | Query |
| ---- | ----- |
-| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 |
-| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 |
+| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 |
+| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 |
## Configuring Prometheus to monitor for Kubernetes node metrics
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 7abc600a680..5896f8f72a0 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -76,6 +76,7 @@ X-Gitlab-Event: Push Hook
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 15,
"project":{
+ "id": 15,
"name":"Diaspora",
"description":"",
"web_url":"http://example.com/mike/diaspora",
@@ -156,6 +157,7 @@ X-Gitlab-Event: Tag Push Hook
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 1,
"project":{
+ "id": 1,
"name":"Example",
"description":"",
"web_url":"http://example.com/jsmith/example",
@@ -206,6 +208,7 @@ X-Gitlab-Event: Issue Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project": {
+ "id": 1,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -335,6 +338,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -414,6 +418,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -540,6 +545,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -618,6 +624,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -692,6 +699,7 @@ X-Gitlab-Event: Merge Request Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project": {
+ "id": 1,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -848,6 +856,7 @@ X-Gitlab-Event: Wiki Page Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
},
"project": {
+ "id": 1,
"name": "awesome-project",
"description": "This is awesome",
"web_url": "http://example.com/root/awesome-project",
@@ -919,6 +928,7 @@ X-Gitlab-Event: Pipeline Hook
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"project":{
+ "id": 1,
"name": "Gitlab Test",
"description": "Atque in sunt eos similique dolores voluptatem.",
"web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
@@ -1130,6 +1140,18 @@ From this page, you can repeat delivery with the same data by clicking `Resend R
>**Note:** If URL or secret token of the webhook were updated, data will be delivered to the new address.
+### Receiving duplicate or multiple web hook requests triggered by one event
+
+When GitLab sends a webhook it expects a response in 10 seconds (set default value). If it does not receive one, it'll retry the webhook.
+If the endpoint doesn't send its HTTP response within those 10 seconds, GitLab may decide the hook failed and retry it.
+
+If you are receiving multiple requests, you can try increasing the default value to wait for the HTTP response after sending the webhook
+by uncommenting or adding the following setting to your `/etc/gitlab/gitlab.rb`:
+
+```
+gitlab_rails['webhook_timeout'] = 10
+```
+
## Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 96a5a23ee13..8c2690ec3b2 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -166,12 +166,26 @@ board itself.
![Remove issue from list](img/issue_boards_remove_issue.png)
-## Re-ordering an issue in a list
-
-> Introduced in GitLab 9.0.
-
-Issues can be re-ordered inside of lists. This is as simple as dragging and dropping
-an issue into the order you want.
+## Issue ordering in a list
+
+When visiting a board, issues appear ordered in any list. You are able to change
+that order simply by dragging and dropping the issues. The changed order will be saved
+to the system so that anybody who visits the same board later will see the reordering,
+with some exceptions.
+
+The first time a given issue appears in any board (i.e. the first time a user
+loads a board containing that issue), it will be ordered with
+respect to other issues in that list according to [Priority order][label-priority].
+At that point, that issue will be assigned a relative order value by the system
+representing its relative order with respect to the other issues in the list. Any time
+you drag-and-drop reorder that issue, its relative order value will change accordingly.
+Also, any time that issue appears in any board when it is loaded by a user,
+the updated relative order value will be used for the ordering. (It's only the first
+time an issue appears that it takes from the Priority order mentioned above.) This means that
+if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
+a given board inside your GitLab instance, any time those two issues are subsequently
+loaded in any board in the same instance (could be a different project board or a different group board, for example),
+that ordering will be maintained.
## Filtering issues
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 21a2e1213ec..d7eb4bca89c 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -77,15 +77,32 @@ having their priority set to null.
![Prioritize labels](img/labels_prioritize.png)
-Now that you have labels prioritized, you can use the 'Priority' and 'Label
-priority' filters in the issues or merge requests tracker.
+Now that you have labels prioritized, you can use the 'Label priority' and 'Priority'
+sort orders in the issues or merge requests tracker.
-The 'Label priority' filter puts issues with the highest priority label on top.
+In the following, everything applies to both issues and merge requests, but we'll
+refer to just issues for brevity.
-The 'Priority' filter sorts issues by their soonest milestone due date, then by
-label priority.
+The 'Label priority' sort order positions issues with higher priority labels
+toward the top, and issues with lower priority labels toward the bottom. A non-prioritized
+label is considered to have the lowest priority. For a given issue, we _only_ consider the
+highest priority label assigned to it in the comparison. ([We are discussing](https://gitlab.com/gitlab-org/gitlab-ce/issues/18554)
+including all the labels in a given issue for this comparison.) Given two issues
+are equal according to this sort comparison, their relative order is equal, and
+therefore it's not guaranteed that one will be always above the other.
+
+![Label priority sort order](img/label_priority_sort_order.png)
+
+The 'Priority' sort order comparison first considers an issue's milestone's due date,
+(if the issue is assigned a milestone and the milestone's due date exists), and then
+secondarily considers the label priority comparison above. Sooner due dates results
+a higher sort order. If an issue doesn't have a milestone due date, it is equivalent to
+being assigned to a milestone that has a due date in the infinite future. Given two issues
+are equal according to this two-stage sort comparison, their relative order is equal, and
+therefore it's not guaranteed that one will be always above the other.
+
+![Priority sort order](img/priority_sort_order.png)
-![Filter labels by priority](img/labels_filter_by_priority.png)
## Subscribe to labels
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index b8dd96087f1..43713855e26 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -21,7 +21,7 @@ want to add.
---
-Select the user and the [permission level](../../user/permissions.md)
+Select the user and the [permission level](../../permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 876b98a4dc5..83adbd8cce2 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -29,7 +29,8 @@ In addition to that you will be able to filter issues or merge requests by group
## Milestone promotion
-You will be able to promote a project milestone to a group milestone [in the future](https://gitlab.com/gitlab-org/gitlab-ce/issues/35833).
+Project milestones can be promoted to group milestones if its project belongs to a group. When a milestone is promoted all other milestones across the group projects with the same title will be merged into it, which means all milestone's children like issues, merge requests and boards will be moved into the new promoted milestone.
+The promote button can be found in the milestone view or milestones list.
## Special milestone filters
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 271adee7da1..17dcd152363 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -230,7 +230,7 @@ test:
- docker run $CI_REGISTRY/group/other-project:latest
```
-[job permissions]: ../permissions.md#jobs-permissions
+[job permissions]: ../permissions.md#job-permissions
[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
[ext]: ../permissions.md#external-users
[gitsub]: ../../ci/git_submodules.md
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index 453e10184f0..1e19f422d94 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com,
it will automatically be accessible under a
-[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com).
+[subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlab-com).
The `namespace` is defined by your username on GitLab.com,
or the group name you created this project under.
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 9ef6f9185c9..f9a268fb789 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -52,7 +52,8 @@ directly in the job artifacts browser without the need to download them.
>**Note:**
With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed
-directly in a new tab without the need to download them.
+directly in a new tab without the need to download them when
+[GitLab Pages](../../../administration/pages/index.md) is enabled
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
@@ -69,7 +70,8 @@ browse inside them.
Below you can see how browsing looks like. In this case we have browsed inside
the archive and at this point there is one directory, a couple files, and
-one HTML file that you can view directly online (opens in a new tab).
+one HTML file that you can view directly online when
+[GitLab Pages](../../../administration/pages/index.md) is enabled (opens in a new tab).
![Job artifacts browser](img/job_artifacts_browser.png)
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 9ad15a12c3c..eac706be3a7 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -44,7 +44,7 @@ GitLab CI so that they can be used in your `.gitlab-ci.yml` file.
To configure that a job can be executed only when the pipeline has been
scheduled (or the opposite), you can use
-[only and except](../../../ci/yaml/README.md#only-and-except) configuration keywords.
+[only and except](../../../ci/yaml/README.md#only-and-except-simplified) configuration keywords.
```
job:on-schedule:
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 56f58fd755a..daa5463d680 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -115,10 +115,12 @@ pages.
Depending on the status of your job, a badge can have the following values:
+- pending
- running
-- success
+- passed
- failed
- skipped
+- canceled
- unknown
You can access a pipeline status badge image using the following link:
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 6b2aba47f54..272f7807ac0 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Workflow
- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 9d466ae1971..23b67310d25 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -1,6 +1,6 @@
![GitLab Flow](gitlab_flow.png)
-## Introduction
+# Introduction to GitLab Flow
Version management with git makes branching and merging much easier than older versioning systems such as SVN.
This allows a wide variety of branching strategies and workflows.
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
index 7e339443b75..f8eb0f01de8 100644
--- a/features/steps/profile/notifications.rb
+++ b/features/steps/profile/notifications.rb
@@ -11,7 +11,7 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
end
step 'I select Mention setting from dropdown' do
- first(:link, "On mention").trigger('click')
+ first(:link, "On mention").click
end
step 'I should see Notification saved message' do
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index ccaf3237815..c3ae33d2aa9 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -40,6 +40,7 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step 'I submit new branch form with invalid name' do
fill_in 'branch_name', with: '1.0 stable'
+ page.find("body").click # defocus the branch_name input
select_branch('master')
click_button 'Create branch'
end
@@ -70,17 +71,16 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step "I click branch 'improve/awesome' delete link" do
page.within '.js-branch-improve\/awesome' do
- find('.btn-remove').click
- sleep 0.05
+ accept_alert { find('.btn-remove').click }
end
end
step "I should not see branch 'improve/awesome'" do
- expect(page.all(visible: true)).not_to have_content 'improve/awesome'
+ expect(page).to have_css('.js-branch-improve\\/awesome', visible: :hidden)
end
def select_branch(branch_name)
- click_button 'master'
+ find('.git-revision-dropdown-toggle').click
page.within '#new-branch-form .dropdown-menu' do
click_link branch_name
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 318e054e978..c623a516c47 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I should see additional file lines' do
- page.within @diff.parent do
+ page.within @diff.query_scope do
expect(first('.new_line').text).not_to have_content "..."
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 2c3ef2efd52..3843374678c 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -20,11 +20,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I should see that I am subscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
+ wait_for_requests
+ expect(find('.js-issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
+ wait_for_requests
+ expect(find('.js-issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click link "Closed"' do
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index dac18c537ac..196e0fff63a 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -16,7 +16,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do
page.within '.labels' do
page.all('.remove-row').each do
- first('.remove-row').click
+ accept_confirm { first('.remove-row').click }
end
end
end
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index 16a2d4a6f93..33a24e8913a 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -3,6 +3,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
include SharedProject
include SharedPaths
include SharedMarkdown
+ include CapybaraHelpers
step 'I should see milestone "v2.2"' do
milestone = @project.milestones.find_by(title: "v2.2")
@@ -65,7 +66,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I click link to remove milestone' do
- click_link 'Delete'
+ confirm_modal_if_present { click_link 'Delete' }
end
step 'I should see no milestones' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index c872bd6f861..aa32528a7ca 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -215,7 +215,7 @@ module SharedDiffNote
end
step 'I click side-by-side diff button' do
- find('#parallel-diff-btn').trigger('click')
+ find('#parallel-diff-btn').click
end
step 'I see side-by-side diff button' do
@@ -227,12 +227,11 @@ module SharedDiffNote
end
def click_diff_line(code)
- find(".line_holder[id='#{code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{code}'] button").trigger 'click'
+ find(".line_holder[id='#{code}'] button").click
end
def click_parallel_diff_line(code, line_type)
- find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').trigger 'mouseover'
- find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
+ find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover
+ find(".line_holder.parallel button[data-line-code='#{code}']").click
end
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 0cd7b506a95..95f0cd2156e 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -14,7 +14,7 @@ module SharedNote
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
- find(".js-note-delete").click
+ accept_confirm { find(".js-note-delete").click }
end
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index f4691647d4b..3c4db8b9601 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -1,22 +1,21 @@
-require 'capybara/poltergeist'
require 'capybara-screenshot/spinach'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :poltergeist
-Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(
- app,
- js_errors: true,
- timeout: timeout,
- window_size: [1366, 768],
- url_whitelist: %w[localhost 127.0.0.1],
- url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
- phantomjs_options: [
- '--load-images=yes'
- ]
+Capybara.javascript_driver = :chrome
+Capybara.register_driver :chrome do |app|
+ extra_args = []
+ extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ chromeOptions: {
+ 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ }
)
+
+ Capybara::Selenium::Driver
+ .new(app, browser: :chrome, desired_capabilities: capabilities)
end
Capybara.default_max_wait_time = timeout
@@ -24,6 +23,10 @@ Capybara.ignore_hidden_elements = false
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
+# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
+Capybara::Screenshot.register_driver(:chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+end
Spinach.hooks.before_run do
TestEnv.eager_load_driver_server
diff --git a/features/support/capybara_helpers.rb b/features/support/capybara_helpers.rb
new file mode 100644
index 00000000000..647f8d087c3
--- /dev/null
+++ b/features/support/capybara_helpers.rb
@@ -0,0 +1,10 @@
+module CapybaraHelpers
+ def confirm_modal_if_present
+ if Capybara.current_driver == Capybara.javascript_driver
+ accept_confirm { yield }
+ return
+ end
+
+ yield
+ end
+end
diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json
index e2f47db0de2..415dd5a54e0 100644
--- a/fixtures/emojis/aliases.json
+++ b/fixtures/emojis/aliases.json
@@ -339,6 +339,7 @@
"baguette_bread":"french_bread",
"anguished":"frowning",
"white_frowning_face":"frowning2",
+ "rainbow_flag":"gay_pride_flag",
"goal_net":"goal",
"hammer_and_pick":"hammer_pick",
"raised_hand_with_fingers_splayed":"hand_splayed",
@@ -488,6 +489,7 @@
"slightly_smiling_face":"slight_smile",
"sneeze":"sneezing_face",
"speaking_head_in_silhouette":"speaking_head",
+ "left_speech_bubble":"speech_left",
"sleuth_or_spy":"spy",
"sleuth_or_spy_tone1":"spy_tone1",
"sleuth_or_spy_tone2":"spy_tone2",
@@ -537,4 +539,4 @@
"wrestling_tone4":"wrestlers_tone4",
"wrestling_tone5":"wrestlers_tone5",
"zipper_mouth_face":"zipper_mouth"
-}
+} \ No newline at end of file
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
index 589cff165f3..3c8f6426f93 100644
--- a/fixtures/emojis/digests.json
+++ b/fixtures/emojis/digests.json
@@ -1478,7 +1478,7 @@
},
"cartwheel_tone4": {
"category": "activity",
- "moji": "🤸ðŸ¾,",
+ "moji": "🤸ðŸ¾",
"description": "person doing cartwheel tone 4",
"unicodeVersion": "9.0",
"digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
@@ -5375,6 +5375,13 @@
"unicodeVersion": "6.0",
"digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1"
},
+ "gay_pride_flag": {
+ "category": "flags",
+ "moji": "ðŸ³ðŸŒˆ",
+ "description": "gay_pride_flag",
+ "unicodeVersion": "6.0",
+ "digest": "924e668c559db61b7f4724a661223081c2fc60d55169f3fe1ad6156934d1d37f"
+ },
"gemini": {
"category": "symbols",
"moji": "♊",
@@ -7578,7 +7585,7 @@
"moji": "🤶",
"description": "mother christmas",
"unicodeVersion": "9.0",
- "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
+ "digest": "357d769371305a8584f46d6087a962d647b6af22fab363a44702f38ab7814091"
},
"mrs_claus_tone1": {
"category": "people",
@@ -10709,6 +10716,13 @@
"unicodeVersion": "6.0",
"digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
},
+ "speech_left": {
+ "category": "symbols",
+ "moji": "🗨",
+ "description": "left speech bubble",
+ "unicodeVersion": "7.0",
+ "digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
+ },
"speedboat": {
"category": "travel",
"moji": "🚤",
diff --git a/fixtures/emojis/generate_aliases.rb b/fixtures/emojis/generate_aliases.rb
deleted file mode 100755
index 8838fb9a3af..00000000000
--- a/fixtures/emojis/generate_aliases.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'json'
-
-aliases = {}
-
-index_file = File.expand_path("./index.json")
-index = JSON.parse(File.read(index_file))
-
-index.each_pair do |key, data|
- data['aliases'].each do |a|
- a.tr!(':', '')
-
- aliases[a] = key
- end
-end
-
-puts JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: '')
diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json
index 2a990913b9c..f55571d31fa 100644
--- a/fixtures/emojis/index.json
+++ b/fixtures/emojis/index.json
@@ -4023,7 +4023,7 @@
],
"aliases_ascii": [],
"keywords": [],
- "moji": "🤸ðŸ¾,"
+ "moji": "🤸ðŸ¾"
},
"cartwheel_tone5": {
"unicode": "1F938-1F3FF",
@@ -14475,6 +14475,19 @@
],
"moji": "💎"
},
+ "gay_pride_flag": {
+ "unicode": "1F3F3-1F308",
+ "unicode_alternates": [],
+ "name": "gay_pride_flag",
+ "shortname": ":gay_pride_flag:",
+ "category": "extras",
+ "aliases": [
+ ":rainbow_flag:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "ðŸ³ðŸŒˆ"
+ },
"gemini": {
"unicode": "264A",
"unicode_alternates": [
@@ -16830,7 +16843,6 @@
"0:-)",
"0:)",
"0;^)",
- "O:-)",
"O:)",
"O;-)",
"O=)",
@@ -28506,6 +28518,21 @@
],
"moji": "💬"
},
+ "speech_left": {
+ "unicode": "1F5E8",
+ "unicode_alternates": [
+ "1F5E8-FE0F"
+ ],
+ "name": "left speech bubble",
+ "shortname": ":speech_left:",
+ "category": "symbols",
+ "aliases": [
+ ":left_speech_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🗨"
+ },
"speedboat": {
"unicode": "1F6A4",
"unicode_alternates": [],
@@ -33477,4 +33504,4 @@
],
"moji": "💤"
}
-}
+} \ No newline at end of file
diff --git a/lib/additional_email_headers_interceptor.rb b/lib/additional_email_headers_interceptor.rb
index 2358fa6bbfd..3cb1694b9f1 100644
--- a/lib/additional_email_headers_interceptor.rb
+++ b/lib/additional_email_headers_interceptor.rb
@@ -1,8 +1,6 @@
class AdditionalEmailHeadersInterceptor
def self.delivering_email(message)
- message.headers(
- 'Auto-Submitted' => 'auto-generated',
- 'X-Auto-Response-Suppress' => 'All'
- )
+ message.header['Auto-Submitted'] ||= 'auto-generated'
+ message.header['X-Auto-Response-Suppress'] ||= 'All'
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7db18e25a5f..8094597d238 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -61,7 +61,10 @@ module API
mount ::API::V3::Variables
end
- before { header['X-Frame-Options'] = 'SAMEORIGIN' }
+ before do
+ header['X-Frame-Options'] = 'SAMEORIGIN'
+ header['X-Content-Type-Options'] = 'nosniff'
+ end
# The locale is set to the current user's locale when `current_user` is loaded
after { Gitlab::I18n.use_default_locale }
@@ -142,7 +145,6 @@ module API
mount ::API::Runner
mount ::API::Runners
mount ::API::Services
- mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 87b9db66efd..9aeebc34525 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,9 +6,6 @@ module API
module APIGuard
extend ActiveSupport::Concern
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze
- PRIVATE_TOKEN_PARAM = :private_token
-
included do |base|
# OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
@@ -42,123 +39,19 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
- def find_current_user
- user =
- find_user_from_private_token ||
- find_user_from_oauth_token ||
- find_user_from_warden
+ include Gitlab::Auth::UserAuthFinders
- return nil unless user
+ def find_current_user!
+ user = find_user_from_access_token || find_user_from_warden
+ return unless user
- raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
- def private_token
- params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
- end
-
private
- def find_user_from_private_token
- token_string = private_token.to_s
- return nil unless token_string.present?
-
- user =
- find_user_by_authentication_token(token_string) ||
- find_user_by_personal_access_token(token_string)
-
- raise UnauthorizedError unless user
-
- user
- end
-
- # Invokes the doorkeeper guard.
- #
- # If token is presented and valid, then it sets @current_user.
- #
- # If the token does not have sufficient scopes to cover the requred scopes,
- # then it raises InsufficientScopeError.
- #
- # If the token is expired, then it raises ExpiredError.
- #
- # If the token is revoked, then it raises RevokedError.
- #
- # If the token is not found (nil), then it returns nil
- #
- # Arguments:
- #
- # scopes: (optional) scopes required for this guard.
- # Defaults to empty array.
- #
- def find_user_from_oauth_token
- access_token = find_oauth_access_token
- return unless access_token
-
- find_user_by_access_token(access_token)
- end
-
- def find_user_by_authentication_token(token_string)
- User.find_by_authentication_token(token_string)
- end
-
- def find_user_by_personal_access_token(token_string)
- access_token = PersonalAccessToken.find_by_token(token_string)
- return unless access_token
-
- find_user_by_access_token(access_token)
- end
-
- # Check the Rails session for valid authentication details
- def find_user_from_warden
- warden.try(:authenticate) if verified_request?
- end
-
- def warden
- env['warden']
- end
-
- # Check if the request is GET/HEAD, or if CSRF token is valid.
- def verified_request?
- Gitlab::RequestForgeryProtection.verified?(env)
- end
-
- def find_oauth_access_token
- return @oauth_access_token if defined?(@oauth_access_token)
-
- token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
- return @oauth_access_token = nil unless token
-
- @oauth_access_token = OauthAccessToken.by_token(token)
- raise UnauthorizedError unless @oauth_access_token
-
- @oauth_access_token.revoke_previous_refresh_token!
- @oauth_access_token
- end
-
- def find_user_by_access_token(access_token)
- scopes = scopes_registered_for_endpoint
-
- case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
- when AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
-
- when AccessTokenValidationService::EXPIRED
- raise ExpiredError
-
- when AccessTokenValidationService::REVOKED
- raise RevokedError
-
- when AccessTokenValidationService::VALID
- access_token.user
- end
- end
-
- def doorkeeper_request
- @doorkeeper_request ||= ActionDispatch::Request.new(env)
- end
-
# An array of scopes that were registered (using `allow_access_with_scope`)
# for the current endpoint class. It also returns scopes registered on
# `API::API`, since these are meant to apply to all API routes.
@@ -181,8 +74,11 @@ module API
private
def install_error_responders(base)
- error_classes = [MissingTokenError, TokenNotFoundError,
- ExpiredError, RevokedError, InsufficientScopeError]
+ error_classes = [Gitlab::Auth::MissingTokenError,
+ Gitlab::Auth::TokenNotFoundError,
+ Gitlab::Auth::ExpiredError,
+ Gitlab::Auth::RevokedError,
+ Gitlab::Auth::InsufficientScopeError]
base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -191,25 +87,25 @@ module API
proc do |e|
response =
case e
- when MissingTokenError
+ when Gitlab::Auth::MissingTokenError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
- when TokenNotFoundError
+ when Gitlab::Auth::TokenNotFoundError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Bad Access Token.")
- when ExpiredError
+ when Gitlab::Auth::ExpiredError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token is expired. You can either do re-authorization or token refresh.")
- when RevokedError
+ when Gitlab::Auth::RevokedError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token was revoked. You have to re-authorize from the user.")
- when InsufficientScopeError
+ when Gitlab::Auth::InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
@@ -222,22 +118,5 @@ module API
end
end
end
-
- #
- # Exceptions
- #
-
- MissingTokenError = Class.new(StandardError)
- TokenNotFoundError = Class.new(StandardError)
- ExpiredError = Class.new(StandardError)
- RevokedError = Class.new(StandardError)
- UnauthorizedError = Class.new(StandardError)
-
- class InsufficientScopeError < StandardError
- attr_reader :scopes
- def initialize(scopes)
- @scopes = scopes
- end
- end
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 19152c9f395..cdef1b546a9 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -29,12 +29,11 @@ module API
use :pagination
end
get ':id/repository/branches' do
- branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
+ repository = user_project.repository
+ branches = ::Kaminari.paginate_array(repository.branches.sort_by(&:name))
+ merged_branch_names = repository.merged_branch_names(branches.map(&:name))
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- present paginate(branches), with: Entities::Branch, project: user_project
- end
+ present paginate(branches), with: Entities::Branch, project: user_project, merged_branch_names: merged_branch_names
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 2685dc27252..38e05074353 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -117,7 +117,7 @@ module API
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- notes = user_project.notes.where(commit_id: commit.id).order(:created_at)
+ notes = commit.notes.order(:created_at)
present paginate(notes), with: Entities::CommitNote
end
@@ -180,10 +180,12 @@ module API
if params[:path]
commit.raw_diffs(limits: false).each do |diff|
next unless diff.new_path == params[:path]
+
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
+
break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index efe874b2e6b..16ae99b5c6c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -57,10 +57,6 @@ module API
expose :admin?, as: :is_admin
end
- class UserWithPrivateDetails < UserWithAdmin
- expose :private_token
- end
-
class Email < Grape::Entity
expose :id, :email
end
@@ -246,10 +242,7 @@ module API
end
expose :merged do |repo_branch, options|
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- options[:project].repository.merged_to_root_ref?(repo_branch.name)
- end
+ options[:project].repository.merged_to_root_ref?(repo_branch, options[:merged_branch_names])
end
expose :protected do |repo_branch, options|
@@ -482,6 +475,10 @@ module API
expose :subscribed do |merge_request, options|
merge_request.subscribed?(options[:current_user], options[:project])
end
+
+ expose :changes_count do |merge_request, _options|
+ merge_request.merge_request_diff.real_size
+ end
end
class MergeRequestChanges < MergeRequest
@@ -826,6 +823,7 @@ module API
class Job < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
+ expose :duration
expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: Commit
@@ -1044,6 +1042,11 @@ module API
expose :value
end
+ class PagesDomainCertificateExpiration < Grape::Entity
+ expose :expired?, as: :expired
+ expose :expiration
+ end
+
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
@@ -1051,12 +1054,23 @@ module API
expose :certificate_text
end
+ class PagesDomainBasic < Grape::Entity
+ expose :domain
+ expose :url
+ expose :certificate,
+ as: :certificate_expiration,
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: PagesDomainCertificateExpiration do |pages_domain|
+ pages_domain
+ end
+ end
+
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :certificate,
- if: ->(pages_domain, _) { pages_domain.certificate? },
- using: PagesDomainCertificate do |pages_domain|
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: PagesDomainCertificate do |pages_domain|
pages_domain
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index e817dcbbc4b..bcf2e6dae1d 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -25,22 +25,7 @@ module API
optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end
- def present_groups(groups, options = {})
- options = options.reverse_merge(
- with: Entities::Group,
- current_user: current_user
- )
-
- groups = groups.with_statistics if options[:statistics]
- present paginate(groups), options
- end
- end
-
- resource :groups do
- desc 'Get a groups list' do
- success Entities::Group
- end
- params do
+ params :group_list_params do
use :statistics_params
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
@@ -50,14 +35,47 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
- get do
- find_params = { all_available: params[:all_available], owned: params[:owned] }
+
+ def find_groups(params)
+ find_params = {
+ all_available: params[:all_available],
+ custom_attributes: params[:custom_attributes],
+ owned: params[:owned]
+ }
+ find_params[:parent] = find_group!(params[:id]) if params[:id]
+
groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
- present_groups groups, statistics: params[:statistics] && current_user.admin?
+ groups
+ end
+
+ def present_groups(params, groups)
+ options = {
+ with: Entities::Group,
+ current_user: current_user,
+ statistics: params[:statistics] && current_user.admin?
+ }
+
+ groups = groups.with_statistics if options[:statistics]
+ present paginate(groups), options
+ end
+ end
+
+ resource :groups do
+ include CustomAttributesEndpoints
+
+ desc 'Get a groups list' do
+ success Entities::Group
+ end
+ params do
+ use :group_list_params
+ end
+ get do
+ groups = find_groups(params)
+ present_groups params, groups
end
desc 'Create a group. Available only for users who can create groups.' do
@@ -159,6 +177,17 @@ module API
present paginate(projects), with: entity, current_user: current_user
end
+ desc 'Get a list of subgroups in this group.' do
+ success Entities::Group
+ end
+ params do
+ use :group_list_params
+ end
+ get ":id/subgroups" do
+ groups = find_groups(params)
+ present_groups params, groups
+ end
+
desc 'Transfer a project to the group namespace. Available only for admin.' do
success Entities::GroupDetail
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 7a2ec865860..b26c61ab8da 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -41,6 +41,8 @@ module API
sudo!
+ validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
+
@current_user
end
@@ -153,6 +155,11 @@ module API
end
end
+ def authenticated_with_full_private_access!
+ authenticate!
+ forbidden! unless current_user.full_private_access?
+ end
+
def authenticated_as_admin!
authenticate!
forbidden! unless current_user.admin?
@@ -188,6 +195,10 @@ module API
not_found! unless user_project.pages_available?
end
+ def require_pages_config_enabled!
+ not_found! unless Gitlab.config.pages.enabled
+ end
+
def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
@@ -326,6 +337,7 @@ module API
finder_params[:archived] = params[:archived]
finder_params[:search] = params[:search] if params[:search]
finder_params[:user] = params.delete(:user) if params[:user]
+ finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params
end
@@ -385,32 +397,31 @@ module API
return @initial_current_user if defined?(@initial_current_user)
begin
- @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
- rescue APIGuard::UnauthorizedError
+ @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
+ rescue Gitlab::Auth::UnauthorizedError
unauthorized!
end
end
def sudo!
return unless sudo_identifier
- return unless initial_current_user
+
+ unauthorized! unless initial_current_user
unless initial_current_user.admin?
forbidden!('Must be admin to use sudo')
end
- # Only private tokens should be used for the SUDO feature
- unless private_token == initial_current_user.private_token
- forbidden!('Private token must be specified in order to use sudo')
+ unless access_token
+ forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
end
+ validate_access_token!(scopes: [:sudo])
+
sudoed_user = find_user(sudo_identifier)
+ not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
- if sudoed_user
- @current_user = sudoed_user
- else
- not_found!("No user id or username for: #{sudo_identifier}")
- end
+ @current_user = sudoed_user
end
def sudo_identifier
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
index 0a8f3073a50..dd4f6c41131 100644
--- a/lib/api/helpers/custom_validators.rb
+++ b/lib/api/helpers/custom_validators.rb
@@ -4,6 +4,7 @@ module API
class Absence < Grape::Validations::Base
def validate_param!(attr_name, params)
return if params.respond_to?(:key?) && !params.key?(attr_name)
+
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
end
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 4c0db4d42b1..4b3c473b0bb 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -36,6 +36,18 @@ module API
{}
end
+ def fix_git_env_repository_paths(env, repository_path)
+ if obj_dir_relative = env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence
+ env['GIT_OBJECT_DIRECTORY'] = File.join(repository_path, obj_dir_relative)
+ end
+
+ if alt_obj_dirs_relative = env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'].presence
+ env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_obj_dirs_relative.map { |dir| File.join(repository_path, dir) }
+ end
+
+ env
+ end
+
def log_user_activity(actor)
commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 282af32ca94..2cae53dba53 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -14,6 +14,7 @@ module API
def get_runner_version_from_params
return unless params['info'].present?
+
attributes_for_keys(%w(name version revision platform architecture), params['info'])
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 6e78ac2c903..451121a4cea 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -19,7 +19,9 @@ module API
status 200
# Stores some Git-specific env thread-safely
- Gitlab::Git::Env.set(parse_env)
+ env = parse_env
+ env = fix_git_env_repository_paths(env, repository_path) if project
+ Gitlab::Git::Env.set(env)
actor =
if params[:key_id]
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 0df41dcc903..74dfd9f96de 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -68,7 +68,7 @@ module API
desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
end
get do
- issues = find_issues
+ issues = paginate(find_issues)
options = {
with: Entities::IssueBasic,
@@ -76,7 +76,7 @@ module API
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
- present paginate(issues), options
+ present issues, options
end
end
@@ -95,7 +95,7 @@ module API
get ":id/issues" do
group = find_group!(params[:id])
- issues = find_issues(group_id: group.id)
+ issues = paginate(find_issues(group_id: group.id))
options = {
with: Entities::IssueBasic,
@@ -103,7 +103,7 @@ module API
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
- present paginate(issues), options
+ present issues, options
end
end
@@ -124,7 +124,7 @@ module API
get ":id/issues" do
project = find_project!(params[:id])
- issues = find_issues(project_id: project.id)
+ issues = paginate(find_issues(project_id: project.id))
options = {
with: Entities::IssueBasic,
@@ -133,7 +133,7 @@ module API
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
- present paginate(issues), options
+ present issues, options
end
desc 'Get a single project issue' do
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 3c1c412ba42..a116ab3c9bd 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -136,7 +136,7 @@ module API
authorize_update_builds!
build = find_build!(params[:job_id])
- authorize!(:update_build, build)
+ authorize!(:erase_build, build)
return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index be843ec8251..726f09e3669 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -295,7 +295,7 @@ module API
unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
- ::MergeRequest::MergeWhenPipelineSucceedsService
+ ::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user)
.cancel(merge_request)
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 0b9ab4eeb05..ceaaeca4046 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -33,7 +33,7 @@ module API
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
- paginate(noteable.notes)
+ paginate(noteable.notes.with_metadata)
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
@@ -50,7 +50,7 @@ module API
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
noteable = find_project_noteable(noteables_str, params[:noteable_id])
- note = noteable.notes.find(params[:note_id])
+ note = noteable.notes.with_metadata.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index 259f3f34068..d7b613a717e 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -4,7 +4,6 @@ module API
before do
authenticate!
- require_pages_enabled!
end
after_validation do
@@ -29,10 +28,31 @@ module API
end
end
+ resource :pages do
+ before do
+ require_pages_config_enabled!
+ authenticated_with_full_private_access!
+ end
+
+ desc "Get all pages domains" do
+ success Entities::PagesDomainBasic
+ end
+ params do
+ use :pagination
+ end
+ get "domains" do
+ present paginate(PagesDomain.all), with: Entities::PagesDomainBasic
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
+ before do
+ require_pages_enabled!
+ end
+
desc 'Get all pages domains' do
success Entities::PagesDomain
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index aab7a6c3f93..4cd7e714aa2 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -119,6 +119,8 @@ module API
end
resource :projects do
+ include CustomAttributesEndpoints
+
desc 'Get a list of visible projects for authenticated user' do
success Entities::BasicProjectDetails
end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index d3559ef71be..e816fcdd928 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -165,17 +165,20 @@ module API
def authenticate_show_runner!(runner)
return if runner.is_shared || current_user.admin?
+
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def authenticate_update_runner!(runner)
return if current_user.admin?
+
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def authenticate_delete_runner!(runner)
return if current_user.admin?
+
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
forbidden!("No access granted") unless user_can_access_runner?(runner)
@@ -185,6 +188,7 @@ module API
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner is locked") if runner.locked?
return if current_user.admin?
+
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1e4f7c29633..bbcc851d07a 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -374,6 +374,26 @@ module API
desc: 'The Slack token'
}
],
+ 'packagist' => [
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Packagist API token'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'The server'
+ }
+ ],
'pipelines-email' => [
{
required: true,
@@ -502,6 +522,12 @@ module API
name: :webhook,
type: String,
desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username to use to post the message'
}
],
'teamcity' => [
@@ -551,6 +577,7 @@ module API
KubernetesService,
MattermostSlashCommandsService,
SlackSlashCommandsService,
+ PackagistService,
PipelinesEmailService,
PivotaltrackerService,
PrometheusService,
diff --git a/lib/api/session.rb b/lib/api/session.rb
deleted file mode 100644
index 016415c3023..00000000000
--- a/lib/api/session.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module API
- class Session < Grape::API
- desc 'Login to get token' do
- success Entities::UserWithPrivateDetails
- end
- params do
- optional :login, type: String, desc: 'The username'
- optional :email, type: String, desc: 'The email of the user'
- requires :password, type: String, desc: 'The password of the user'
- at_least_one_of :login, :email
- end
- post "/session" do
- user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
-
- return unauthorized! unless user
- return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
- present user, with: Entities::UserWithPrivateDetails
- end
- end
-end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 00eb7c60f16..c736cc32021 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -95,6 +95,7 @@ module API
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :update_personal_snippet, snippet
attrs = declared_params(include_missing: false).merge(request: request, api: true)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index b6f97a1eac2..0cd89b1bcf8 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -31,7 +31,6 @@ module API
optional :location, type: String, desc: 'The location of the user'
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
- optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
optional :avatar, type: File, desc: 'Avatar image for user'
all_or_none_of :extern_uid, :provider
@@ -101,6 +100,7 @@ module API
requires :email, type: String, desc: 'The email of the user'
optional :password, type: String, desc: 'The password of the new user'
optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
+ optional :skip_confirmation, type: Boolean, desc: 'Flag indicating the account is confirmed'
at_least_one_of :password, :reset_password
requires :name, type: String, desc: 'The name of the user'
requires :username, type: String, desc: 'The username of the user'
@@ -134,6 +134,7 @@ module API
requires :id, type: Integer, desc: 'The ID of the user'
optional :email, type: String, desc: 'The email of the user'
optional :password, type: String, desc: 'The password of the new user'
+ optional :skip_reconfirmation, type: Boolean, desc: 'Flag indicating the account skips the confirmation by email'
optional :name, type: String, desc: 'The name of the user'
optional :username, type: String, desc: 'The username of the user'
use :optional_attributes
@@ -507,9 +508,7 @@ module API
end
get do
entity =
- if sudo?
- Entities::UserWithPrivateDetails
- elsif current_user.admin?
+ if current_user.admin?
Entities::UserWithAdmin
else
Entities::UserPublic
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 69cd12de72c..b201bf77667 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -14,9 +14,11 @@ module API
success ::API::Entities::Branch
end
get ":id/repository/branches" do
- branches = user_project.repository.branches.sort_by(&:name)
+ repository = user_project.repository
+ branches = repository.branches.sort_by(&:name)
+ merged_branch_names = repository.merged_branch_names(branches.map(&:name))
- present branches, with: ::API::Entities::Branch, project: user_project
+ present branches, with: ::API::Entities::Branch, project: user_project, merged_branch_names: merged_branch_names
end
desc 'Delete a branch'
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index f493fd7c7ec..fa0bef39602 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -169,7 +169,7 @@ module API
authorize_update_builds!
build = get_build!(params[:build_id])
- authorize!(:update_build, build)
+ authorize!(:erase_build, build)
return forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index ed206a6def0..0ef26aa696a 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -106,7 +106,7 @@ module API
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- notes = Note.where(commit_id: commit.id).order(:created_at)
+ notes = commit.notes.order(:created_at)
present paginate(notes), with: ::API::Entities::CommitNote
end
@@ -169,10 +169,12 @@ module API
if params[:path]
commit.raw_diffs(limits: false).each do |diff|
next unless diff.new_path == params[:path]
+
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
+
break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
end
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
index faa265f3314..c6d9957d452 100644
--- a/lib/api/v3/runners.rb
+++ b/lib/api/v3/runners.rb
@@ -51,6 +51,7 @@ module API
helpers do
def authenticate_delete_runner!(runner)
return if current_user.admin?
+
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
forbidden!("No access granted") unless user_can_access_runner?(runner)
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
index 2d13d6fabfd..44ed94d2869 100644
--- a/lib/api/v3/services.rb
+++ b/lib/api/v3/services.rb
@@ -395,6 +395,26 @@ module API
desc: 'The Slack token'
}
],
+ 'packagist' => [
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Packagist API token'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'The server'
+ }
+ ],
'pipelines-email' => [
{
required: true,
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
index 0762fc02d70..126ec72248e 100644
--- a/lib/api/v3/snippets.rb
+++ b/lib/api/v3/snippets.rb
@@ -91,6 +91,7 @@ module API
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :update_personal_snippet, snippet
attrs = declared_params(include_missing: false)
@@ -113,6 +114,7 @@ module API
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :destroy_personal_snippet, snippet
snippet.destroy
no_content!
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 3ad09a1b421..b6d273b98c2 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -7,12 +7,16 @@ module Backup
prepare
Project.find_each(batch_size: 1000) do |project|
- progress.print " * #{project.full_path} ... "
+ progress.print " * #{display_repo_path(project)} ... "
path_to_project_repo = path_to_repo(project)
path_to_project_bundle = path_to_bundle(project)
- # Create namespace dir if missing
- FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace
+ # Create namespace dir or hashed path if missing
+ if project.hashed_storage?(:repository)
+ FileUtils.mkdir_p(File.dirname(File.join(backup_repos_path, project.disk_path)))
+ else
+ FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace
+ end
if empty_repo?(project)
progress.puts "[SKIPPED]".color(:cyan)
@@ -42,7 +46,7 @@ module Backup
path_to_wiki_bundle = path_to_bundle(wiki)
if File.exist?(path_to_wiki_repo)
- progress.print " * #{wiki.full_path} ... "
+ progress.print " * #{display_repo_path(wiki)} ... "
if empty_repo?(wiki)
progress.puts " [SKIPPED]".color(:cyan)
else
@@ -71,7 +75,7 @@ module Backup
end
Project.find_each(batch_size: 1000) do |project|
- progress.print " * #{project.full_path} ... "
+ progress.print " * #{display_repo_path(project)} ... "
path_to_project_repo = path_to_repo(project)
path_to_project_bundle = path_to_bundle(project)
@@ -104,7 +108,7 @@ module Backup
path_to_wiki_bundle = path_to_bundle(wiki)
if File.exist?(path_to_wiki_bundle)
- progress.print " * #{wiki.full_path} ... "
+ progress.print " * #{display_repo_path(wiki)} ... "
# If a wiki bundle exists, first remove the empty repo
# that was initialized with ProjectWiki.new() and then
@@ -185,14 +189,14 @@ module Backup
def progress_warn(project, cmd, output)
progress.puts "[WARNING] Executing #{cmd}".color(:orange)
- progress.puts "Ignoring error on #{project.full_path} - #{output}".color(:orange)
+ progress.puts "Ignoring error on #{display_repo_path(project)} - #{output}".color(:orange)
end
def empty_repo?(project_or_wiki)
project_or_wiki.repository.expire_exists_cache # protect backups from stale cache
project_or_wiki.repository.empty_repo?
rescue => e
- progress.puts "Ignoring repository error and continuing backing up project: #{project_or_wiki.full_path} - #{e.message}".color(:orange)
+ progress.puts "Ignoring repository error and continuing backing up project: #{display_repo_path(project_or_wiki)} - #{e.message}".color(:orange)
false
end
@@ -204,5 +208,9 @@ module Backup
def progress
$progress
end
+
+ def display_repo_path(project)
+ project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path
+ end
end
end
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 35ca234c1ba..5df98f66f3b 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -3,8 +3,8 @@ module Banzai
Renderer.render(text, context)
end
- def self.render_field(object, field)
- Renderer.render_field(object, field)
+ def self.render_field(object, field, context = {})
+ Renderer.render_field(object, field, context)
end
def self.cache_collection_render(texts_and_contexts)
diff --git a/lib/banzai/filter/absolute_link_filter.rb b/lib/banzai/filter/absolute_link_filter.rb
new file mode 100644
index 00000000000..1ec6201523f
--- /dev/null
+++ b/lib/banzai/filter/absolute_link_filter.rb
@@ -0,0 +1,34 @@
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that converts relative urls into absolute ones.
+ class AbsoluteLinkFilter < HTML::Pipeline::Filter
+ def call
+ return doc unless context[:only_path] == false
+
+ doc.search('a.gfm').each do |el|
+ process_link_attr el.attribute('href')
+ end
+
+ doc
+ end
+
+ protected
+
+ def process_link_attr(html_attr)
+ return if html_attr.blank?
+ return if html_attr.value.start_with?('//')
+
+ uri = URI(html_attr.value)
+ html_attr.value = absolute_link_attr(uri) if uri.relative?
+ rescue URI::Error
+ # noop
+ end
+
+ def absolute_link_attr(uri)
+ URI.join(Gitlab.config.gitlab.url, uri).to_s
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index ef4578aabd6..9fef386de16 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -95,7 +95,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc unless project || group
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
@@ -288,10 +288,14 @@ module Banzai
end
def current_project_path
+ return unless project
+
@current_project_path ||= project.full_path
end
def current_project_namespace_path
+ return unless project
+
@current_project_namespace_path ||= project.namespace.full_path
end
@@ -307,30 +311,6 @@ module Banzai
def project_refs_cache
RequestStore[:banzai_project_refs] ||= {}
end
-
- def cached_call(request_store_key, cache_key, path: [])
- if RequestStore.active?
- cache = RequestStore[request_store_key] ||= Hash.new do |hash, key|
- hash[key] = Hash.new { |h, k| h[k] = {} }
- end
-
- cache = cache.dig(*path) if path.any?
-
- get_or_set_cache(cache, cache_key) { yield }
- else
- yield
- end
- end
-
- def get_or_set_cache(cache, key)
- if cache.key?(key)
- cache[key]
- else
- value = yield
- cache[key] = value if key.present?
- value
- end
- end
end
end
end
diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb
new file mode 100644
index 00000000000..b545b947a2c
--- /dev/null
+++ b/lib/banzai/filter/mermaid_filter.rb
@@ -0,0 +1,20 @@
+module Banzai
+ module Filter
+ class MermaidFilter < HTML::Pipeline::Filter
+ def call
+ doc.css('pre[lang="mermaid"]').add_class('mermaid')
+ doc.css('pre[lang="mermaid"]').add_class('js-render-mermaid')
+
+ # The `<code></code>` blocks are added in the lib/banzai/filter/syntax_highlight_filter.rb
+ # We want to keep context and consistency, so we the blocks are added for all filters.
+ # Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107/diffs?diff_id=7962900#note_45495859
+ doc.css('pre[lang="mermaid"]').each do |pre|
+ document = pre.at('code')
+ document.replace(document.content)
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 4fc5f211e84..bb5da310e09 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -56,7 +56,7 @@ module Banzai
end
def find_milestone_with_finder(project, params)
- finder_params = { project_ids: [project.id], order: nil }
+ finder_params = { project_ids: [project.id], order: nil, state: 'all' }
# We don't support IID lookups for group milestones, because IIDs can
# clash between group and project milestones.
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index a6f8650ed3d..b9d5ecf70ec 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -8,6 +8,8 @@ module Banzai
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
+ include RequestStoreReferenceCache
+
class << self
attr_accessor :reference_type
end
@@ -55,6 +57,10 @@ module Banzai
context[:project]
end
+ def group
+ context[:group]
+ end
+
def skip_project_check?
context[:skip_project_check]
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 7da565043d1..a79a0154846 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -14,23 +14,26 @@ module Banzai
end
def highlight_node(node)
- language = node.attr('lang')
code = node.text
- css_classes = "code highlight"
- lexer = lexer_for(language)
- lang = lexer.tag
-
- begin
- code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
-
- css_classes << " js-syntax-highlight #{lang}"
- rescue
- lang = nil
- # Gracefully handle syntax highlighter bugs/errors to ensure
- # users can still access an issue/comment/etc.
+ css_classes = 'code highlight js-syntax-highlight'
+ language = node.attr('lang')
+
+ if use_rouge?(language)
+ lexer = lexer_for(language)
+ language = lexer.tag
+
+ begin
+ code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: language)
+ css_classes << " #{language}"
+ rescue
+ # Gracefully handle syntax highlighter bugs/errors to ensure
+ # users can still access an issue/comment/etc.
+
+ language = nil
+ end
end
- highlighted = %(<pre class="#{css_classes}" lang="#{lang}" v-pre="true"><code>#{code}</code></pre>)
+ highlighted = %(<pre class="#{css_classes}" lang="#{language}" v-pre="true"><code>#{code}</code></pre>)
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
@@ -51,6 +54,10 @@ module Banzai
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
+
+ def use_rouge?(language)
+ %w(math mermaid plantuml).exclude?(language)
+ end
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f3356d6c51e..c7fa8a8119f 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil? && !skip_project_check?
+ return doc if project.nil? && group.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -60,10 +60,14 @@ module Banzai
self.class.references_in(text) do |match, username|
if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
- elsif namespace = namespaces[username.downcase]
- link_to_namespace(namespace, link_content: link_content) || match
else
- match
+ cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
+ if namespace = namespaces[username.downcase]
+ link_to_namespace(namespace, link_content: link_content) || match
+ else
+ match
+ end
+ end
end
end
end
@@ -74,7 +78,10 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path).transform_keys(&:downcase)
+ @namespaces ||= Namespace.eager_load(:owner, :route)
+ .where_full_path_in(usernames)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
end
# Returns all usernames referenced in the current document.
@@ -101,19 +108,12 @@ module Banzai
end
def link_to_all(link_content: nil)
- project = context[:project]
author = context[:author]
- if author && !project.team.member?(author)
+ if author && !team_member?(author)
link_content
else
- url = urls.project_url(project,
- only_path: context[:only_path])
-
- data = data_attribute(project: project.id, author: author.try(:id))
- content = link_content || User.reference_prefix + 'all'
-
- link_tag(url, data, content, 'All Project and Group Members')
+ parent_url(link_content, author)
end
end
@@ -144,6 +144,35 @@ module Banzai
def link_tag(url, data, link_content, title)
%(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end
+
+ def parent
+ context[:project] || context[:group]
+ end
+
+ def parent_group?
+ parent.is_a?(Group)
+ end
+
+ def team_member?(user)
+ if parent_group?
+ parent.member?(user)
+ else
+ parent.team.member?(user)
+ end
+ end
+
+ def parent_url(link_content, author)
+ if parent_group?
+ url = urls.group_url(parent, only_path: context[:only_path])
+ data = data_attribute(group: group.id, author: author.try(:id))
+ else
+ url = urls.project_url(parent, only_path: context[:only_path])
+ data = data_attribute(project: project.id, author: author.try(:id))
+ end
+
+ content = link_content || User.reference_prefix + 'all'
+ link_tag(url, data, content, 'All Project and Group Members')
+ end
end
end
end
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
deleted file mode 100644
index 2b7c10f1a0e..00000000000
--- a/lib/banzai/note_renderer.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module Banzai
- module NoteRenderer
- # Renders a collection of Note instances.
- #
- # notes - The notes to render.
- # project - The project to use for redacting.
- # user - The user viewing the notes.
- # path - The request path.
- # wiki - The project's wiki.
- # git_ref - The current Git reference.
- def self.render(notes, project, user = nil, path = nil, wiki = nil, git_ref = nil)
- renderer = ObjectRenderer.new(project,
- user,
- requested_path: path,
- project_wiki: wiki,
- ref: git_ref)
-
- renderer.render(notes, :note)
- end
- end
-end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index e40556e869c..ecb3affbba5 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -37,7 +37,7 @@ module Banzai
objects.each_with_index do |object, index|
redacted_data = redacted[index]
- object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) # rubocop:disable GitlabSecurity/PublicSend
+ object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html(save_options).html_safe) # rubocop:disable GitlabSecurity/PublicSend
object.user_visible_reference_count = redacted_data[:visible_reference_count] if object.respond_to?(:user_visible_reference_count)
end
end
@@ -83,5 +83,11 @@ module Banzai
skip_redaction: true
)
end
+
+ def save_options
+ return {} unless base_context[:xhtml]
+
+ { save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML }
+ end
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 3208abfc538..55874ad50a3 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -14,6 +14,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::MathFilter,
+ Filter::MermaidFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index 131ac3b0eec..dcd52bc03c7 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -3,9 +3,10 @@ module Banzai
class PostProcessPipeline < BasePipeline
def self.filters
FilterArray[
+ Filter::RedactorFilter,
Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
- Filter::RedactorFilter
+ Filter::AbsoluteLinkFilter
]
end
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
index fb2faae02bc..a19a05e8c0d 100644
--- a/lib/banzai/querying.rb
+++ b/lib/banzai/querying.rb
@@ -52,8 +52,10 @@ module Banzai
children.each do |child|
next if child.text.blank?
+
node = nodes.shift
break unless node == child
+
filtered_nodes << node
end
end
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index 4d336068861..8932d4f2905 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -31,6 +31,7 @@ module Banzai
nodes.each do |node|
if node.has_attribute?(group_attr)
next unless can_read_group_reference?(node, user, groups)
+
visible << node
elsif can_read_project_reference?(node)
visible << node
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 5f91884a878..0050295eeda 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -32,12 +32,9 @@ module Banzai
# Convert a Markdown-containing field on an object into an HTML-safe String
# of HTML. This method is analogous to calling render(object.field), but it
# can cache the rendered HTML in the object, rather than Redis.
- #
- # The context to use is managed by the object and cannot be changed.
- # Use #render, passing it the field text, if a custom rendering is needed.
- def self.render_field(object, field)
+ def self.render_field(object, field, context = {})
unless object.respond_to?(:cached_markdown_fields)
- return cacheless_render_field(object, field)
+ return cacheless_render_field(object, field, context)
end
object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
@@ -46,9 +43,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
- def self.cacheless_render_field(object, field, options = {})
+ def self.cacheless_render_field(object, field, context = {})
text = object.__send__(field) # rubocop:disable GitlabSecurity/PublicSend
- context = object.banzai_render_context(field).merge(options)
+ context = context.reverse_merge(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)
cacheless_render(text, context)
end
@@ -152,6 +149,7 @@ module Banzai
def self.full_cache_key(cache_key, pipeline_name)
return unless cache_key
+
["banzai", *cache_key, pipeline_name || :full]
end
@@ -160,6 +158,7 @@ module Banzai
# method.
def self.full_cache_multi_key(cache_key, pipeline_name)
return unless cache_key
+
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/lib/banzai/request_store_reference_cache.rb b/lib/banzai/request_store_reference_cache.rb
new file mode 100644
index 00000000000..426131442a2
--- /dev/null
+++ b/lib/banzai/request_store_reference_cache.rb
@@ -0,0 +1,27 @@
+module Banzai
+ module RequestStoreReferenceCache
+ def cached_call(request_store_key, cache_key, path: [])
+ if RequestStore.active?
+ cache = RequestStore[request_store_key] ||= Hash.new do |hash, key|
+ hash[key] = Hash.new { |h, k| h[k] = {} }
+ end
+
+ cache = cache.dig(*path) if path.any?
+
+ get_or_set_cache(cache, cache_key) { yield }
+ else
+ yield
+ end
+ end
+
+ def get_or_set_cache(cache, key)
+ if cache.key?(key)
+ cache[key]
+ else
+ value = yield
+ cache[key] = value if key.present?
+ value
+ end
+ end
+ end
+end
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
index 6fc1d56d7a0..fd2ac2db0a9 100644
--- a/lib/constraints/group_url_constrainer.rb
+++ b/lib/constraints/group_url_constrainer.rb
@@ -2,7 +2,7 @@ class GroupUrlConstrainer
def matches?(request)
full_path = request.params[:group_id] || request.params[:id]
- return false unless DynamicPathValidator.valid_group_path?(full_path)
+ return false unless NamespacePathValidator.valid_path?(full_path)
Group.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 5bef29eb1da..e90ecb5ec69 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -4,7 +4,7 @@ class ProjectUrlConstrainer
project_path = request.params[:project_id] || request.params[:id]
full_path = [namespace_path, project_path].join('/')
- return false unless DynamicPathValidator.valid_project_path?(full_path)
+ return false unless ProjectPathValidator.valid_path?(full_path)
# We intentionally allow SELECT(*) here so result of this query can be used
# as cache for further Project.find_by_full_path calls within request
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
index d16ae7f3f40..b7633aa7cbb 100644
--- a/lib/constraints/user_url_constrainer.rb
+++ b/lib/constraints/user_url_constrainer.rb
@@ -2,7 +2,7 @@ class UserUrlConstrainer
def matches?(request)
full_path = request.params[:username]
- return false unless DynamicPathValidator.valid_user_path?(full_path)
+ return false unless UserPathValidator.valid_path?(full_path)
User.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb
index ae65653645b..b1949d693ad 100644
--- a/lib/declarative_policy.rb
+++ b/lib/declarative_policy.rb
@@ -30,6 +30,7 @@ module DeclarativePolicy
policy_class = class_for_class(subject.class)
raise "no policy for #{subject.class.name}" if policy_class.nil?
+
policy_class
end
@@ -84,6 +85,7 @@ module DeclarativePolicy
while subject.respond_to?(:declarative_policy_delegate)
raise ArgumentError, "circular delegations" if seen.include?(subject.object_id)
+
seen << subject.object_id
subject = subject.declarative_policy_delegate
end
diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb
index b028169f500..47542194497 100644
--- a/lib/declarative_policy/base.rb
+++ b/lib/declarative_policy/base.rb
@@ -276,6 +276,7 @@ module DeclarativePolicy
# boolean `false`
def cache(key, &b)
return @cache[key] if cached?(key)
+
@cache[key] = yield
end
@@ -291,6 +292,7 @@ module DeclarativePolicy
@_conditions[name] ||=
begin
raise "invalid condition #{name}" unless self.class.conditions.key?(name)
+
ManifestCondition.new(self.class.conditions[name], self)
end
end
diff --git a/lib/declarative_policy/cache.rb b/lib/declarative_policy/cache.rb
index 0804edba016..780d8f707bd 100644
--- a/lib/declarative_policy/cache.rb
+++ b/lib/declarative_policy/cache.rb
@@ -3,6 +3,7 @@ module DeclarativePolicy
class << self
def user_key(user)
return '<anonymous>' if user.nil?
+
id_for(user)
end
@@ -15,6 +16,7 @@ module DeclarativePolicy
def subject_key(subject)
return '<nil>' if subject.nil?
return subject.inspect if subject.is_a?(Symbol)
+
"#{subject.class.name}:#{id_for(subject)}"
end
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
index 7cfa82a9a9f..e309244a3b3 100644
--- a/lib/declarative_policy/rule.rb
+++ b/lib/declarative_policy/rule.rb
@@ -83,6 +83,7 @@ module DeclarativePolicy
def cached_pass?(context)
condition = context.condition(@name)
return nil unless condition.cached?
+
condition.pass?
end
@@ -109,6 +110,7 @@ module DeclarativePolicy
def delegated_context(context)
policy = context.delegated_policies[@delegate_name]
raise MissingDelegate if policy.nil?
+
policy
end
@@ -121,6 +123,7 @@ module DeclarativePolicy
def cached_pass?(context)
condition = delegated_context(context).condition(@name)
return nil unless condition.cached?
+
condition.pass?
rescue MissingDelegate
false
@@ -157,6 +160,7 @@ module DeclarativePolicy
def cached_pass?(context)
runner = context.runner(@ability)
return nil unless runner.cached?
+
runner.pass?
end
@@ -258,6 +262,7 @@ module DeclarativePolicy
def score(context)
return 0 unless cached_pass?(context).nil?
+
@rules.map { |r| r.score(context) }.inject(0, :+)
end
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 45ff2ef9ced..77c91817382 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -43,6 +43,7 @@ module DeclarativePolicy
# used by Rule::Ability. See #steps_by_score
def score
return 0 if cached?
+
steps.map(&:score).inject(0, :+)
end
diff --git a/lib/feature.rb b/lib/feature.rb
index 4bd29aed687..ac3bc65c0d5 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -5,6 +5,10 @@ class Feature
class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature
# Using `self.table_name` won't work. ActiveRecord bug?
superclass.table_name = 'features'
+
+ def self.feature_names
+ pluck(:key)
+ end
end
class FlipperGate < Flipper::Adapters::ActiveRecord::Gate
@@ -22,11 +26,19 @@ class Feature
flipper.feature(key)
end
+ def persisted_names
+ if RequestStore.active?
+ RequestStore[:flipper_persisted_names] ||= FlipperFeature.feature_names
+ else
+ FlipperFeature.feature_names
+ end
+ end
+
def persisted?(feature)
# Flipper creates on-memory features when asked for a not-yet-created one.
# If we want to check if a feature has been actually set, we look for it
# on the persisted features list.
- all.map(&:name).include?(feature.name)
+ persisted_names.include?(feature.name)
end
def enabled?(key, thing = nil)
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index de391de9059..69d981e8be9 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -8,6 +8,7 @@ class FileSizeValidator < ActiveModel::EachValidator
def initialize(options)
if range = (options.delete(:in) || options.delete(:within))
raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
+
options[:minimum], options[:maximum] = range.begin, range.end
options[:maximum] -= 1 if range.exclude_end?
end
diff --git a/lib/github/client.rb b/lib/github/client.rb
deleted file mode 100644
index 29bd9c1f39e..00000000000
--- a/lib/github/client.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-module Github
- class Client
- TIMEOUT = 60
- DEFAULT_PER_PAGE = 100
-
- attr_reader :connection, :rate_limit
-
- def initialize(options)
- @connection = Faraday.new(url: options.fetch(:url, root_endpoint)) do |faraday|
- faraday.options.open_timeout = options.fetch(:timeout, TIMEOUT)
- faraday.options.timeout = options.fetch(:timeout, TIMEOUT)
- faraday.authorization 'token', options.fetch(:token)
- faraday.adapter :net_http
- faraday.ssl.verify = verify_ssl
- end
-
- @rate_limit = RateLimit.new(connection)
- end
-
- def get(url, query = {})
- exceed, reset_in = rate_limit.get
- sleep reset_in if exceed
-
- Github::Response.new(connection.get(url, { per_page: DEFAULT_PER_PAGE }.merge(query)))
- end
-
- private
-
- def root_endpoint
- custom_endpoint || github_endpoint
- end
-
- def custom_endpoint
- github_omniauth_provider.dig('args', 'client_options', 'site')
- end
-
- def verify_ssl
- # If there is no config, we're connecting to github.com
- # and we should verify ssl.
- github_omniauth_provider.fetch('verify_ssl', true)
- end
-
- def github_endpoint
- OmniAuth::Strategies::GitHub.default_options[:client_options][:site]
- end
-
- def github_omniauth_provider
- @github_omniauth_provider ||=
- Gitlab.config.omniauth.providers
- .find { |provider| provider.name == 'github' }
- .to_h
- end
- end
-end
diff --git a/lib/github/collection.rb b/lib/github/collection.rb
deleted file mode 100644
index 014b2038c4b..00000000000
--- a/lib/github/collection.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Github
- class Collection
- attr_reader :options
-
- def initialize(options)
- @options = options
- end
-
- def fetch(url, query = {})
- return [] if url.blank?
-
- Enumerator.new do |yielder|
- loop do
- response = client.get(url, query)
- response.body.each { |item| yielder << item }
-
- raise StopIteration unless response.rels.key?(:next)
- url = response.rels[:next]
- end
- end.lazy
- end
-
- private
-
- def client
- @client ||= Github::Client.new(options)
- end
- end
-end
diff --git a/lib/github/error.rb b/lib/github/error.rb
deleted file mode 100644
index 66d7afaa787..00000000000
--- a/lib/github/error.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-module Github
- RepositoryFetchError = Class.new(StandardError)
-end
diff --git a/lib/github/import.rb b/lib/github/import.rb
deleted file mode 100644
index 76612799412..00000000000
--- a/lib/github/import.rb
+++ /dev/null
@@ -1,376 +0,0 @@
-require_relative 'error'
-require_relative 'import/issue'
-require_relative 'import/legacy_diff_note'
-require_relative 'import/merge_request'
-require_relative 'import/note'
-
-module Github
- class Import
- include Gitlab::ShellAdapter
-
- attr_reader :project, :repository, :repo, :repo_url, :wiki_url,
- :options, :errors, :cached, :verbose, :last_fetched_at
-
- def initialize(project, options = {})
- @project = project
- @repository = project.repository
- @repo = project.import_source
- @repo_url = project.import_url
- @wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
- @options = options.reverse_merge(token: project.import_data&.credentials&.fetch(:user))
- @verbose = options.fetch(:verbose, false)
- @cached = Hash.new { |hash, key| hash[key] = Hash.new }
- @errors = []
- @last_fetched_at = nil
- end
-
- # rubocop: disable Rails/Output
- def execute
- puts 'Fetching repository...'.color(:aqua) if verbose
- setup_and_fetch_repository
- puts 'Fetching labels...'.color(:aqua) if verbose
- fetch_labels
- puts 'Fetching milestones...'.color(:aqua) if verbose
- fetch_milestones
- puts 'Fetching pull requests...'.color(:aqua) if verbose
- fetch_pull_requests
- puts 'Fetching issues...'.color(:aqua) if verbose
- fetch_issues
- puts 'Fetching releases...'.color(:aqua) if verbose
- fetch_releases
- puts 'Cloning wiki repository...'.color(:aqua) if verbose
- fetch_wiki_repository
- puts 'Expiring repository cache...'.color(:aqua) if verbose
- expire_repository_cache
-
- errors.empty?
- rescue Github::RepositoryFetchError
- expire_repository_cache
- false
- ensure
- keep_track_of_errors
- end
-
- private
-
- def setup_and_fetch_repository
- begin
- project.ensure_repository
- project.repository.add_remote('github', repo_url)
- project.repository.set_import_remote_as_mirror('github')
- project.repository.add_remote_fetch_config('github', '+refs/pull/*/head:refs/merge-requests/*/head')
- fetch_remote(forced: true)
- rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
- error(:project, repo_url, e.message)
- raise Github::RepositoryFetchError
- end
- end
-
- def fetch_remote(forced: false)
- @last_fetched_at = Time.now
- project.repository.fetch_remote('github', forced: forced)
- end
-
- def fetch_wiki_repository
- return if project.wiki.repository_exists?
-
- wiki_path = project.wiki.disk_path
- gitlab_shell.import_repository(project.repository_storage_path, wiki_path, wiki_url)
- rescue Gitlab::Shell::Error => e
- # GitHub error message when the wiki repo has not been created,
- # this means that repo has wiki enabled, but have no pages. So,
- # we can skip the import.
- if e.message !~ /repository not exported/
- error(:wiki, wiki_url, e.message)
- end
- end
-
- def fetch_labels
- url = "/repos/#{repo}/labels"
-
- while url
- response = Github::Client.new(options).get(url)
-
- response.body.each do |raw|
- begin
- representation = Github::Representation::Label.new(raw)
-
- label = project.labels.find_or_create_by!(title: representation.title) do |label|
- label.color = representation.color
- end
-
- cached[:label_ids][representation.title] = label.id
- rescue => e
- error(:label, representation.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def fetch_milestones
- url = "/repos/#{repo}/milestones"
-
- while url
- response = Github::Client.new(options).get(url, state: :all)
-
- response.body.each do |raw|
- begin
- milestone = Github::Representation::Milestone.new(raw)
- next if project.milestones.where(iid: milestone.iid).exists?
-
- project.milestones.create!(
- iid: milestone.iid,
- title: milestone.title,
- description: milestone.description,
- due_date: milestone.due_date,
- state: milestone.state,
- created_at: milestone.created_at,
- updated_at: milestone.updated_at
- )
- rescue => e
- error(:milestone, milestone.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def fetch_pull_requests
- url = "/repos/#{repo}/pulls"
-
- while url
- response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
-
- response.body.each do |raw|
- pull_request = Github::Representation::PullRequest.new(raw, options.merge(project: project))
- merge_request = MergeRequest.find_or_initialize_by(iid: pull_request.iid, source_project_id: project.id)
- next unless merge_request.new_record? && pull_request.valid?
-
- begin
- # If the PR has been created/updated after we last fetched the
- # remote, we fetch again to get the up-to-date refs.
- fetch_remote if pull_request.updated_at > last_fetched_at
-
- author_id = user_id(pull_request.author, project.creator_id)
- description = format_description(pull_request.description, pull_request.author)
-
- merge_request.attributes = {
- iid: pull_request.iid,
- title: pull_request.title,
- description: description,
- ref_fetched: true,
- source_project: pull_request.source_project,
- source_branch: pull_request.source_branch_name,
- source_branch_sha: pull_request.source_branch_sha,
- target_project: pull_request.target_project,
- target_branch: pull_request.target_branch_name,
- target_branch_sha: pull_request.target_branch_sha,
- state: pull_request.state,
- milestone_id: milestone_id(pull_request.milestone),
- author_id: author_id,
- assignee_id: user_id(pull_request.assignee),
- created_at: pull_request.created_at,
- updated_at: pull_request.updated_at
- }
-
- merge_request.save!(validate: false)
- merge_request.merge_request_diffs.create
-
- review_comments_url = "/repos/#{repo}/pulls/#{pull_request.iid}/comments"
- fetch_comments(merge_request, :review_comment, review_comments_url, LegacyDiffNote)
- rescue => e
- error(:pull_request, pull_request.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def fetch_issues
- url = "/repos/#{repo}/issues"
-
- while url
- response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
-
- response.body.each { |raw| populate_issue(raw) }
-
- url = response.rels[:next]
- end
- end
-
- def populate_issue(raw)
- representation = Github::Representation::Issue.new(raw, options)
-
- begin
- # Every pull request is an issue, but not every issue
- # is a pull request. For this reason, "shared" actions
- # for both features, like manipulating assignees, labels
- # and milestones, are provided within the Issues API.
- if representation.pull_request?
- return unless representation.labels? || representation.comments?
-
- merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
-
- if representation.labels?
- merge_request.update_attribute(:label_ids, label_ids(representation.labels))
- end
-
- fetch_comments_conditionally(merge_request, representation)
- else
- return if Issue.exists?(iid: representation.iid, project_id: project.id)
-
- author_id = user_id(representation.author, project.creator_id)
- issue = Issue.new
- issue.iid = representation.iid
- issue.project_id = project.id
- issue.title = representation.title
- issue.description = format_description(representation.description, representation.author)
- issue.state = representation.state
- issue.milestone_id = milestone_id(representation.milestone)
- issue.author_id = author_id
- issue.created_at = representation.created_at
- issue.updated_at = representation.updated_at
- issue.save!(validate: false)
-
- issue.update(
- label_ids: label_ids(representation.labels),
- assignee_ids: assignee_ids(representation.assignees))
-
- fetch_comments_conditionally(issue, representation)
- end
- rescue => e
- error(:issue, representation.url, e.message)
- end
- end
-
- def fetch_comments_conditionally(issuable, representation)
- if representation.comments?
- comments_url = "/repos/#{repo}/issues/#{issuable.iid}/comments"
- fetch_comments(issuable, :comment, comments_url)
- end
- end
-
- def fetch_comments(noteable, type, url, klass = Note)
- while url
- comments = Github::Client.new(options).get(url)
-
- ActiveRecord::Base.no_touching do
- comments.body.each do |raw|
- begin
- representation = Github::Representation::Comment.new(raw, options)
- author_id = user_id(representation.author, project.creator_id)
-
- note = klass.new
- note.project_id = project.id
- note.noteable = noteable
- note.note = format_description(representation.note, representation.author)
- note.commit_id = representation.commit_id
- note.line_code = representation.line_code
- note.author_id = author_id
- note.created_at = representation.created_at
- note.updated_at = representation.updated_at
- note.save!(validate: false)
- rescue => e
- error(type, representation.url, e.message)
- end
- end
- end
-
- url = comments.rels[:next]
- end
- end
-
- def fetch_releases
- url = "/repos/#{repo}/releases"
-
- while url
- response = Github::Client.new(options).get(url)
-
- response.body.each do |raw|
- representation = Github::Representation::Release.new(raw)
- next unless representation.valid?
-
- release = ::Release.find_or_initialize_by(project_id: project.id, tag: representation.tag)
- next unless release.new_record?
-
- begin
- release.description = representation.description
- release.created_at = representation.created_at
- release.updated_at = representation.updated_at
- release.save!(validate: false)
- rescue => e
- error(:release, representation.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def label_ids(labels)
- labels.map { |label| cached[:label_ids][label.title] }.compact
- end
-
- def assignee_ids(assignees)
- assignees.map { |assignee| user_id(assignee) }.compact
- end
-
- def milestone_id(milestone)
- return unless milestone.present?
-
- project.milestones.select(:id).find_by(iid: milestone.iid)&.id
- end
-
- def user_id(user, fallback_id = nil)
- return unless user.present?
- return cached[:user_ids][user.id] if cached[:user_ids][user.id].present?
-
- gitlab_user_id = user_id_by_external_uid(user.id) || user_id_by_email(user.email)
-
- cached[:gitlab_user_ids][user.id] = gitlab_user_id.present?
- cached[:user_ids][user.id] = gitlab_user_id || fallback_id
- end
-
- def user_id_by_email(email)
- return nil unless email
-
- ::User.find_by_any_email(email)&.id
- end
-
- def user_id_by_external_uid(id)
- return nil unless id
-
- ::User.select(:id)
- .joins(:identities)
- .merge(::Identity.where(provider: :github, extern_uid: id))
- .first&.id
- end
-
- def format_description(body, author)
- return body if cached[:gitlab_user_ids][author.id]
-
- "*Created by: #{author.username}*\n\n#{body}"
- end
-
- def expire_repository_cache
- repository.expire_content_cache if project.repository_exists?
- end
-
- def keep_track_of_errors
- return unless errors.any?
-
- project.update_column(:import_error, {
- message: 'The remote data could not be fully imported.',
- errors: errors
- }.to_json)
- end
-
- def error(type, url, message)
- errors << { type: type, url: Gitlab::UrlSanitizer.sanitize(url), error: message }
- end
- end
-end
diff --git a/lib/github/import/issue.rb b/lib/github/import/issue.rb
deleted file mode 100644
index 171f0872666..00000000000
--- a/lib/github/import/issue.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- class Import
- class Issue < ::Issue
- self.table_name = 'issues'
-
- self.reset_callbacks :save
- self.reset_callbacks :create
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/import/legacy_diff_note.rb b/lib/github/import/legacy_diff_note.rb
deleted file mode 100644
index 18adff560b6..00000000000
--- a/lib/github/import/legacy_diff_note.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module Github
- class Import
- class LegacyDiffNote < ::LegacyDiffNote
- self.table_name = 'notes'
- self.store_full_sti_class = false
-
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/import/merge_request.rb b/lib/github/import/merge_request.rb
deleted file mode 100644
index c258e5d5e0e..00000000000
--- a/lib/github/import/merge_request.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- class Import
- class MergeRequest < ::MergeRequest
- self.table_name = 'merge_requests'
-
- self.reset_callbacks :create
- self.reset_callbacks :save
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/import/note.rb b/lib/github/import/note.rb
deleted file mode 100644
index 8cf4f30e6b7..00000000000
--- a/lib/github/import/note.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- class Import
- class Note < ::Note
- self.table_name = 'notes'
- self.store_full_sti_class = false
-
- self.reset_callbacks :save
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/rate_limit.rb b/lib/github/rate_limit.rb
deleted file mode 100644
index 884693d093c..00000000000
--- a/lib/github/rate_limit.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Github
- class RateLimit
- SAFE_REMAINING_REQUESTS = 100
- SAFE_RESET_TIME = 500
- RATE_LIMIT_URL = '/rate_limit'.freeze
-
- attr_reader :connection
-
- def initialize(connection)
- @connection = connection
- end
-
- def get
- response = connection.get(RATE_LIMIT_URL)
-
- # GitHub Rate Limit API returns 404 when the rate limit is disabled
- return false unless response.status != 404
-
- body = Oj.load(response.body, class_cache: false, mode: :compat)
- remaining = body.dig('rate', 'remaining').to_i
- reset_in = body.dig('rate', 'reset').to_i
- exceed = remaining <= SAFE_REMAINING_REQUESTS
-
- [exceed, reset_in]
- end
- end
-end
diff --git a/lib/github/repositories.rb b/lib/github/repositories.rb
deleted file mode 100644
index c1c9448f305..00000000000
--- a/lib/github/repositories.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Github
- class Repositories
- attr_reader :options
-
- def initialize(options)
- @options = options
- end
-
- def fetch
- Collection.new(options).fetch(repos_url)
- end
-
- private
-
- def repos_url
- '/user/repos'
- end
- end
-end
diff --git a/lib/github/representation/base.rb b/lib/github/representation/base.rb
deleted file mode 100644
index f26bdbdd546..00000000000
--- a/lib/github/representation/base.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Github
- module Representation
- class Base
- def initialize(raw, options = {})
- @raw = raw
- @options = options
- end
-
- def id
- raw['id']
- end
-
- def url
- raw['url']
- end
-
- def created_at
- raw['created_at']
- end
-
- def updated_at
- raw['updated_at']
- end
-
- private
-
- attr_reader :raw, :options
- end
- end
-end
diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb
deleted file mode 100644
index 0087a3d3c4f..00000000000
--- a/lib/github/representation/branch.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-module Github
- module Representation
- class Branch < Representation::Base
- attr_reader :repository
-
- def user
- raw.dig('user', 'login') || 'unknown'
- end
-
- def repo?
- raw['repo'].present?
- end
-
- def repo
- return unless repo?
-
- @repo ||= Github::Representation::Repo.new(raw['repo'])
- end
-
- def ref
- raw['ref']
- end
-
- def sha
- raw['sha']
- end
-
- def short_sha
- Commit.truncate_sha(sha)
- end
-
- def valid?
- sha.present? && ref.present?
- end
-
- def restore!(name)
- repository.create_branch(name, sha)
- rescue Gitlab::Git::Repository::InvalidRef => e
- Rails.logger.error("#{self.class.name}: Could not restore branch #{name}: #{e}")
- end
-
- def remove!(name)
- repository.delete_branch(name)
- rescue Gitlab::Git::Repository::DeleteBranchError => e
- Rails.logger.error("#{self.class.name}: Could not remove branch #{name}: #{e}")
- end
-
- private
-
- def repository
- @repository ||= options.fetch(:repository)
- end
- end
- end
-end
diff --git a/lib/github/representation/comment.rb b/lib/github/representation/comment.rb
deleted file mode 100644
index 83bf0b5310d..00000000000
--- a/lib/github/representation/comment.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Github
- module Representation
- class Comment < Representation::Base
- def note
- raw['body'] || ''
- end
-
- def author
- @author ||= Github::Representation::User.new(raw['user'], options)
- end
-
- def commit_id
- raw['commit_id']
- end
-
- def line_code
- return unless on_diff?
-
- parsed_lines = Gitlab::Diff::Parser.new.parse(diff_hunk.lines)
- generate_line_code(parsed_lines.to_a.last)
- end
-
- private
-
- def generate_line_code(line)
- Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
- end
-
- def on_diff?
- diff_hunk.present?
- end
-
- def diff_hunk
- raw['diff_hunk']
- end
-
- def file_path
- raw['path']
- end
- end
- end
-end
diff --git a/lib/github/representation/issuable.rb b/lib/github/representation/issuable.rb
deleted file mode 100644
index 768ba3b993c..00000000000
--- a/lib/github/representation/issuable.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module Github
- module Representation
- class Issuable < Representation::Base
- def iid
- raw['number']
- end
-
- def title
- raw['title']
- end
-
- def description
- raw['body'] || ''
- end
-
- def milestone
- return unless raw['milestone'].present?
-
- @milestone ||= Github::Representation::Milestone.new(raw['milestone'])
- end
-
- def author
- @author ||= Github::Representation::User.new(raw['user'], options)
- end
-
- def labels?
- raw['labels'].any?
- end
-
- def labels
- @labels ||= Array(raw['labels']).map do |label|
- Github::Representation::Label.new(label, options)
- end
- end
- end
- end
-end
diff --git a/lib/github/representation/issue.rb b/lib/github/representation/issue.rb
deleted file mode 100644
index 4f1a02cb90f..00000000000
--- a/lib/github/representation/issue.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Github
- module Representation
- class Issue < Representation::Issuable
- def state
- raw['state'] == 'closed' ? 'closed' : 'opened'
- end
-
- def comments?
- raw['comments'] > 0
- end
-
- def pull_request?
- raw['pull_request'].present?
- end
-
- def assigned?
- raw['assignees'].present?
- end
-
- def assignees
- @assignees ||= Array(raw['assignees']).map do |user|
- Github::Representation::User.new(user, options)
- end
- end
- end
- end
-end
diff --git a/lib/github/representation/label.rb b/lib/github/representation/label.rb
deleted file mode 100644
index 60aa51f9569..00000000000
--- a/lib/github/representation/label.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- module Representation
- class Label < Representation::Base
- def color
- "##{raw['color']}"
- end
-
- def title
- raw['name']
- end
- end
- end
-end
diff --git a/lib/github/representation/milestone.rb b/lib/github/representation/milestone.rb
deleted file mode 100644
index 917e6394ad4..00000000000
--- a/lib/github/representation/milestone.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Github
- module Representation
- class Milestone < Representation::Base
- def iid
- raw['number']
- end
-
- def title
- raw['title']
- end
-
- def description
- raw['description']
- end
-
- def due_date
- raw['due_on']
- end
-
- def state
- raw['state'] == 'closed' ? 'closed' : 'active'
- end
- end
- end
-end
diff --git a/lib/github/representation/pull_request.rb b/lib/github/representation/pull_request.rb
deleted file mode 100644
index 0171179bb0f..00000000000
--- a/lib/github/representation/pull_request.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-module Github
- module Representation
- class PullRequest < Representation::Issuable
- delegate :sha, to: :source_branch, prefix: true
- delegate :sha, to: :target_branch, prefix: true
-
- def source_project
- project
- end
-
- def source_branch_name
- # Mimic the "user:branch" displayed in the MR widget,
- # i.e. "Request to merge rymai:add-external-mounts into master"
- cross_project? ? "#{source_branch.user}:#{source_branch.ref}" : source_branch.ref
- end
-
- def target_project
- project
- end
-
- def target_branch_name
- target_branch.ref
- end
-
- def state
- return 'merged' if raw['state'] == 'closed' && raw['merged_at'].present?
- return 'closed' if raw['state'] == 'closed'
-
- 'opened'
- end
-
- def opened?
- state == 'opened'
- end
-
- def valid?
- source_branch.valid? && target_branch.valid?
- end
-
- def assigned?
- raw['assignee'].present?
- end
-
- def assignee
- return unless assigned?
-
- @assignee ||= Github::Representation::User.new(raw['assignee'], options)
- end
-
- private
-
- def project
- @project ||= options.fetch(:project)
- end
-
- def source_branch
- @source_branch ||= Representation::Branch.new(raw['head'], repository: project.repository)
- end
-
- def target_branch
- @target_branch ||= Representation::Branch.new(raw['base'], repository: project.repository)
- end
-
- def cross_project?
- return true unless source_branch.repo?
-
- source_branch.repo.id != target_branch.repo.id
- end
- end
- end
-end
diff --git a/lib/github/representation/release.rb b/lib/github/representation/release.rb
deleted file mode 100644
index e7e4b428c1a..00000000000
--- a/lib/github/representation/release.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Github
- module Representation
- class Release < Representation::Base
- def description
- raw['body']
- end
-
- def tag
- raw['tag_name']
- end
-
- def valid?
- !raw['draft']
- end
- end
- end
-end
diff --git a/lib/github/representation/repo.rb b/lib/github/representation/repo.rb
deleted file mode 100644
index 6938aa7db05..00000000000
--- a/lib/github/representation/repo.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Github
- module Representation
- class Repo < Representation::Base
- end
- end
-end
diff --git a/lib/github/representation/user.rb b/lib/github/representation/user.rb
deleted file mode 100644
index 18591380e25..00000000000
--- a/lib/github/representation/user.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Github
- module Representation
- class User < Representation::Base
- def email
- return @email if defined?(@email)
-
- @email = Github::User.new(username, options).get.fetch('email', nil)
- end
-
- def username
- raw['login']
- end
- end
- end
-end
diff --git a/lib/github/response.rb b/lib/github/response.rb
deleted file mode 100644
index 761c524b553..00000000000
--- a/lib/github/response.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Github
- class Response
- attr_reader :raw, :headers, :status
-
- def initialize(response)
- @raw = response
- @headers = response.headers
- @status = response.status
- end
-
- def body
- Oj.load(raw.body, class_cache: false, mode: :compat)
- end
-
- def rels
- links = headers['Link'].to_s.split(', ').map do |link|
- href, name = link.match(/<(.*?)>; rel="(\w+)"/).captures
-
- [name.to_sym, href]
- end
-
- Hash[*links.flatten]
- end
- end
-end
diff --git a/lib/github/user.rb b/lib/github/user.rb
deleted file mode 100644
index f88a29e590b..00000000000
--- a/lib/github/user.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module Github
- class User
- attr_reader :username, :options
-
- def initialize(username, options)
- @username = username
- @options = options
- end
-
- def get
- client.get(user_url).body
- end
-
- private
-
- def client
- @client ||= Github::Client.new(options)
- end
-
- def user_url
- "/users/#{username}"
- end
- end
-end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index b4012ebbb99..7127948cf00 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -58,9 +58,9 @@ module Gitlab
def protection_options
{
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
- "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch." => PROTECTION_DEV_CAN_MERGE,
- "Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH,
- "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL
+ "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Masters can push to the branch." => PROTECTION_DEV_CAN_MERGE,
+ "Partially protected: Both developers and masters can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH,
+ "Fully protected: Developers cannot push new commits, but masters can. No-one can force push or delete the branch." => PROTECTION_FULL
}
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 87aeb76b66a..cbbc51db99e 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,11 +1,11 @@
module Gitlab
module Auth
- MissingPersonalTokenError = Class.new(StandardError)
+ MissingPersonalAccessTokenError = Class.new(StandardError)
REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
- API_SCOPES = [:api, :read_user].freeze
+ API_SCOPES = [:api, :read_user, :sudo].freeze
# Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze
@@ -25,7 +25,7 @@ module Gitlab
result =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
- lfs_token_check(login, password) ||
+ lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(password) ||
user_with_password_for_git(login, password) ||
@@ -38,7 +38,7 @@ module Gitlab
# If sign-in is disabled and LDAP is not configured, recommend a
# personal access token on failed auth attempts
- raise Gitlab::Auth::MissingPersonalTokenError
+ raise Gitlab::Auth::MissingPersonalAccessTokenError
end
def find_with_user_password(login, password)
@@ -106,7 +106,7 @@ module Gitlab
user = find_with_user_password(login, password)
return unless user
- raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
+ raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
@@ -128,7 +128,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
if token && valid_scoped_token?(token, available_scopes)
- Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes))
+ Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scope(token.scopes))
end
end
@@ -146,7 +146,7 @@ module Gitlab
end.flatten.uniq
end
- def lfs_token_check(login, password)
+ def lfs_token_check(login, password, project)
deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
actor =
@@ -163,6 +163,8 @@ module Gitlab
authentication_abilities =
if token_handler.user?
full_authentication_abilities
+ elsif token_handler.deploy_key_pushable?(project)
+ read_write_authentication_abilities
else
read_authentication_abilities
end
@@ -208,10 +210,15 @@ module Gitlab
]
end
- def full_authentication_abilities
+ def read_write_authentication_abilities
read_authentication_abilities + [
:push_code,
- :create_container_image,
+ :create_container_image
+ ]
+ end
+
+ def full_authentication_abilities
+ read_write_authentication_abilities + [
:admin_container_image
]
end
@@ -226,8 +233,10 @@ module Gitlab
[]
end
- def available_scopes
- API_SCOPES + registry_scopes
+ def available_scopes(current_user = nil)
+ scopes = API_SCOPES + registry_scopes
+ scopes.delete(:sudo) if current_user && !current_user.admin?
+ scopes
end
# Other available scopes
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
new file mode 100644
index 00000000000..46ec040ce92
--- /dev/null
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -0,0 +1,25 @@
+# Use for authentication only, in particular for Rack::Attack.
+# Does not perform authorization of scopes, etc.
+module Gitlab
+ module Auth
+ class RequestAuthenticator
+ include UserAuthFinders
+
+ attr_reader :request
+
+ def initialize(request)
+ @request = request
+ end
+
+ def user
+ find_sessionless_user || find_user_from_warden
+ end
+
+ def find_sessionless_user
+ find_user_from_access_token || find_user_from_rss_token
+ rescue Gitlab::Auth::AuthenticationError
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
new file mode 100644
index 00000000000..b4114a3ac96
--- /dev/null
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -0,0 +1,109 @@
+module Gitlab
+ module Auth
+ #
+ # Exceptions
+ #
+
+ AuthenticationError = Class.new(StandardError)
+ MissingTokenError = Class.new(AuthenticationError)
+ TokenNotFoundError = Class.new(AuthenticationError)
+ ExpiredError = Class.new(AuthenticationError)
+ RevokedError = Class.new(AuthenticationError)
+ UnauthorizedError = Class.new(AuthenticationError)
+
+ class InsufficientScopeError < AuthenticationError
+ attr_reader :scopes
+ def initialize(scopes)
+ @scopes = scopes.map { |s| s.try(:name) || s }
+ end
+ end
+
+ module UserAuthFinders
+ include Gitlab::Utils::StrongMemoize
+
+ PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'.freeze
+ PRIVATE_TOKEN_PARAM = :private_token
+
+ # Check the Rails session for valid authentication details
+ def find_user_from_warden
+ current_request.env['warden']&.authenticate if verified_request?
+ end
+
+ def find_user_from_rss_token
+ return unless current_request.path.ends_with?('.atom') || current_request.format.atom?
+
+ token = current_request.params[:rss_token].presence
+ return unless token
+
+ User.find_by_rss_token(token) || raise(UnauthorizedError)
+ end
+
+ def find_user_from_access_token
+ return unless access_token
+
+ validate_access_token!
+
+ access_token.user || raise(UnauthorizedError)
+ end
+
+ def validate_access_token!(scopes: [])
+ return unless access_token
+
+ case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
+ when AccessTokenValidationService::INSUFFICIENT_SCOPE
+ raise InsufficientScopeError.new(scopes)
+ when AccessTokenValidationService::EXPIRED
+ raise ExpiredError
+ when AccessTokenValidationService::REVOKED
+ raise RevokedError
+ end
+ end
+
+ private
+
+ def access_token
+ strong_memoize(:access_token) do
+ find_oauth_access_token || find_personal_access_token
+ end
+ end
+
+ def find_personal_access_token
+ token =
+ current_request.params[PRIVATE_TOKEN_PARAM].presence ||
+ current_request.env[PRIVATE_TOKEN_HEADER].presence
+
+ return unless token
+
+ # Expiration, revocation and scopes are verified in `validate_access_token!`
+ PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError)
+ end
+
+ def find_oauth_access_token
+ token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
+ return unless token
+
+ # Expiration, revocation and scopes are verified in `validate_access_token!`
+ oauth_token = OauthAccessToken.by_token(token)
+ raise UnauthorizedError unless oauth_token
+
+ oauth_token.revoke_previous_refresh_token!
+ oauth_token
+ end
+
+ # Check if the request is GET/HEAD, or if CSRF token is valid.
+ def verified_request?
+ Gitlab::RequestForgeryProtection.verified?(current_request.env)
+ end
+
+ def ensure_action_dispatch_request(request)
+ return request if request.is_a?(ActionDispatch::Request)
+
+ ActionDispatch::Request.new(request.env)
+ end
+
+ def current_request
+ @current_request ||= ensure_action_dispatch_request(request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
index c88eb9783ed..67a39d28944 100644
--- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -51,10 +51,20 @@ module Gitlab
FROM projects
WHERE forked_project_links.forked_from_project_id = projects.id
)
+ AND NOT EXISTS (
+ SELECT true
+ FROM forked_project_links AS parent_links
+ WHERE parent_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ AND NOT EXISTS (
+ SELECT true
+ FROM projects
+ WHERE parent_links.forked_from_project_id = projects.id
+ )
+ )
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
MISSING_MEMBERS
- ForkNetworkMember.count_by_sql(count_sql) > 0
+ ForkedProjectLink.count_by_sql(count_sql) > 0
end
def log(message)
diff --git a/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb
new file mode 100644
index 00000000000..7e109e96e73
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module BackgroundMigration
+ class PopulateMergeRequestsLatestMergeRequestDiffId
+ BATCH_SIZE = 1_000
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ def perform(start_id, stop_id)
+ update = '
+ latest_merge_request_diff_id = (
+ SELECT MAX(id)
+ FROM merge_request_diffs
+ WHERE merge_requests.id = merge_request_diffs.merge_request_id
+ )'.squish
+
+ MergeRequest
+ .where(id: start_id..stop_id)
+ .where(latest_merge_request_diff_id: nil)
+ .each_batch(of: BATCH_SIZE) do |relation|
+
+ relation.update_all(update)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb
new file mode 100644
index 00000000000..196de667805
--- /dev/null
+++ b/lib/gitlab/bare_repository_import/importer.rb
@@ -0,0 +1,101 @@
+module Gitlab
+ module BareRepositoryImport
+ class Importer
+ NoAdminError = Class.new(StandardError)
+
+ def self.execute(import_path)
+ import_path << '/' unless import_path.ends_with?('/')
+ repos_to_import = Dir.glob(import_path + '**/*.git')
+
+ unless user = User.admins.order_id_asc.first
+ raise NoAdminError.new('No admin user found to import repositories')
+ end
+
+ repos_to_import.each do |repo_path|
+ bare_repo = Gitlab::BareRepositoryImport::Repository.new(import_path, repo_path)
+
+ if bare_repo.hashed? || bare_repo.wiki?
+ log " * Skipping repo #{bare_repo.repo_path}".color(:yellow)
+
+ next
+ end
+
+ log "Processing #{repo_path}".color(:yellow)
+
+ new(user, bare_repo).create_project_if_needed
+ end
+ end
+
+ attr_reader :user, :project_name, :bare_repo
+
+ delegate :log, to: :class
+ delegate :project_name, :project_full_path, :group_path, :repo_path, :wiki_path, to: :bare_repo
+
+ def initialize(user, bare_repo)
+ @user = user
+ @bare_repo = bare_repo
+ end
+
+ def create_project_if_needed
+ if project = Project.find_by_full_path(project_full_path)
+ log " * #{project.name} (#{project_full_path}) exists"
+
+ return project
+ end
+
+ create_project
+ end
+
+ private
+
+ def create_project
+ group = find_or_create_groups
+
+ project = Projects::CreateService.new(user,
+ name: project_name,
+ path: project_name,
+ skip_disk_validation: true,
+ namespace_id: group&.id).execute
+
+ if project.persisted? && mv_repo(project)
+ log " * Created #{project.name} (#{project_full_path})".color(:green)
+
+ ProjectCacheWorker.perform_async(project.id)
+ else
+ log " * Failed trying to create #{project.name} (#{project_full_path})".color(:red)
+ log " Errors: #{project.errors.messages}".color(:red) if project.errors.any?
+ end
+
+ project
+ end
+
+ def mv_repo(project)
+ FileUtils.mv(repo_path, File.join(project.repository_storage_path, project.disk_path + '.git'))
+
+ if bare_repo.wiki_exists?
+ FileUtils.mv(wiki_path, File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ end
+
+ true
+ rescue => e
+ log " * Failed to move repo: #{e.message}".color(:red)
+
+ false
+ end
+
+ def find_or_create_groups
+ return nil unless group_path.present?
+
+ log " * Using namespace: #{group_path}"
+
+ Groups::NestedCreateService.new(user, group_path: group_path).execute
+ end
+
+ # This is called from within a rake task only used by Admins, so allow writing
+ # to STDOUT
+ def self.log(message)
+ puts message # rubocop:disable Rails/Output
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_import/repository.rb b/lib/gitlab/bare_repository_import/repository.rb
new file mode 100644
index 00000000000..8574ac6eb30
--- /dev/null
+++ b/lib/gitlab/bare_repository_import/repository.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module BareRepositoryImport
+ class Repository
+ attr_reader :group_path, :project_name, :repo_path
+
+ def initialize(root_path, repo_path)
+ @root_path = root_path
+ @repo_path = repo_path
+
+ # Split path into 'all/the/namespaces' and 'project_name'
+ @group_path, _, @project_name = repo_relative_path.rpartition('/')
+ end
+
+ def wiki_exists?
+ File.exist?(wiki_path)
+ end
+
+ def wiki?
+ @wiki ||= repo_path.end_with?('.wiki.git')
+ end
+
+ def wiki_path
+ @wiki_path ||= repo_path.sub(/\.git$/, '.wiki.git')
+ end
+
+ def hashed?
+ @hashed ||= group_path.start_with?('@hashed')
+ end
+
+ def project_full_path
+ @project_full_path ||= "#{group_path}/#{project_name}"
+ end
+
+ private
+
+ def repo_relative_path
+ # Remove root path and `.git` at the end
+ repo_path[@root_path.size...-4]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_importer.rb b/lib/gitlab/bare_repository_importer.rb
deleted file mode 100644
index 1d98d187805..00000000000
--- a/lib/gitlab/bare_repository_importer.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-module Gitlab
- class BareRepositoryImporter
- NoAdminError = Class.new(StandardError)
-
- def self.execute
- Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
- git_base_path = repository_storage['path']
- repos_to_import = Dir.glob(git_base_path + '/**/*.git')
-
- repos_to_import.each do |repo_path|
- if repo_path.end_with?('.wiki.git')
- log " * Skipping wiki repo"
- next
- end
-
- log "Processing #{repo_path}".color(:yellow)
-
- repo_relative_path = repo_path[repository_storage['path'].length..-1]
- .sub(/^\//, '') # Remove leading `/`
- .sub(/\.git$/, '') # Remove `.git` at the end
- new(storage_name, repo_relative_path).create_project_if_needed
- end
- end
- end
-
- attr_reader :storage_name, :full_path, :group_path, :project_path, :user
- delegate :log, to: :class
-
- def initialize(storage_name, repo_path)
- @storage_name = storage_name
- @full_path = repo_path
-
- unless @user = User.admins.order_id_asc.first
- raise NoAdminError.new('No admin user found to import repositories')
- end
-
- @group_path, @project_path = File.split(repo_path)
- @group_path = nil if @group_path == '.'
- end
-
- def create_project_if_needed
- if project = Project.find_by_full_path(full_path)
- log " * #{project.name} (#{full_path}) exists"
- return project
- end
-
- create_project
- end
-
- private
-
- def create_project
- group = find_or_create_group
-
- project_params = {
- name: project_path,
- path: project_path,
- repository_storage: storage_name,
- namespace_id: group&.id,
- skip_disk_validation: true
- }
-
- project = Projects::CreateService.new(user, project_params).execute
-
- if project.persisted?
- log " * Created #{project.name} (#{full_path})".color(:green)
- ProjectCacheWorker.perform_async(project.id)
- else
- log " * Failed trying to create #{project.name} (#{full_path})".color(:red)
- log " Errors: #{project.errors.messages}".color(:red)
- end
-
- project
- end
-
- def find_or_create_group
- return nil unless group_path
-
- if namespace = Namespace.find_by_full_path(group_path)
- log " * Namespace #{group_path} exists.".color(:green)
- return namespace
- end
-
- log " * Creating Group: #{group_path}"
- Groups::NestedCreateService.new(user, group_path: group_path).execute
- end
-
- # This is called from within a rake task only used by Admins, so allow writing
- # to STDOUT
- #
- # rubocop:disable Rails/Output
- def self.log(message)
- puts message
- end
- # rubocop:enable Rails/Output
- end
-end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 033ecd15749..d48ae17aeaf 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -61,9 +61,9 @@ module Gitlab
def import_wiki
return if project.wiki.repository_exists?
- path_with_namespace = "#{project.full_path}.wiki"
+ disk_path = project.wiki.disk_path
import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
- gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url)
+ gitlab_shell.import_repository(project.repository_storage_path, disk_path, import_url)
rescue StandardError => e
errors << { type: :wiki, errors: e.message }
end
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
index 5b32fca00a4..9c9e6668e6f 100644
--- a/lib/gitlab/changes_list.rb
+++ b/lib/gitlab/changes_list.rb
@@ -16,6 +16,7 @@ module Gitlab
@changes ||= begin
@raw_changes.map do |change|
next if change.blank?
+
oldrev, newrev, ref = change.strip.split(' ')
{ oldrev: oldrev, newrev: newrev, ref: ref }
end.compact
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index b6805230348..ef92fc5a0a0 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -12,7 +12,8 @@ module Gitlab
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
delete_protected_tag: 'Protected tags cannot be deleted.',
- create_protected_tag: 'You are not allowed to create this tag as it is protected.'
+ create_protected_tag: 'You are not allowed to create this tag as it is protected.',
+ lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
attr_reader :user_access, :project, :skip_authorization, :protocol
@@ -36,6 +37,7 @@ module Gitlab
push_checks
branch_checks
tag_checks
+ lfs_objects_exist_check
true
end
@@ -136,6 +138,14 @@ module Gitlab
def matching_merge_request?
Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
end
+
+ def lfs_objects_exist_check
+ lfs_check = Checks::LfsIntegrity.new(project, @newrev)
+
+ if lfs_check.objects_missing?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ end
+ end
end
end
end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
new file mode 100644
index 00000000000..f7276a380dc
--- /dev/null
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Checks
+ class LfsIntegrity
+ REV_LIST_OBJECT_LIMIT = 2_000
+
+ def initialize(project, newrev)
+ @project = project
+ @newrev = newrev
+ end
+
+ def objects_missing?
+ return false unless @newrev && @project.lfs_enabled?
+
+ new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev).new_pointers(object_limit: REV_LIST_OBJECT_LIMIT)
+
+ return false unless new_lfs_pointers.present?
+
+ existing_count = @project.lfs_storage_project
+ .lfs_objects
+ .where(oid: new_lfs_pointers.map(&:lfs_oid))
+ .count
+
+ existing_count != new_lfs_pointers.count
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index a788fb3fcbc..0bbd60d8ffe 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -98,6 +98,7 @@ module Gitlab
def read_string(gz)
string_size = read_uint32(gz)
return nil unless string_size
+
gz.read(string_size)
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 22941d48edf..5b2f09e03ea 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -43,6 +43,7 @@ module Gitlab
def parent
return nil unless has_parent?
+
self.class.new(@path.to_s.chomp(basename), @entries)
end
@@ -64,6 +65,7 @@ module Gitlab
def directories(opts = {})
return [] unless directory?
+
dirs = children.select(&:directory?)
return dirs unless has_parent? && opts[:parent]
@@ -74,6 +76,7 @@ module Gitlab
def files
return [] unless directory?
+
children.select(&:file?)
end
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
index b88b2e36d53..c811f88f483 100644
--- a/lib/gitlab/ci/build/image.rb
+++ b/lib/gitlab/ci/build/image.rb
@@ -8,6 +8,7 @@ module Gitlab
def from_image(job)
image = Gitlab::Ci::Build::Image.new(job.options[:image])
return unless image.valid?
+
image
end
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 6555c589173..2844be80a84 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -37,6 +37,7 @@ module Gitlab
def value
return { name: @config } if string?
return @config if hash?
+
{}
end
end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 0159179f0a9..eb606b57667 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -111,6 +111,7 @@ module Gitlab
def validate_string_or_regexp(value)
return false unless value.is_a?(String)
return validate_regexp(value) if look_like_regexp?(value)
+
true
end
end
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 8ad3e57e59d..2d9166d6bdd 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def action_icon
- 'icon_action_cancel'
+ 'cancel'
end
def action_path
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
index e42d3574357..dc90f398c7e 100644
--- a/lib/gitlab/ci/status/build/failed_allowed.rb
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def icon
- 'icon_status_warning'
+ 'status_warning'
end
def group
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index c7726543599..b7b45466d3b 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def action_icon
- 'icon_action_play'
+ 'play'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 8c8fdc56d75..44ffe783e50 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def action_icon
- 'icon_action_retry'
+ 'retry'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index d464738deaf..46e730797e4 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def action_icon
- 'icon_action_stop'
+ 'stop'
end
def action_title
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index e5fdc1f8136..e6195a60d4f 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_canceled'
+ 'status_canceled'
end
def favicon
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index d188bd286a6..846f00b83dd 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_created'
+ 'status_created'
end
def favicon
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index 38e45714c22..27ce85bd3ed 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_failed'
+ 'status_failed'
end
def favicon
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index a4a7edadac9..fc387e2fd25 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_manual'
+ 'status_manual'
end
def favicon
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index 5164260b861..6780780db32 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_pending'
+ 'status_pending'
end
def favicon
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index 993937e98ca..ee13905e46d 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_running'
+ 'status_running'
end
def favicon
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 0c942920b02..0dbdc4de426 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_skipped'
+ 'status_skipped'
end
def favicon
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index d7af98857b0..731013ec017 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_success'
+ 'status_success'
end
def favicon
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 4d7d82e04cf..32b4cf43e48 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def icon
- 'icon_status_warning'
+ 'status_warning'
end
def group
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index dfd17e35707..633de9f9776 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -2,6 +2,7 @@ module Gitlab
class Daemon
def self.initialize_instance(*args)
raise "#{name} singleton instance already initialized" if @instance
+
@instance = new(*args)
Kernel.at_exit(&@instance.method(:stop))
@instance
@@ -43,7 +44,7 @@ module Gitlab
if thread
thread.wakeup if thread.alive?
- thread.join
+ thread.join unless Thread.current == thread
@thread = nil
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 357f16936c6..cd7b4c043da 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -4,6 +4,10 @@ module Gitlab
# https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
MAX_INT_VALUE = 2147483647
+ # The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz:
+ # https://www.postgresql.org/docs/9.1/static/datatype-datetime.html
+ # https://dev.mysql.com/doc/refman/5.7/en/datetime.html
+ MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze
def self.config
ActiveRecord::Base.configurations[Rails.env]
@@ -104,20 +108,45 @@ module Gitlab
end
end
- def self.bulk_insert(table, rows)
+ # Bulk inserts a number of rows into a table, optionally returning their
+ # IDs.
+ #
+ # table - The name of the table to insert the rows into.
+ # rows - An Array of Hash instances, each mapping the columns to their
+ # values.
+ # return_ids - When set to true the return value will be an Array of IDs of
+ # the inserted rows, this only works on PostgreSQL.
+ def self.bulk_insert(table, rows, return_ids: false)
return if rows.empty?
keys = rows.first.keys
columns = keys.map { |key| connection.quote_column_name(key) }
+ return_ids = false if mysql?
tuples = rows.map do |row|
row.values_at(*keys).map { |value| connection.quote(value) }
end
- connection.execute <<-EOF
+ sql = <<-EOF
INSERT INTO #{table} (#{columns.join(', ')})
VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
EOF
+
+ if return_ids
+ sql << 'RETURNING id'
+ end
+
+ result = connection.execute(sql)
+
+ if return_ids
+ result.values.map { |tuple| tuple[0].to_i }
+ else
+ []
+ end
+ end
+
+ def self.sanitize_timestamp(timestamp)
+ MAX_TIMESTAMP_VALUE > timestamp ? timestamp : MAX_TIMESTAMP_VALUE.dup
end
# pool_size - The size of the DB pool.
diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb
index aee3981e79a..9f76967fc77 100644
--- a/lib/gitlab/database/grant.rb
+++ b/lib/gitlab/database/grant.rb
@@ -6,28 +6,36 @@ module Gitlab
if Database.postgresql?
'information_schema.role_table_grants'
else
- 'mysql.user'
+ 'information_schema.schema_privileges'
end
- def self.scope_to_current_user
- if Database.postgresql?
- where('grantee = user')
- else
- where("CONCAT(User, '@', Host) = current_user()")
- end
- end
-
# Returns true if the current user can create and execute triggers on the
# given table.
def self.create_and_execute_trigger?(table)
priv =
if Database.postgresql?
where(privilege_type: 'TRIGGER', table_name: table)
+ .where('grantee = user')
else
- where(Trigger_priv: 'Y')
+ queries = [
+ Grant.select(1)
+ .from('information_schema.user_privileges')
+ .where("PRIVILEGE_TYPE = 'SUPER'")
+ .where("GRANTEE = CONCAT('\\'', REPLACE(CURRENT_USER(), '@', '\\'@\\''), '\\'')"),
+
+ Grant.select(1)
+ .from('information_schema.schema_privileges')
+ .where("PRIVILEGE_TYPE = 'TRIGGER'")
+ .where('TABLE_SCHEMA = ?', Gitlab::Database.database_name)
+ .where("GRANTEE = CONCAT('\\'', REPLACE(CURRENT_USER(), '@', '\\'@\\''), '\\'')")
+ ]
+
+ union = SQL::Union.new(queries).to_sql
+
+ Grant.from("(#{union}) privs")
end
- priv.scope_to_current_user.any?
+ priv.any?
end
end
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 2c35da8f1aa..c276c3566b4 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -220,6 +220,15 @@ module Gitlab
# column - The name of the column to update.
# value - The value for the column.
#
+ # The `value` argument is typically a literal. To perform a computed
+ # update, an Arel literal can be used instead:
+ #
+ # update_value = Arel.sql('bar * baz')
+ #
+ # update_column_in_batches(:projects, :foo, update_value) do |table, query|
+ # query.where(table[:some_column].eq('hello'))
+ # end
+ #
# Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop
# determines this method to be too complex while there's no way to make it
# less "complex" without introducing extra methods (which actually will
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
index 5481024db8e..7e492938eac 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
@@ -68,6 +68,11 @@ module Gitlab
has_one :route, as: :source
self.table_name = 'projects'
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
+
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]['path']
end
@@ -76,6 +81,13 @@ module Gitlab
def self.name
'Project'
end
+
+ def hashed_storage?(feature)
+ raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
+ return false unless respond_to?(:storage_version)
+
+ self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
+ end
end
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
index 75a75f61953..d32616862f0 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
@@ -22,9 +22,11 @@ module Gitlab
end
def move_project_folders(project, old_full_path, new_full_path)
- move_repository(project, old_full_path, new_full_path)
- move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
- move_uploads(old_full_path, new_full_path)
+ unless project.hashed_storage?(:repository)
+ move_repository(project, old_full_path, new_full_path)
+ move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
+ end
+ move_uploads(old_full_path, new_full_path) unless project.hashed_storage?(:attachments)
move_pages(old_full_path, new_full_path)
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index ea5891a028a..d0cfe2386ca 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -25,6 +25,10 @@ module Gitlab
@repository = repository
@diff_refs = diff_refs
@fallback_diff_refs = fallback_diff_refs
+
+ # Ensure items are collected in the the batch
+ new_blob
+ old_blob
end
def position(position_marker, position_type: :text)
@@ -95,21 +99,15 @@ module Gitlab
end
def new_blob
- return @new_blob if defined?(@new_blob)
-
- sha = new_content_sha
- return @new_blob = nil unless sha
+ return unless new_content_sha
- @new_blob = repository.blob_at(sha, file_path)
+ Blob.lazy(repository.project, new_content_sha, file_path)
end
def old_blob
- return @old_blob if defined?(@old_blob)
-
- sha = old_content_sha
- return @old_blob = nil unless sha
+ return unless old_content_sha
- @old_blob = repository.blob_at(sha, old_path)
+ Blob.lazy(repository.project, old_content_sha, old_path)
end
def content_sha
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index 88ae65cb468..a6007ebf531 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -22,10 +22,7 @@ module Gitlab
end
def diff_files
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37445
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
- end
+ @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
end
def diff_file_with_old_path(old_path)
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 55708d42161..2d7b57120a6 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -102,6 +102,7 @@ module Gitlab
new_char = b[pos]
break if old_char != new_char
+
length += 1
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 7dc9cc7c281..8302f30a0a2 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -30,6 +30,7 @@ module Gitlab
line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0
next if line_old <= 1 && line_new <= 1 # top of file
+
yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
line_obj_index += 1
next
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index ccfb908bcca..690b27cde81 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -125,6 +125,7 @@ module Gitlab
def find_diff_file(repository)
return unless diff_refs.complete?
return unless comparison = diff_refs.compare_in(repository.project)
+
comparison.diffs(paths: paths, expanded: true).diff_files.first
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c4c60d1dfee..efc2e46d289 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,8 +2,8 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- DEFAULT_CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
- EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+ DEFAULT_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
+ EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
PLEASE_READ_THIS_BANNER = %Q{
@@ -17,14 +17,16 @@ module Gitlab
============================================================\n
}.freeze
- attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
- attr_reader :failed_files
+ attr_reader :ee_repo_dir, :patches_dir, :ce_project_url, :ce_repo_url, :ce_branch, :ee_branch_found
+ attr_reader :job_id, :failed_files
- def initialize(branch:, ce_repo: DEFAULT_CE_REPO)
+ def initialize(branch:, ce_project_url: DEFAULT_CE_PROJECT_URL, job_id: nil)
@ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
- @ce_repo = ce_repo
+ @ce_project_url = ce_project_url
+ @ce_repo_url = "#{ce_project_url}.git"
+ @job_id = job_id
end
def check
@@ -59,8 +61,8 @@ module Gitlab
step("#{ee_repo_dir} already exists")
else
step(
- "Cloning #{EE_REPO} into #{ee_repo_dir}",
- %W[git clone --branch master --single-branch --depth=200 #{EE_REPO} #{ee_repo_dir}]
+ "Cloning #{EE_REPO_URL} into #{ee_repo_dir}",
+ %W[git clone --branch master --single-branch --depth=200 #{EE_REPO_URL} #{ee_repo_dir}]
)
end
end
@@ -132,7 +134,7 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
step("Resetting to latest master", %w[git reset --hard origin/master])
- step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo} #{ce_branch}])
+ step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo_url} #{ce_branch}])
step(
"Checking if #{patch_path} applies cleanly to EE/master",
# Don't use --check here because it can result in a 0-exit status even
@@ -191,7 +193,7 @@ module Gitlab
# Repository is initially cloned with a depth of 20 so we need to fetch
# deeper in the case the branch has more than 20 commits on top of master
fetch(branch: branch, depth: depth)
- fetch(branch: 'master', depth: depth)
+ fetch(branch: 'master', depth: depth, remote: DEFAULT_CE_PROJECT_URL)
merge_base_found?
end
@@ -199,10 +201,10 @@ module Gitlab
raise "\n#{branch} is too far behind master, please rebase it!\n" unless success
end
- def fetch(branch:, depth:)
+ def fetch(branch:, depth:, remote: 'origin')
step(
"Fetching deeper...",
- %W[git fetch --depth=#{depth} --prune origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}]
+ %W[git fetch --depth=#{depth} --prune #{remote} +refs/heads/#{branch}:refs/remotes/origin/#{branch}]
) do |output, status|
raise "Fetch failed: #{output}" unless status.zero?
end
@@ -237,7 +239,7 @@ module Gitlab
end
def patch_url
- "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/#{ENV['CI_JOB_ID']}/artifacts/raw/ee_compat_check/patches/#{ce_patch_name}"
+ "#{ce_project_url}/-/jobs/#{job_id}/artifacts/raw/ee_compat_check/patches/#{ce_patch_name}"
end
def step(desc, cmd = nil)
@@ -304,7 +306,7 @@ module Gitlab
# In the EE repo
$ git fetch origin
$ git checkout -b #{ee_branch_prefix} origin/master
- $ git fetch #{ce_repo} #{ce_branch}
+ $ git fetch #{ce_repo_url} #{ce_branch}
$ git cherry-pick SHA # Repeat for all the commits you want to pick
You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb
index 5894384da5d..ea80e21532e 100644
--- a/lib/gitlab/email/handler/unsubscribe_handler.rb
+++ b/lib/gitlab/email/handler/unsubscribe_handler.rb
@@ -16,6 +16,7 @@ module Gitlab
noteable = sent_notification.noteable
raise NoteableNotFoundError unless noteable
+
noteable.unsubscribe(sent_notification.recipient)
end
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
index 2152182b37f..acb000e3e23 100644
--- a/lib/gitlab/fogbugz_import/client.rb
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -45,6 +45,7 @@ module Gitlab
project_name = repo(project_id).name
res = @api.command(:search, q: "project:'#{project_name}'", cols: 'ixPersonAssignedTo,ixPersonOpenedBy,ixPersonClosedBy,sStatus,sPriority,sCategory,fOpen,sTitle,sLatestTextSummary,dtOpened,dtClosed,dtResolved,dtLastUpdated,events')
return [] unless res['cases']['count'].to_i > 0
+
res['cases']['case']
end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 3dcee681c72..5e426b13ade 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -18,6 +18,7 @@ module Gitlab
def execute
return true unless repo.valid?
+
client = Gitlab::FogbugzImport::Client.new(token: fb_session[:token], uri: fb_session[:uri])
@cases = client.cases(@repo.id.to_i)
@@ -206,6 +207,7 @@ module Gitlab
def format_content(raw_content)
return raw_content if raw_content.nil?
+
linkify_issues(escape_for_markdown(raw_content))
end
diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb
deleted file mode 100644
index 195391f0e3c..00000000000
--- a/lib/gitlab/gcp/model.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module Gcp
- module Model
- def table_name_prefix
- "gcp_"
- end
-
- def model_name
- @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
- end
- end
- end
-end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index a4336facee5..ddd52136bc4 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -12,6 +12,12 @@ module Gitlab
# blob data should use load_all_data!.
MAX_DATA_DISPLAY_SIZE = 10.megabytes
+ # These limits are used as a heuristic to ignore files which can't be LFS
+ # pointers. The format of these is described in
+ # https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer
+ LFS_POINTER_MIN_SIZE = 120.bytes
+ LFS_POINTER_MAX_SIZE = 200.bytes
+
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
class << self
@@ -30,16 +36,7 @@ module Gitlab
if is_enabled
Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
else
- blob = repository.lookup(sha)
-
- next unless blob.is_a?(Rugged::Blob)
-
- new(
- id: blob.oid,
- size: blob.size,
- data: blob.content(MAX_DATA_DISPLAY_SIZE),
- binary: blob.binary?
- )
+ rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE)
end
end
end
@@ -59,10 +56,25 @@ module Gitlab
end
end
+ # Find LFS blobs given an array of sha ids
+ # Returns array of Gitlab::Git::Blob
+ # Does not guarantee blob data will be set
+ def batch_lfs_pointers(repository, blob_ids)
+ blob_ids.lazy
+ .select { |sha| possible_lfs_blob?(repository, sha) }
+ .map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) }
+ .select(&:lfs_pointer?)
+ .force
+ end
+
def binary?(data)
EncodingHelper.detect_libgit2_binary?(data)
end
+ def size_could_be_lfs?(size)
+ size.between?(LFS_POINTER_MIN_SIZE, LFS_POINTER_MAX_SIZE)
+ end
+
private
# Recursive search of blob id by path
@@ -90,6 +102,7 @@ module Gitlab
if path_arr.size > 1
return nil unless entry[:type] == :tree
+
path_arr.shift
find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
else
@@ -166,6 +179,31 @@ module Gitlab
)
end
end
+ rescue Rugged::ReferenceError
+ nil
+ end
+
+ def rugged_raw(repository, sha, limit:)
+ blob = repository.lookup(sha)
+
+ return unless blob.is_a?(Rugged::Blob)
+
+ new(
+ id: blob.oid,
+ size: blob.size,
+ data: blob.content(limit),
+ binary: blob.binary?
+ )
+ end
+
+ # Efficient lookup to determine if object size
+ # and type make it a possible LFS blob without loading
+ # blob content into memory with repository.lookup(sha)
+ def possible_lfs_blob?(repository, sha)
+ object_header = repository.rugged.read_header(sha)
+
+ object_header[:type] == :blob &&
+ size_could_be_lfs?(object_header[:len])
end
end
@@ -226,7 +264,7 @@ module Gitlab
# size
# see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
def lfs_pointer?
- has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
+ self.class.size_could_be_lfs?(size) && has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
end
def lfs_oid
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 23ae37ff71e..d5518814483 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -73,7 +73,7 @@ module Gitlab
decorate(repo, commit) if commit
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository,
- Rugged::OdbError, Rugged::TreeError
+ Rugged::OdbError, Rugged::TreeError, ArgumentError
nil
end
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
new file mode 100644
index 00000000000..732dd5d998a
--- /dev/null
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Git
+ class LfsChanges
+ def initialize(repository, newrev)
+ @repository = repository
+ @newrev = newrev
+ end
+
+ def new_pointers(object_limit: nil, not_in: nil)
+ @new_pointers ||= begin
+ rev_list.new_objects(not_in: not_in, require_path: true) do |object_ids|
+ object_ids = object_ids.take(object_limit) if object_limit
+
+ Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
+ end
+ end
+ end
+
+ def all_pointers
+ rev_list.all_objects(require_path: true) do |object_ids|
+ Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
+ end
+ end
+
+ private
+
+ def rev_list
+ ::Gitlab::Git::RevList.new(path_to_repo: @repository.path_to_repo,
+ newrev: @newrev)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index ab94ba8a73a..e36d5410431 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -72,7 +72,7 @@ module Gitlab
# Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
# it would be created from `start_branch_name`.
- # If `start_project` is passed, and the branch doesn't exist,
+ # If `start_repository` is passed, and the branch doesn't exist,
# it would try to find the commits from it instead of current repository.
def with_branch(
branch_name,
@@ -80,15 +80,13 @@ module Gitlab
start_repository: repository,
&block)
- # Refactoring aid
- unless start_repository.is_a?(Gitlab::Git::Repository)
- raise "expected a Gitlab::Git::Repository, got #{start_repository}"
- end
+ Gitlab::Git.check_namespace!(start_repository)
+ start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
start_branch_name = nil if start_repository.empty_repo?
if start_branch_name && !start_repository.branch_exists?(start_branch_name)
- raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}"
+ raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.relative_path}"
end
update_branch_with_hooks(branch_name) do
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index b45da6020ee..d41fe78daa1 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -7,7 +7,7 @@ module Gitlab
module Popen
FAST_GIT_PROCESS_TIMEOUT = 15.seconds
- def popen(cmd, path, vars = {})
+ def popen(cmd, path, vars = {}, lazy_block: nil)
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
@@ -22,7 +22,12 @@ module Gitlab
yield(stdin) if block_given?
stdin.close
- @cmd_output << stdout.read
+ if lazy_block
+ return lazy_block.call(stdout.lazy)
+ else
+ @cmd_output << stdout.read
+ end
+
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
end
diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb
new file mode 100644
index 00000000000..3685aa20669
--- /dev/null
+++ b/lib/gitlab/git/remote_repository.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Git
+ #
+ # When a Gitaly call involves two repositories instead of one we cannot
+ # assume that both repositories are on the same Gitaly server. In this
+ # case we need to make a distinction between the repository that the
+ # call is being made on (a Repository instance), and the "other"
+ # repository (a RemoteRepository instance). This is the reason why we
+ # have the RemoteRepository class in Gitlab::Git.
+ #
+ # When you make changes, be aware that gitaly-ruby sub-classes this
+ # class.
+ #
+ class RemoteRepository
+ attr_reader :path, :relative_path, :gitaly_repository
+
+ def initialize(repository)
+ @relative_path = repository.relative_path
+ @gitaly_repository = repository.gitaly_repository
+
+ # These instance variables will not be available in gitaly-ruby, where
+ # we have no disk access to this repository.
+ @repository = repository
+ @path = repository.path
+ end
+
+ def empty_repo?
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.empty_repo?
+ end
+
+ def commit_id(revision)
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.commit(revision)&.sha
+ end
+
+ def branch_exists?(name)
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.branch_exists?(name)
+ end
+
+ # Compares self to a Gitlab::Git::Repository. This implementation uses
+ # 'self.gitaly_repository' so that it will also work in the
+ # GitalyRemoteRepository subclass defined in gitaly-ruby.
+ def same_repository?(other_repository)
+ gitaly_repository.storage_name == other_repository.storage &&
+ gitaly_repository.relative_path == other_repository.relative_path
+ end
+
+ def fetch_env
+ gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
+ gitaly_address = gitaly_client.address(storage)
+ gitaly_token = gitaly_client.token(storage)
+
+ request = Gitaly::SSHUploadPackRequest.new(repository: gitaly_repository)
+ env = {
+ 'GITALY_ADDRESS' => gitaly_address,
+ 'GITALY_PAYLOAD' => request.to_json,
+ 'GITALY_WD' => Dir.pwd,
+ 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
+ }
+ env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
+
+ env
+ end
+
+ private
+
+ # Must return an object that responds to 'address' and 'storage'.
+ def gitaly_client
+ Gitlab::GitalyClient
+ end
+
+ def storage
+ gitaly_repository.storage_name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index fc8af38d4d9..dcca20c75ef 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -6,6 +6,7 @@ require "rubygems/package"
module Gitlab
module Git
class Repository
+ include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
@@ -57,7 +58,7 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
- attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver
+ attr_reader :storage, :gl_repository, :relative_path
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
@@ -65,7 +66,6 @@ module Gitlab
@storage = storage
@relative_path = relative_path
@gl_repository = gl_repository
- @gitaly_resolver = Gitlab::GitalyClient
storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
@@ -104,7 +104,7 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
+ Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_repository_client.exists?
else
@@ -290,13 +290,27 @@ module Gitlab
end
end
+ def batch_existence(object_ids, existing: true)
+ filter_method = existing ? :select : :reject
+
+ object_ids.public_send(filter_method) do |oid| # rubocop:disable GitlabSecurity/PublicSend
+ rugged.exists?(oid)
+ end
+ end
+
# Returns an Array of branch and tag names
def ref_names
branch_names + tag_names
end
def delete_all_refs_except(prefixes)
- delete_refs(*all_ref_names_except(prefixes))
+ gitaly_migrate(:ref_delete_refs) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
+ else
+ delete_refs(*all_ref_names_except(prefixes))
+ end
+ end
end
# Returns an Array of all ref names, except when it's matching pattern
@@ -750,13 +764,13 @@ module Gitlab
end
def ff_merge(user, source_sha, target_branch)
- OperationService.new(user, self).with_branch(target_branch) do |our_commit|
- raise ArgumentError, 'Invalid merge target' unless our_commit
-
- source_sha
+ gitaly_migrate(:operation_user_ff_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ff_merge(user, source_sha, target_branch)
+ else
+ rugged_ff_merge(user, source_sha, target_branch)
+ end
end
- rescue Rugged::ReferenceError
- raise ArgumentError, 'Invalid merge source'
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
@@ -890,16 +904,30 @@ module Gitlab
end
end
- # Delete the specified remote from this repository.
- def remote_delete(remote_name)
+ def add_remote(remote_name, url)
+ rugged.remotes.create(remote_name, url)
+ rescue Rugged::ConfigError
+ remote_update(remote_name, url: url)
+ end
+
+ def remove_remote(remote_name)
+ # When a remote is deleted all its remote refs are deleted too, but in
+ # the case of mirrors we map its refs (that would usualy go under
+ # [remote_name]/) to the top level namespace. We clean the mapping so
+ # those don't get deleted.
+ if rugged.config["remote.#{remote_name}.mirror"]
+ rugged.config.delete("remote.#{remote_name}.fetch")
+ end
+
rugged.remotes.delete(remote_name)
- nil
+ true
+ rescue Rugged::ConfigError
+ false
end
- # Add a new remote to this repository.
- def remote_add(remote_name, url)
- rugged.remotes.create(remote_name, url)
- nil
+ # Returns true if a remote exists.
+ def remote_exists?(name)
+ rugged.remotes[name].present?
end
# Update the specified remote using the values in the +options+ hash
@@ -962,6 +990,10 @@ module Gitlab
@attributes.attributes(path)
end
+ def gitattribute(path, name)
+ attributes(path)[name]
+ end
+
def languages(ref = nil)
Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled|
if is_enabled
@@ -991,23 +1023,22 @@ module Gitlab
def with_repo_branch_commit(start_repository, start_branch_name)
Gitlab::Git.check_namespace!(start_repository)
+ start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
return yield nil if start_repository.empty_repo?
- if start_repository == self
+ if start_repository.same_repository?(self)
yield commit(start_branch_name)
else
- start_commit = start_repository.commit(start_branch_name)
+ start_commit_id = start_repository.commit_id(start_branch_name)
- return yield nil unless start_commit
+ return yield nil unless start_commit_id
- sha = start_commit.sha
-
- if branch_commit = commit(sha)
+ if branch_commit = commit(start_commit_id)
yield branch_commit
else
with_repo_tmp_commit(
- start_repository, start_branch_name, sha) do |tmp_commit|
+ start_repository, start_branch_name, start_commit_id) do |tmp_commit|
yield tmp_commit
end
end
@@ -1026,13 +1057,12 @@ module Gitlab
delete_refs(tmp_ref) if tmp_ref
end
- def fetch_source_branch(source_repository, source_branch, local_ref)
- with_repo_branch_commit(source_repository, source_branch) do |commit|
- if commit
- write_ref(local_ref, commit.sha)
- true
+ def fetch_source_branch!(source_repository, source_branch, local_ref)
+ Gitlab::GitalyClient.migrate(:fetch_source_branch) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref)
else
- false
+ rugged_fetch_source_branch(source_repository, source_branch, local_ref)
end
end
end
@@ -1064,6 +1094,9 @@ module Gitlab
end
def fetch_ref(source_repository, source_ref:, target_ref:)
+ Gitlab::Git.check_namespace!(source_repository)
+ source_repository = RemoteRepository.new(source_repository) unless source_repository.is_a?(RemoteRepository)
+
message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled|
if is_enabled
gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref)
@@ -1127,6 +1160,11 @@ module Gitlab
Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha)
end
+ # Items should be of format [[commit_id, path], [commit_id1, path1]]
+ def batch_blobs(items, blob_size_limit: nil)
+ Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
+ end
+
def commit_index(user, branch_name, index, options)
committer = user_to_committer(user)
@@ -1169,14 +1207,25 @@ module Gitlab
Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
raise NoRepository.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
rescue GRPC::InvalidArgument => e
raise ArgumentError.new(e)
+ rescue GRPC::BadStatus => e
+ raise CommandError.new(e)
end
private
+ def rugged_fetch_source_branch(source_repository, source_branch, local_ref)
+ with_repo_branch_commit(source_repository, source_branch) do |commit|
+ if commit
+ write_ref(local_ref, commit.sha)
+ true
+ else
+ false
+ end
+ end
+ end
+
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
def branches_filter(filter: nil, sort_by: nil)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
@@ -1352,6 +1401,7 @@ module Gitlab
end
return nil unless tmp_entry.type == :tree
+
tmp_entry = tmp_entry[dir]
end
end
@@ -1472,6 +1522,7 @@ module Gitlab
# Ref names must start with `refs/`.
def rugged_ref_exists?(ref_name)
raise ArgumentError, 'invalid refname' unless ref_name.start_with?('refs/')
+
rugged.references.exist?(ref_name)
rescue Rugged::ReferenceError
false
@@ -1538,6 +1589,7 @@ module Gitlab
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
rescue Rugged::ReferenceError => e
raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
+
raise InvalidRef.new("Invalid reference #{start_point}")
end
@@ -1597,22 +1649,25 @@ module Gitlab
end
def gitaly_fetch_ref(source_repository, source_ref:, target_ref:)
- gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
- gitaly_address = gitaly_resolver.address(source_repository.storage)
- gitaly_token = gitaly_resolver.token(source_repository.storage)
-
- request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository)
- env = {
- 'GITALY_ADDRESS' => gitaly_address,
- 'GITALY_PAYLOAD' => request.to_json,
- 'GITALY_WD' => Dir.pwd,
- 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
- }
- env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
-
args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref})
- run_git(args, env: env)
+ run_git(args, env: source_repository.fetch_env)
+ end
+
+ def gitaly_ff_merge(user, source_sha, target_branch)
+ gitaly_operations_client.user_ff_branch(user, source_sha, target_branch)
+ rescue GRPC::FailedPrecondition => e
+ raise CommitError, e
+ end
+
+ def rugged_ff_merge(user, source_sha, target_branch)
+ OperationService.new(user, self).with_branch(target_branch) do |our_commit|
+ raise ArgumentError, 'Invalid merge target' unless our_commit
+
+ source_sha
+ end
+ rescue Rugged::ReferenceError
+ raise ArgumentError, 'Invalid merge source'
end
end
end
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
new file mode 100644
index 00000000000..4500482d68f
--- /dev/null
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -0,0 +1,95 @@
+module Gitlab
+ module Git
+ module RepositoryMirroring
+ IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
+ IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
+ MIRROR_REMOTE = 'mirror'.freeze
+
+ RemoteError = Class.new(StandardError)
+
+ def set_remote_as_mirror(remote_name)
+ # This is used to define repository as equivalent as "git clone --mirror"
+ rugged.config["remote.#{remote_name}.fetch"] = 'refs/*:refs/*'
+ rugged.config["remote.#{remote_name}.mirror"] = true
+ rugged.config["remote.#{remote_name}.prune"] = true
+ end
+
+ def set_import_remote_as_mirror(remote_name)
+ # Add first fetch with Rugged so it does not create its own.
+ rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
+
+ add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
+
+ rugged.config["remote.#{remote_name}.mirror"] = true
+ rugged.config["remote.#{remote_name}.prune"] = true
+ end
+
+ def add_remote_fetch_config(remote_name, refspec)
+ run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
+ end
+
+ def fetch_mirror(url)
+ add_remote(MIRROR_REMOTE, url)
+ set_remote_as_mirror(MIRROR_REMOTE)
+ fetch(MIRROR_REMOTE)
+ remove_remote(MIRROR_REMOTE)
+ end
+
+ def remote_tags(remote)
+ # Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n"
+ # We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...]
+ list_remote_tags(remote).map do |line|
+ target, path = line.strip.split("\t")
+
+ # When the remote repo does not have tags.
+ if target.nil? || path.nil?
+ Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}"
+ return []
+ end
+
+ name = path.split('/', 3).last
+ # We're only interested in tag references
+ # See: http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name
+ next if name =~ /\^\{\}\Z/
+
+ target_commit = Gitlab::Git::Commit.find(self, target)
+ Gitlab::Git::Tag.new(self, name, target, target_commit)
+ end.compact
+ end
+
+ def remote_branches(remote_name)
+ branches = []
+
+ rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
+ name = ref.name.sub(/\Arefs\/remotes\/#{remote_name}\//, '')
+
+ begin
+ target_commit = Gitlab::Git::Commit.find(self, ref.target)
+ branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end
+
+ branches
+ end
+
+ private
+
+ def list_remote_tags(remote)
+ tag_list, exit_code, error = nil
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote})
+
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
+ tag_list = stdout.read
+ error = stderr.read
+ exit_code = wait_thr.value.exitstatus
+ end
+
+ raise RemoteError, error unless exit_code.zero?
+
+ tag_list.split("\n")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 60b2a4ec411..4974205b8fd 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -13,11 +13,32 @@ module Gitlab
@path_to_repo = path_to_repo
end
- # This method returns an array of new references
+ # This method returns an array of new commit references
def new_refs
execute([*base_args, newrev, '--not', '--all'])
end
+ # Finds newly added objects
+ # Returns an array of shas
+ #
+ # Can skip objects which do not have a path using required_path: true
+ # This skips commit objects and root trees, which might not be needed when
+ # looking for blobs
+ #
+ # When given a block it will yield objects as a lazy enumerator so
+ # the caller can limit work done instead of processing megabytes of data
+ def new_objects(require_path: nil, not_in: nil, &lazy_block)
+ args = [*base_args, newrev, *not_in_refs(not_in), '--objects']
+
+ get_objects(args, require_path: require_path, &lazy_block)
+ end
+
+ def all_objects(require_path: nil, &lazy_block)
+ args = [*base_args, '--all', '--objects']
+
+ get_objects(args, require_path: require_path, &lazy_block)
+ end
+
# This methods returns an array of missed references
#
# Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348.
@@ -27,6 +48,13 @@ module Gitlab
private
+ def not_in_refs(references)
+ return ['--not', '--all'] unless references
+ return [] if references.empty?
+
+ references.prepend('--not')
+ end
+
def execute(args)
output, status = popen(args, nil, Gitlab::Git::Env.to_env_hash)
@@ -37,6 +65,10 @@ module Gitlab
output.split("\n")
end
+ def lazy_execute(args, &lazy_block)
+ popen(args, nil, Gitlab::Git::Env.to_env_hash, lazy_block: lazy_block)
+ end
+
def base_args
[
Gitlab.config.git.bin_path,
@@ -44,6 +76,30 @@ module Gitlab
'rev-list'
]
end
+
+ def get_objects(args, require_path: nil)
+ if block_given?
+ lazy_execute(args) do |lazy_output|
+ objects = objects_from_output(lazy_output, require_path: require_path)
+
+ yield(objects)
+ end
+ else
+ object_output = execute(args)
+
+ objects_from_output(object_output, require_path: require_path)
+ end
+ end
+
+ def objects_from_output(object_output, require_path: nil)
+ object_output.map do |output_line|
+ sha, path = output_line.split(' ', 2)
+
+ next if require_path && path.blank?
+
+ sha
+ end.reject(&:nil?)
+ end
end
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index f01d5c96fc8..d4a53d32c28 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -10,6 +10,8 @@ module Gitlab
end
PageBlob = Struct.new(:name)
+ attr_reader :repository
+
def self.default_ref
'master'
end
@@ -35,51 +37,74 @@ module Gitlab
end
def delete_page(page_path, commit_details)
- assert_type!(commit_details, CommitDetails)
-
- gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
- nil
+ @repository.gitaly_migrate(:wiki_delete_page) do |is_enabled|
+ if is_enabled
+ gitaly_delete_page(page_path, commit_details)
+ gollum_wiki.clear_cache
+ else
+ gollum_delete_page(page_path, commit_details)
+ end
+ end
end
def update_page(page_path, title, format, content, commit_details)
- assert_type!(format, Symbol)
- assert_type!(commit_details, CommitDetails)
-
- gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h)
- nil
+ @repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
+ if is_enabled
+ gitaly_update_page(page_path, title, format, content, commit_details)
+ gollum_wiki.clear_cache
+ else
+ gollum_update_page(page_path, title, format, content, commit_details)
+ end
+ end
end
- def pages
- gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
+ def pages(limit: nil)
+ @repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ if is_enabled
+ gitaly_get_all_pages
+ else
+ gollum_get_all_pages(limit: limit)
+ end
+ end
end
def page(title:, version: nil, dir: nil)
- if version
- version = Gitlab::Git::Commit.find(@repository, version).id
+ @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
+ if is_enabled
+ gitaly_find_page(title: title, version: version, dir: dir)
+ else
+ gollum_find_page(title: title, version: version, dir: dir)
+ end
end
-
- gollum_page = gollum_wiki.page(title, version, dir)
- return unless gollum_page
-
- new_page(gollum_page)
end
def file(name, version)
- version ||= self.class.default_ref
- gollum_file = gollum_wiki.file(name, version)
- return unless gollum_file
-
- Gitlab::Git::WikiFile.new(gollum_file)
+ @repository.gitaly_migrate(:wiki_find_file) do |is_enabled|
+ if is_enabled
+ gitaly_find_file(name, version)
+ else
+ gollum_find_file(name, version)
+ end
+ end
end
- def page_versions(page_path)
+ # options:
+ # :page - The Integer page number.
+ # :per_page - The number of items per page.
+ # :limit - Total number of items to return.
+ def page_versions(page_path, options = {})
current_page = gollum_page_by_path(page_path)
- current_page.versions.map do |gollum_git_commit|
- gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id)
- new_version(gollum_page, gollum_git_commit.id)
+
+ commits_from_page(current_page, options).map do |gitlab_git_commit|
+ gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id)
+ Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format)
end
end
+ def count_page_versions(page_path)
+ @repository.count_commits(ref: 'HEAD', path: page_path)
+ end
+
def preview_slug(title, format)
# Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid
# using Rugged through a Gollum::Wiki instance
@@ -94,6 +119,22 @@ module Gitlab
private
+ # options:
+ # :page - The Integer page number.
+ # :per_page - The number of items per page.
+ # :limit - Total number of items to return.
+ def commits_from_page(gollum_page, options = {})
+ unless options[:limit]
+ options[:offset] = ([1, options.delete(:page).to_i].max - 1) * Gollum::Page.per_page
+ options[:limit] = (options.delete(:per_page) || Gollum::Page.per_page).to_i
+ end
+
+ @repository.log(ref: gollum_page.last_version.id,
+ path: gollum_page.path,
+ limit: options[:limit],
+ offset: options[:offset])
+ end
+
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
@@ -110,8 +151,17 @@ module Gitlab
end
def new_version(gollum_page, commit_id)
- commit = Gitlab::Git::Commit.find(@repository, commit_id)
- Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format)
+ Gitlab::Git::WikiPageVersion.new(version(commit_id), gollum_page&.format)
+ end
+
+ def version(commit_id)
+ commit_find_proc = -> { Gitlab::Git::Commit.find(@repository, commit_id) }
+
+ if RequestStore.active?
+ RequestStore.fetch([:wiki_version_commit, commit_id]) { commit_find_proc.call }
+ else
+ commit_find_proc.call
+ end
end
def assert_type!(object, klass)
@@ -135,9 +185,75 @@ module Gitlab
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
+ def gollum_delete_page(page_path, commit_details)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
+ nil
+ end
+
+ def gollum_update_page(page_path, title, format, content, commit_details)
+ assert_type!(format, Symbol)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h)
+ nil
+ end
+
+ def gollum_find_page(title:, version: nil, dir: nil)
+ if version
+ version = Gitlab::Git::Commit.find(@repository, version).id
+ end
+
+ gollum_page = gollum_wiki.page(title, version, dir)
+ return unless gollum_page
+
+ new_page(gollum_page)
+ end
+
+ def gollum_find_file(name, version)
+ version ||= self.class.default_ref
+ gollum_file = gollum_wiki.file(name, version)
+ return unless gollum_file
+
+ Gitlab::Git::WikiFile.new(gollum_file)
+ end
+
+ def gollum_get_all_pages(limit: nil)
+ gollum_wiki.pages(limit: limit).map { |gollum_page| new_page(gollum_page) }
+ end
+
def gitaly_write_page(name, format, content, commit_details)
gitaly_wiki_client.write_page(name, format, content, commit_details)
end
+
+ def gitaly_update_page(page_path, title, format, content, commit_details)
+ gitaly_wiki_client.update_page(page_path, title, format, content, commit_details)
+ end
+
+ def gitaly_delete_page(page_path, commit_details)
+ gitaly_wiki_client.delete_page(page_path, commit_details)
+ end
+
+ def gitaly_find_page(title:, version: nil, dir: nil)
+ wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir)
+ return unless wiki_page
+
+ Gitlab::Git::WikiPage.new(wiki_page, version)
+ end
+
+ def gitaly_find_file(name, version)
+ wiki_file = gitaly_wiki_client.find_file(name, version)
+ return unless wiki_file
+
+ Gitlab::Git::WikiFile.new(wiki_file)
+ end
+
+ def gitaly_get_all_pages
+ gitaly_wiki_client.get_all_pages.map do |wiki_page, version|
+ Gitlab::Git::WikiPage.new(wiki_page, version)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 6868be26758..572f4c892f6 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -34,10 +34,11 @@ module Gitlab
private_constant :MUTEX
class << self
- attr_accessor :query_time
+ attr_accessor :query_time, :migrate_histogram
end
self.query_time = 0
+ self.migrate_histogram = Gitlab::Metrics.histogram(:gitaly_migrate_call_duration, "Gitaly migration call execution timings")
def self.stub(name, storage)
MUTEX.synchronize do
@@ -74,6 +75,10 @@ module Gitlab
address
end
+ def self.address_metadata(storage)
+ Base64.strict_encode64(JSON.dump({ storage => { 'address' => address(storage), 'token' => token(storage) } }))
+ end
+
# All Gitaly RPC call sites should use GitalyClient.call. This method
# makes sure that per-request authentication headers are set.
#
@@ -88,18 +93,19 @@ module Gitlab
# kwargs.merge(deadline: Time.now + 10)
# end
#
- def self.call(storage, service, rpc, request)
+ def self.call(storage, service, rpc, request, remote_storage: nil)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
enforce_gitaly_request_limits(:call)
- kwargs = request_kwargs(storage)
+ kwargs = request_kwargs(storage, remote_storage: remote_storage)
kwargs = yield(kwargs) if block_given?
+
stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
ensure
self.query_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
end
- def self.request_kwargs(storage)
+ def self.request_kwargs(storage, remote_storage: nil)
encoded_token = Base64.strict_encode64(token(storage).to_s)
metadata = {
'authorization' => "Bearer #{encoded_token}",
@@ -109,6 +115,7 @@ module Gitlab
feature_stack = Thread.current[:gitaly_feature_stack]
feature = feature_stack && feature_stack[0]
metadata['call_site'] = feature.to_s if feature
+ metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
{ metadata: metadata }
end
@@ -171,8 +178,11 @@ module Gitlab
feature_stack = Thread.current[:gitaly_feature_stack] ||= []
feature_stack.unshift(feature)
begin
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield is_enabled
ensure
+ total_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
+ migrate_histogram.observe({ gitaly_enabled: is_enabled, feature: feature }, total_time)
feature_stack.shift
Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty?
end
diff --git a/lib/gitlab/gitaly_client/attributes_bag.rb b/lib/gitlab/gitaly_client/attributes_bag.rb
new file mode 100644
index 00000000000..198a1de91c7
--- /dev/null
+++ b/lib/gitlab/gitaly_client/attributes_bag.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module GitalyClient
+ # This module expects an `ATTRS` const to be defined on the subclass
+ # See GitalyClient::WikiFile for an example
+ module AttributesBag
+ extend ActiveSupport::Concern
+
+ included do
+ attr_accessor(*const_get(:ATTRS))
+ end
+
+ def initialize(params)
+ params = params.with_indifferent_access
+
+ attributes.each do |attr|
+ instance_variable_set("@#{attr}", params[attr])
+ end
+ end
+
+ def ==(other)
+ attributes.all? do |field|
+ instance_variable_get("@#{field}") == other.instance_variable_get("@#{field}")
+ end
+ end
+
+ def attributes
+ self.class.const_get(:ATTRS)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index a2b50f2507e..da5505cb2fe 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -18,7 +18,7 @@ module Gitlab
response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request)
response.flat_map do |msg|
- msg.paths.map { |d| d.dup.force_encoding(Encoding::UTF_8) }
+ msg.paths.map { |d| EncodingHelper.encode!(d.dup) }
end
end
diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb
index 54df6304865..d98a0ce988f 100644
--- a/lib/gitlab/gitaly_client/diff.rb
+++ b/lib/gitlab/gitaly_client/diff.rb
@@ -1,21 +1,9 @@
module Gitlab
module GitalyClient
class Diff
- FIELDS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze
+ ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze
- attr_accessor(*FIELDS)
-
- def initialize(params)
- params.each do |key, val|
- public_send(:"#{key}=", val) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
- def ==(other)
- FIELDS.all? do |field|
- public_send(field) == other.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
+ include AttributesBag
end
end
end
diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb
index 65d81dc5d46..da243ee2d1a 100644
--- a/lib/gitlab/gitaly_client/diff_stitcher.rb
+++ b/lib/gitlab/gitaly_client/diff_stitcher.rb
@@ -12,7 +12,7 @@ module Gitlab
@rpc_response.each do |diff_msg|
if current_diff.nil?
- diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::FIELDS)
+ diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::ATTRS)
# gRPC uses frozen strings by default, and we need to have an unfrozen string as it
# gets processed further down the line. So we unfreeze the first chunk of the patch
# in case it's the only chunk we receive for this diff.
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index adaf255f24b..526d44a8b77 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -105,6 +105,23 @@ module Gitlab
ensure
request_enum.close
end
+
+ def user_ff_branch(user, source_sha, target_branch)
+ request = Gitaly::UserFFBranchRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ commit_id: source_sha,
+ branch: GitalyClient.encode(target_branch)
+ )
+
+ branch_update = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_ff_branch,
+ request
+ ).branch_update
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index b0c73395cb1..31b04bc2650 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -126,6 +126,15 @@ module Gitlab
GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request)
end
+ def delete_refs(except_with_prefixes:)
+ request = Gitaly::DeleteRefsRequest.new(
+ repository: @gitaly_repo,
+ except_with_prefix: except_with_prefixes
+ )
+
+ GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request)
+ end
+
private
def consume_refs_response(response)
@@ -137,6 +146,7 @@ module Gitlab
enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym)
raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value
+
enum_value
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index cef692d3c2a..70cb16bd810 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -65,6 +65,25 @@ module Gitlab
response.value
end
+
+ def fetch_source_branch(source_repository, source_branch, local_ref)
+ request = Gitaly::FetchSourceBranchRequest.new(
+ repository: @gitaly_repo,
+ source_repository: source_repository.gitaly_repository,
+ source_branch: source_branch.b,
+ target_ref: local_ref.b
+ )
+
+ response = GitalyClient.call(
+ @storage,
+ :repository_service,
+ :fetch_source_branch,
+ request,
+ remote_storage: source_repository.storage
+ )
+
+ response.result
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_file.rb b/lib/gitlab/gitaly_client/wiki_file.rb
new file mode 100644
index 00000000000..47c60c92484
--- /dev/null
+++ b/lib/gitlab/gitaly_client/wiki_file.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module GitalyClient
+ class WikiFile
+ ATTRS = %i(name mime_type path raw_data).freeze
+
+ include AttributesBag
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb
new file mode 100644
index 00000000000..7339468e911
--- /dev/null
+++ b/lib/gitlab/gitaly_client/wiki_page.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module GitalyClient
+ class WikiPage
+ ATTRS = %i(title format url_path path name historical raw_data).freeze
+
+ include AttributesBag
+
+ def initialize(params)
+ super
+
+ # All gRPC strings in a response are frozen, so we get an unfrozen
+ # version here so appending to `raw_data` doesn't blow up.
+ @raw_data = @raw_data.dup
+ end
+
+ def historical?
+ @historical
+ end
+
+ def format
+ @format.to_sym
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 03afcce81f0..c8f065f5881 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -15,11 +15,7 @@ module Gitlab
repository: @gitaly_repo,
name: GitalyClient.encode(name),
format: format.to_s,
- commit_details: Gitaly::WikiCommitDetails.new(
- name: GitalyClient.encode(commit_details.name),
- email: GitalyClient.encode(commit_details.email),
- message: GitalyClient.encode(commit_details.message)
- )
+ commit_details: gitaly_commit_details(commit_details)
)
strio = StringIO.new(content)
@@ -40,6 +36,135 @@ module Gitlab
raise Gitlab::Git::Wiki::DuplicatePageError, error
end
end
+
+ def update_page(page_path, title, format, content, commit_details)
+ request = Gitaly::WikiUpdatePageRequest.new(
+ repository: @gitaly_repo,
+ page_path: GitalyClient.encode(page_path),
+ title: GitalyClient.encode(title),
+ format: format.to_s,
+ commit_details: gitaly_commit_details(commit_details)
+ )
+
+ strio = StringIO.new(content)
+
+ enum = Enumerator.new do |y|
+ until strio.eof?
+ chunk = strio.read(MAX_MSG_SIZE)
+ request.content = GitalyClient.encode(chunk)
+
+ y.yield request
+
+ request = Gitaly::WikiUpdatePageRequest.new
+ end
+ end
+
+ GitalyClient.call(@repository.storage, :wiki_service, :wiki_update_page, enum)
+ end
+
+ def delete_page(page_path, commit_details)
+ request = Gitaly::WikiDeletePageRequest.new(
+ repository: @gitaly_repo,
+ page_path: GitalyClient.encode(page_path),
+ commit_details: gitaly_commit_details(commit_details)
+ )
+
+ GitalyClient.call(@repository.storage, :wiki_service, :wiki_delete_page, request)
+ end
+
+ def find_page(title:, version: nil, dir: nil)
+ request = Gitaly::WikiFindPageRequest.new(
+ repository: @gitaly_repo,
+ title: GitalyClient.encode(title),
+ revision: GitalyClient.encode(version),
+ directory: GitalyClient.encode(dir)
+ )
+
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
+
+ wiki_page_from_iterator(response)
+ end
+
+ def get_all_pages
+ request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo)
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request)
+ pages = []
+
+ loop do
+ page, version = wiki_page_from_iterator(response) { |message| message.end_of_page }
+
+ break unless page && version
+
+ pages << [page, version]
+ end
+
+ pages
+ end
+
+ def find_file(name, revision)
+ request = Gitaly::WikiFindFileRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(name),
+ revision: GitalyClient.encode(revision)
+ )
+
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request)
+ wiki_file = nil
+
+ response.each do |message|
+ next unless message.name.present?
+
+ if wiki_file
+ wiki_file.raw_data << message.raw_data
+ else
+ wiki_file = GitalyClient::WikiFile.new(message.to_h)
+ # All gRPC strings in a response are frozen, so we get
+ # an unfrozen version here so appending in the else clause below doesn't blow up.
+ wiki_file.raw_data = wiki_file.raw_data.dup
+ end
+ end
+
+ wiki_file
+ end
+
+ private
+
+ # If a block is given and the yielded value is true, iteration will be
+ # stopped early at that point; else the iterator is consumed entirely.
+ # The iterator is traversed with `next` to allow resuming the iteration.
+ def wiki_page_from_iterator(iterator)
+ wiki_page = version = nil
+
+ while message = iterator.next
+ break if block_given? && yield(message)
+
+ page = message.page
+ next unless page
+
+ if wiki_page
+ wiki_page.raw_data << page.raw_data
+ else
+ wiki_page = GitalyClient::WikiPage.new(page.to_h)
+
+ version = Gitlab::Git::WikiPageVersion.new(
+ Gitlab::Git::Commit.decorate(@repository, page.version.commit),
+ page.version.format
+ )
+ end
+ end
+
+ [wiki_page, version]
+ rescue StopIteration
+ [wiki_page, version]
+ end
+
+ def gitaly_commit_details(commit_details)
+ Gitaly::WikiCommitDetails.new(
+ name: GitalyClient.encode(commit_details.name),
+ email: GitalyClient.encode(commit_details.email),
+ message: GitalyClient.encode(commit_details.message)
+ )
+ end
end
end
end
diff --git a/lib/gitlab/github_import.rb b/lib/gitlab/github_import.rb
new file mode 100644
index 00000000000..d2ae4c1255e
--- /dev/null
+++ b/lib/gitlab/github_import.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module GithubImport
+ def self.new_client_for(project, token: nil, parallel: true)
+ token_to_use = token || project.import_data&.credentials&.fetch(:user)
+
+ Client.new(token_to_use, parallel: parallel)
+ end
+
+ # Inserts a raw row and returns the ID of the inserted row.
+ #
+ # attributes - The attributes/columns to set.
+ # relation - An ActiveRecord::Relation to use for finding the ID of the row
+ # when using MySQL.
+ def self.insert_and_return_id(attributes, relation)
+ # We use bulk_insert here so we can bypass any queries executed by
+ # callbacks or validation rules, as doing this wouldn't scale when
+ # importing very large projects.
+ result = Gitlab::Database
+ .bulk_insert(relation.table_name, [attributes], return_ids: true)
+
+ # MySQL doesn't support returning the IDs of a bulk insert in a way that
+ # is not a pain, so in this case we'll issue an extra query instead.
+ result.first ||
+ relation.where(iid: attributes[:iid]).limit(1).pluck(:id).first
+ end
+
+ # Returns the ID of the ghost user.
+ def self.ghost_user_id
+ key = 'github-import/ghost-user-id'
+
+ Caching.read_integer(key) || Caching.write(key, User.select(:id).ghost.id)
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb
new file mode 100644
index 00000000000..147597289cf
--- /dev/null
+++ b/lib/gitlab/github_import/bulk_importing.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module BulkImporting
+ # Builds and returns an Array of objects to bulk insert into the
+ # database.
+ #
+ # enum - An Enumerable that returns the objects to turn into database
+ # rows.
+ def build_database_rows(enum)
+ enum.each_with_object([]) do |(object, _), rows|
+ rows << build(object) unless already_imported?(object)
+ end
+ end
+
+ # Bulk inserts the given rows into the database.
+ def bulk_insert(model, rows, batch_size: 100)
+ rows.each_slice(batch_size) do |slice|
+ Gitlab::Database.bulk_insert(model.table_name, slice)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/caching.rb b/lib/gitlab/github_import/caching.rb
new file mode 100644
index 00000000000..b08f133794f
--- /dev/null
+++ b/lib/gitlab/github_import/caching.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Caching
+ # The default timeout of the cache keys.
+ TIMEOUT = 24.hours.to_i
+
+ WRITE_IF_GREATER_SCRIPT = <<-EOF.strip_heredoc.freeze
+ local key, value, ttl = KEYS[1], tonumber(ARGV[1]), ARGV[2]
+ local existing = tonumber(redis.call("get", key))
+
+ if existing == nil or value > existing then
+ redis.call("set", key, value)
+ redis.call("expire", key, ttl)
+ return true
+ else
+ return false
+ end
+ EOF
+
+ # Reads a cache key.
+ #
+ # If the key exists and has a non-empty value its TTL is refreshed
+ # automatically.
+ #
+ # raw_key - The cache key to read.
+ # timeout - The new timeout of the key if the key is to be refreshed.
+ def self.read(raw_key, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+ value = Redis::Cache.with { |redis| redis.get(key) }
+
+ if value.present?
+ # We refresh the expiration time so frequently used keys stick
+ # around, removing the need for querying the database as much as
+ # possible.
+ #
+ # A key may be empty when we looked up a GitHub user (for example) but
+ # did not find a matching GitLab user. In that case we _don't_ want to
+ # refresh the TTL so we automatically pick up the right data when said
+ # user were to register themselves on the GitLab instance.
+ Redis::Cache.with { |redis| redis.expire(key, timeout) }
+ end
+
+ value
+ end
+
+ # Reads an integer from the cache, or returns nil if no value was found.
+ #
+ # See Caching.read for more information.
+ def self.read_integer(raw_key, timeout: TIMEOUT)
+ value = read(raw_key, timeout: timeout)
+
+ value.to_i if value.present?
+ end
+
+ # Sets a cache key to the given value.
+ #
+ # key - The cache key to write.
+ # value - The value to set.
+ # timeout - The time after which the cache key should expire.
+ def self.write(raw_key, value, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.set(key, value, ex: timeout)
+ end
+
+ value
+ end
+
+ # Adds a value to a set.
+ #
+ # raw_key - The key of the set to add the value to.
+ # value - The value to add to the set.
+ # timeout - The new timeout of the key.
+ def self.set_add(raw_key, value, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.multi do |m|
+ m.sadd(key, value)
+ m.expire(key, timeout)
+ end
+ end
+ end
+
+ # Returns true if the given value is present in the set.
+ #
+ # raw_key - The key of the set to check.
+ # value - The value to check for.
+ def self.set_includes?(raw_key, value)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.sismember(key, value)
+ end
+ end
+
+ # Sets multiple keys to a given value.
+ #
+ # mapping - A Hash mapping the cache keys to their values.
+ # timeout - The time after which the cache key should expire.
+ def self.write_multiple(mapping, timeout: TIMEOUT)
+ Redis::Cache.with do |redis|
+ redis.multi do |multi|
+ mapping.each do |raw_key, value|
+ multi.set(cache_key_for(raw_key), value, ex: timeout)
+ end
+ end
+ end
+ end
+
+ # Sets the expiration time of a key.
+ #
+ # raw_key - The key for which to change the timeout.
+ # timeout - The new timeout.
+ def self.expire(raw_key, timeout)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.expire(key, timeout)
+ end
+ end
+
+ # Sets a key to the given integer but only if the existing value is
+ # smaller than the given value.
+ #
+ # This method uses a Lua script to ensure the read and write are atomic.
+ #
+ # raw_key - The key to set.
+ # value - The new value for the key.
+ # timeout - The key timeout in seconds.
+ #
+ # Returns true when the key was overwritten, false otherwise.
+ def self.write_if_greater(raw_key, value, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+ val = Redis::Cache.with do |redis|
+ redis
+ .eval(WRITE_IF_GREATER_SCRIPT, keys: [key], argv: [value, timeout])
+ end
+
+ val ? true : false
+ end
+
+ def self.cache_key_for(raw_key)
+ "#{Redis::Cache::CACHE_NAMESPACE}:#{raw_key}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 0550f9695bd..5da9befa08e 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -1,147 +1,216 @@
+# frozen_string_literal: true
+
module Gitlab
module GithubImport
+ # HTTP client for interacting with the GitHub API.
+ #
+ # This class is basically a fancy wrapped around Octokit while adding some
+ # functionality to deal with rate limiting and parallel imports. Usage is
+ # mostly the same as Octokit, for example:
+ #
+ # client = GithubImport::Client.new('hunter2')
+ #
+ # client.labels.each do |label|
+ # puts label.name
+ # end
class Client
- GITHUB_SAFE_REMAINING_REQUESTS = 100
- GITHUB_SAFE_SLEEP_TIME = 500
+ attr_reader :octokit
+
+ # A single page of data and the corresponding page number.
+ Page = Struct.new(:objects, :number)
+
+ # The minimum number of requests we want to keep available.
+ #
+ # We don't use a value of 0 as multiple threads may be using the same
+ # token in parallel. This could result in all of them hitting the GitHub
+ # rate limit at once. The threshold is put in place to not hit the limit
+ # in most cases.
+ RATE_LIMIT_THRESHOLD = 50
+
+ # token - The GitHub API token to use.
+ #
+ # per_page - The number of objects that should be displayed per page.
+ #
+ # parallel - When set to true hitting the rate limit will result in a
+ # dedicated error being raised. When set to `false` we will
+ # instead just `sleep()` until the rate limit is reset. Setting
+ # this value to `true` for parallel importing is crucial as
+ # otherwise hitting the rate limit will result in a thread
+ # being blocked in a `sleep()` call for up to an hour.
+ def initialize(token, per_page: 100, parallel: true)
+ @octokit = Octokit::Client.new(
+ access_token: token,
+ per_page: per_page,
+ api_endpoint: api_endpoint
+ )
- attr_reader :access_token, :host, :api_version
+ @octokit.connection_options[:ssl] = { verify: verify_ssl }
- def initialize(access_token, host: nil, api_version: 'v3')
- @access_token = access_token
- @host = host.to_s.sub(%r{/+\z}, '')
- @api_version = api_version
- @users = {}
+ @parallel = parallel
+ end
- if access_token
- ::Octokit.auto_paginate = false
- end
+ def parallel?
+ @parallel
end
- def api
- @api ||= ::Octokit::Client.new(
- access_token: access_token,
- api_endpoint: api_endpoint,
- # If there is no config, we're connecting to github.com and we
- # should verify ssl.
- connection_options: {
- ssl: { verify: config ? config['verify_ssl'] : true }
- }
- )
+ # Returns the details of a GitHub user.
+ #
+ # username - The username of the user.
+ def user(username)
+ with_rate_limit { octokit.user(username) }
end
- def client
- unless config
- raise Projects::ImportService::Error,
- 'OAuth configuration for GitHub missing.'
- end
+ # Returns the details of a GitHub repository.
+ #
+ # name - The path (in the form `owner/repository`) of the repository.
+ def repository(name)
+ with_rate_limit { octokit.repo(name) }
+ end
- @client ||= ::OAuth2::Client.new(
- config.app_id,
- config.app_secret,
- github_options.merge(ssl: { verify: config['verify_ssl'] })
- )
+ def labels(*args)
+ each_object(:labels, *args)
end
- def authorize_url(redirect_uri)
- client.auth_code.authorize_url({
- redirect_uri: redirect_uri,
- scope: "repo, user, user:email"
- })
+ def milestones(*args)
+ each_object(:milestones, *args)
end
- def get_token(code)
- client.auth_code.get_token(code).token
+ def releases(*args)
+ each_object(:releases, *args)
end
- def method_missing(method, *args, &block)
- if api.respond_to?(method)
- request(method, *args, &block)
- else
- super(method, *args, &block)
+ # Fetches data from the GitHub API and yields a Page object for every page
+ # of data, without loading all of them into memory.
+ #
+ # method - The Octokit method to use for getting the data.
+ # args - Arguments to pass to the Octokit method.
+ #
+ # rubocop: disable GitlabSecurity/PublicSend
+ def each_page(method, *args, &block)
+ return to_enum(__method__, method, *args) unless block_given?
+
+ page =
+ if args.last.is_a?(Hash) && args.last[:page]
+ args.last[:page]
+ else
+ 1
+ end
+
+ collection = with_rate_limit { octokit.public_send(method, *args) }
+ next_url = octokit.last_response.rels[:next]
+
+ yield Page.new(collection, page)
+
+ while next_url
+ response = with_rate_limit { next_url.get }
+ next_url = response.rels[:next]
+
+ yield Page.new(response.data, page += 1)
end
end
- def respond_to?(method)
- api.respond_to?(method) || super
+ # Iterates over all of the objects for the given method (e.g. `:labels`).
+ #
+ # method - The method to send to Octokit for querying data.
+ # args - Any arguments to pass to the Octokit method.
+ def each_object(method, *args, &block)
+ return to_enum(__method__, method, *args) unless block_given?
+
+ each_page(method, *args) do |page|
+ page.objects.each do |object|
+ yield object
+ end
+ end
end
- def user(login)
- return nil unless login.present?
- return @users[login] if @users.key?(login)
+ # Yields the supplied block, responding to any rate limit errors.
+ #
+ # The exact strategy used for handling rate limiting errors depends on
+ # whether we are running in parallel mode or not. For more information see
+ # `#rate_or_wait_for_rate_limit`.
+ def with_rate_limit
+ return yield unless rate_limiting_enabled?
- @users[login] = api.user(login)
- end
+ request_count_counter.increment
- private
+ raise_or_wait_for_rate_limit unless requests_remaining?
- def api_endpoint
- if host.present? && api_version.present?
- "#{host}/api/#{api_version}"
- else
- github_options[:site]
+ begin
+ yield
+ rescue Octokit::TooManyRequests
+ raise_or_wait_for_rate_limit
+
+ # This retry will only happen when running in sequential mode as we'll
+ # raise an error in parallel mode.
+ retry
end
end
- def config
- Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" }
+ # Returns `true` if we're still allowed to perform API calls.
+ def requests_remaining?
+ remaining_requests > RATE_LIMIT_THRESHOLD
+ end
+
+ def remaining_requests
+ octokit.rate_limit.remaining
end
- def github_options
- if config
- config["args"]["client_options"].deep_symbolize_keys
+ def raise_or_wait_for_rate_limit
+ rate_limit_counter.increment
+
+ if parallel?
+ raise RateLimitError
else
- OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ sleep(rate_limit_resets_in)
end
end
- def rate_limit
- api.rate_limit!
- # GitHub Rate Limit API returns 404 when the rate limit is
- # disabled. In this case we just want to return gracefully
- # instead of spitting out an error.
- rescue Octokit::NotFound
- nil
+ def rate_limit_resets_in
+ # We add a few seconds to the rate limit so we don't _immediately_
+ # resume when the rate limit resets as this may result in us performing
+ # a request before GitHub has a chance to reset the limit.
+ octokit.rate_limit.resets_in + 5
end
- def has_rate_limit?
- return @has_rate_limit if defined?(@has_rate_limit)
-
- @has_rate_limit = rate_limit.present?
+ def rate_limiting_enabled?
+ @rate_limiting_enabled ||= api_endpoint.include?('.github.com')
end
- def rate_limit_exceed?
- has_rate_limit? && rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS
+ def api_endpoint
+ custom_api_endpoint || default_api_endpoint
end
- def rate_limit_sleep_time
- rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
+ def custom_api_endpoint
+ github_omniauth_provider.dig('args', 'client_options', 'site')
end
- def request(method, *args, &block)
- sleep rate_limit_sleep_time if rate_limit_exceed?
+ def default_api_endpoint
+ OmniAuth::Strategies::GitHub.default_options[:client_options][:site]
+ end
- data = api.__send__(method, *args) # rubocop:disable GitlabSecurity/PublicSend
- return data unless data.is_a?(Array)
+ def verify_ssl
+ github_omniauth_provider.fetch('verify_ssl', true)
+ end
- last_response = api.last_response
+ def github_omniauth_provider
+ @github_omniauth_provider ||=
+ Gitlab.config.omniauth.providers
+ .find { |provider| provider.name == 'github' }
+ .to_h
+ end
- if block_given?
- yield data
- # api.last_response could change while we're yielding (e.g. fetching labels for each PR)
- # so we cache our own last response
- each_response_page(last_response, &block)
- else
- each_response_page(last_response) { |page| data.concat(page) }
- data
- end
+ def rate_limit_counter
+ @rate_limit_counter ||= Gitlab::Metrics.counter(
+ :github_importer_rate_limit_hits,
+ 'The number of times we hit the GitHub rate limit when importing projects'
+ )
end
- def each_response_page(last_response)
- while last_response.rels[:next]
- sleep rate_limit_sleep_time if rate_limit_exceed?
- last_response = last_response.rels[:next].get
- yield last_response.data if last_response.data.is_a?(Array)
- end
+ def request_count_counter
+ @request_counter ||= Gitlab::Metrics.counter(
+ :github_importer_request_count,
+ 'The number of GitHub API calls performed when importing projects'
+ )
end
end
end
diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb
new file mode 100644
index 00000000000..8274f37d358
--- /dev/null
+++ b/lib/gitlab/github_import/importer/diff_note_importer.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class DiffNoteImporter
+ attr_reader :note, :project, :client, :user_finder
+
+ # note - An instance of `Gitlab::GithubImport::Representation::DiffNote`.
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ def initialize(note, project, client)
+ @note = note
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ end
+
+ def execute
+ return unless (mr_id = find_merge_request_id)
+
+ author_id, author_found = user_finder.author_id_for(note)
+
+ note_body =
+ MarkdownText.format(note.note, note.author, author_found)
+
+ attributes = {
+ noteable_type: 'MergeRequest',
+ noteable_id: mr_id,
+ project_id: project.id,
+ author_id: author_id,
+ note: note_body,
+ system: false,
+ commit_id: note.commit_id,
+ line_code: note.line_code,
+ type: 'LegacyDiffNote',
+ created_at: note.created_at,
+ updated_at: note.updated_at,
+ st_diff: note.diff_hash.to_yaml
+ }
+
+ # It's possible that during an import we'll insert tens of thousands
+ # of diff notes. If we were to use the Note/LegacyDiffNote model here
+ # we'd also have to run additional queries for both validations and
+ # callbacks, putting a lot of pressure on the database.
+ #
+ # To work around this we're using bulk_insert with a single row. This
+ # allows us to efficiently insert data (even if it's just 1 row)
+ # without having to use all sorts of hacks to disable callbacks.
+ Gitlab::Database.bulk_insert(LegacyDiffNote.table_name, [attributes])
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project and the issue have been deleted since
+ # scheduling this job. In this case we'll just skip creating the note.
+ end
+
+ # Returns the ID of the merge request this note belongs to.
+ def find_merge_request_id
+ GithubImport::IssuableFinder.new(project, note).database_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/diff_notes_importer.rb b/lib/gitlab/github_import/importer/diff_notes_importer.rb
new file mode 100644
index 00000000000..966f12c5c2f
--- /dev/null
+++ b/lib/gitlab/github_import/importer/diff_notes_importer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class DiffNotesImporter
+ include ParallelScheduling
+
+ def representation_class
+ Representation::DiffNote
+ end
+
+ def importer_class
+ DiffNoteImporter
+ end
+
+ def sidekiq_worker_class
+ ImportDiffNoteWorker
+ end
+
+ def collection_method
+ :pull_requests_comments
+ end
+
+ def id_for_already_imported_cache(note)
+ note.id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/issue_and_label_links_importer.rb b/lib/gitlab/github_import/importer/issue_and_label_links_importer.rb
new file mode 100644
index 00000000000..bad064b76c8
--- /dev/null
+++ b/lib/gitlab/github_import/importer/issue_and_label_links_importer.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class IssueAndLabelLinksImporter
+ attr_reader :issue, :project, :client
+
+ # issue - An instance of `Gitlab::GithubImport::Representation::Issue`.
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(issue, project, client)
+ @issue = issue
+ @project = project
+ @client = client
+ end
+
+ def execute
+ IssueImporter.import_if_issue(issue, project, client)
+ LabelLinksImporter.new(issue, project, client).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
new file mode 100644
index 00000000000..31fefebf787
--- /dev/null
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class IssueImporter
+ attr_reader :project, :issue, :client, :user_finder, :milestone_finder,
+ :issuable_finder
+
+ # Imports an issue if it's a regular issue and not a pull request.
+ def self.import_if_issue(issue, project, client)
+ new(issue, project, client).execute unless issue.pull_request?
+ end
+
+ # issue - An instance of `Gitlab::GithubImport::Representation::Issue`.
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(issue, project, client)
+ @issue = issue
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ @milestone_finder = MilestoneFinder.new(project)
+ @issuable_finder = GithubImport::IssuableFinder.new(project, issue)
+ end
+
+ def execute
+ Issue.transaction do
+ if (issue_id = create_issue)
+ create_assignees(issue_id)
+ issuable_finder.cache_database_id(issue_id)
+ end
+ end
+ end
+
+ # Creates a new GitLab issue for the current GitHub issue.
+ #
+ # Returns the ID of the created issue as an Integer. If the issue
+ # couldn't be created this method will return `nil` instead.
+ def create_issue
+ author_id, author_found = user_finder.author_id_for(issue)
+
+ description =
+ MarkdownText.format(issue.description, issue.author, author_found)
+
+ attributes = {
+ iid: issue.iid,
+ title: issue.truncated_title,
+ author_id: author_id,
+ project_id: project.id,
+ description: description,
+ milestone_id: milestone_finder.id_for(issue),
+ state: issue.state,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at
+ }
+
+ GithubImport.insert_and_return_id(attributes, project.issues)
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project has been deleted since scheduling this
+ # job. In this case we'll just skip creating the issue.
+ end
+
+ # Stores all issue assignees in the database.
+ #
+ # issue_id - The ID of the created issue.
+ def create_assignees(issue_id)
+ assignees = []
+
+ issue.assignees.each do |assignee|
+ if (user_id = user_finder.user_id_for(assignee))
+ assignees << { issue_id: issue_id, user_id: user_id }
+ end
+ end
+
+ Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignees)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/issues_importer.rb b/lib/gitlab/github_import/importer/issues_importer.rb
new file mode 100644
index 00000000000..ac6d0666b3a
--- /dev/null
+++ b/lib/gitlab/github_import/importer/issues_importer.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class IssuesImporter
+ include ParallelScheduling
+
+ def importer_class
+ IssueAndLabelLinksImporter
+ end
+
+ def representation_class
+ Representation::Issue
+ end
+
+ def sidekiq_worker_class
+ ImportIssueWorker
+ end
+
+ def collection_method
+ :issues
+ end
+
+ def id_for_already_imported_cache(issue)
+ issue.number
+ end
+
+ def collection_options
+ { state: 'all', sort: 'created', direction: 'asc' }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/label_links_importer.rb b/lib/gitlab/github_import/importer/label_links_importer.rb
new file mode 100644
index 00000000000..2001b7e3482
--- /dev/null
+++ b/lib/gitlab/github_import/importer/label_links_importer.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class LabelLinksImporter
+ attr_reader :issue, :project, :client, :label_finder
+
+ # issue - An instance of `Gitlab::GithubImport::Representation::Issue`
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(issue, project, client)
+ @issue = issue
+ @project = project
+ @client = client
+ @label_finder = LabelFinder.new(project)
+ end
+
+ def execute
+ create_labels
+ end
+
+ def create_labels
+ time = Time.zone.now
+ rows = []
+ target_id = find_target_id
+
+ issue.label_names.each do |label_name|
+ # Although unlikely it's technically possible for an issue to be
+ # given a label that was created and assigned after we imported all
+ # the project's labels.
+ next unless (label_id = label_finder.id_for(label_name))
+
+ rows << {
+ label_id: label_id,
+ target_id: target_id,
+ target_type: issue.issuable_type,
+ created_at: time,
+ updated_at: time
+ }
+ end
+
+ Gitlab::Database.bulk_insert(LabelLink.table_name, rows)
+ end
+
+ def find_target_id
+ GithubImport::IssuableFinder.new(project, issue).database_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/labels_importer.rb b/lib/gitlab/github_import/importer/labels_importer.rb
new file mode 100644
index 00000000000..a73033d35ba
--- /dev/null
+++ b/lib/gitlab/github_import/importer/labels_importer.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class LabelsImporter
+ include BulkImporting
+
+ attr_reader :project, :client, :existing_labels
+
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ def initialize(project, client)
+ @project = project
+ @client = client
+ @existing_labels = project.labels.pluck(:title).to_set
+ end
+
+ def execute
+ bulk_insert(Label, build_labels)
+ build_labels_cache
+ end
+
+ def build_labels
+ build_database_rows(each_label)
+ end
+
+ def already_imported?(label)
+ existing_labels.include?(label.name)
+ end
+
+ def build_labels_cache
+ LabelFinder.new(project).build_cache
+ end
+
+ def build(label)
+ time = Time.zone.now
+
+ {
+ title: label.name,
+ color: '#' + label.color,
+ project_id: project.id,
+ type: 'ProjectLabel',
+ created_at: time,
+ updated_at: time
+ }
+ end
+
+ def each_label
+ client.labels(project.import_source)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/milestones_importer.rb b/lib/gitlab/github_import/importer/milestones_importer.rb
new file mode 100644
index 00000000000..c53480e828a
--- /dev/null
+++ b/lib/gitlab/github_import/importer/milestones_importer.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class MilestonesImporter
+ include BulkImporting
+
+ attr_reader :project, :client, :existing_milestones
+
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(project, client)
+ @project = project
+ @client = client
+ @existing_milestones = project.milestones.pluck(:iid).to_set
+ end
+
+ def execute
+ bulk_insert(Milestone, build_milestones)
+ build_milestones_cache
+ end
+
+ def build_milestones
+ build_database_rows(each_milestone)
+ end
+
+ def already_imported?(milestone)
+ existing_milestones.include?(milestone.number)
+ end
+
+ def build_milestones_cache
+ MilestoneFinder.new(project).build_cache
+ end
+
+ def build(milestone)
+ {
+ iid: milestone.number,
+ title: milestone.title,
+ description: milestone.description,
+ project_id: project.id,
+ state: state_for(milestone),
+ created_at: milestone.created_at,
+ updated_at: milestone.updated_at
+ }
+ end
+
+ def state_for(milestone)
+ milestone.state == 'open' ? :active : :closed
+ end
+
+ def each_milestone
+ client.milestones(project.import_source, state: 'all')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb
new file mode 100644
index 00000000000..c890f2df360
--- /dev/null
+++ b/lib/gitlab/github_import/importer/note_importer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class NoteImporter
+ attr_reader :note, :project, :client, :user_finder
+
+ # note - An instance of `Gitlab::GithubImport::Representation::Note`.
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ def initialize(note, project, client)
+ @note = note
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ end
+
+ def execute
+ return unless (noteable_id = find_noteable_id)
+
+ author_id, author_found = user_finder.author_id_for(note)
+
+ note_body =
+ MarkdownText.format(note.note, note.author, author_found)
+
+ attributes = {
+ noteable_type: note.noteable_type,
+ noteable_id: noteable_id,
+ project_id: project.id,
+ author_id: author_id,
+ note: note_body,
+ system: false,
+ created_at: note.created_at,
+ updated_at: note.updated_at
+ }
+
+ # We're using bulk_insert here so we can bypass any validations and
+ # callbacks. Running these would result in a lot of unnecessary SQL
+ # queries being executed when importing large projects.
+ Gitlab::Database.bulk_insert(Note.table_name, [attributes])
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project and the issue have been deleted since
+ # scheduling this job. In this case we'll just skip creating the note.
+ end
+
+ # Returns the ID of the issue or merge request to create the note for.
+ def find_noteable_id
+ GithubImport::IssuableFinder.new(project, note).database_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/notes_importer.rb b/lib/gitlab/github_import/importer/notes_importer.rb
new file mode 100644
index 00000000000..5aec760ea5f
--- /dev/null
+++ b/lib/gitlab/github_import/importer/notes_importer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class NotesImporter
+ include ParallelScheduling
+
+ def importer_class
+ NoteImporter
+ end
+
+ def representation_class
+ Representation::Note
+ end
+
+ def sidekiq_worker_class
+ ImportNoteWorker
+ end
+
+ def collection_method
+ :issues_comments
+ end
+
+ def id_for_already_imported_cache(note)
+ note.id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb
new file mode 100644
index 00000000000..49d859f9624
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_request_importer.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestImporter
+ attr_reader :pull_request, :project, :client, :user_finder,
+ :milestone_finder, :issuable_finder
+
+ # pull_request - An instance of
+ # `Gitlab::GithubImport::Representation::PullRequest`.
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(pull_request, project, client)
+ @pull_request = pull_request
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ @milestone_finder = MilestoneFinder.new(project)
+ @issuable_finder =
+ GithubImport::IssuableFinder.new(project, pull_request)
+ end
+
+ def execute
+ if (mr_id = create_merge_request)
+ issuable_finder.cache_database_id(mr_id)
+ end
+ end
+
+ # Creates the merge request and returns its ID.
+ #
+ # This method will return `nil` if the merge request could not be
+ # created.
+ def create_merge_request
+ author_id, author_found = user_finder.author_id_for(pull_request)
+
+ description = MarkdownText
+ .format(pull_request.description, pull_request.author, author_found)
+
+ # This work must be wrapped in a transaction as otherwise we can leave
+ # behind incomplete data in the event of an error. This can then lead
+ # to duplicate key errors when jobs are retried.
+ MergeRequest.transaction do
+ attributes = {
+ iid: pull_request.iid,
+ title: pull_request.truncated_title,
+ description: description,
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: pull_request.formatted_source_branch,
+ target_branch: pull_request.target_branch,
+ state: pull_request.state,
+ milestone_id: milestone_finder.id_for(pull_request),
+ author_id: author_id,
+ assignee_id: user_finder.assignee_id_for(pull_request),
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ }
+
+ # When creating merge requests there are a lot of hooks that may
+ # run, for many different reasons. Many of these hooks (e.g. the
+ # ones used for rendering Markdown) are completely unnecessary and
+ # may even lead to transaction timeouts.
+ #
+ # To ensure importing pull requests has a minimal impact and can
+ # complete in a reasonable time we bypass all the hooks by inserting
+ # the row and then retrieving it. We then only perform the
+ # additional work that is strictly necessary.
+ merge_request_id = GithubImport
+ .insert_and_return_id(attributes, project.merge_requests)
+
+ merge_request = project.merge_requests.find(merge_request_id)
+
+ # These fields are set so we can create the correct merge request
+ # diffs.
+ merge_request.source_branch_sha = pull_request.source_branch_sha
+ merge_request.target_branch_sha = pull_request.target_branch_sha
+
+ merge_request.keep_around_commit
+ merge_request.merge_request_diffs.create
+
+ merge_request.id
+ end
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project has been deleted since scheduling this
+ # job. In this case we'll just skip creating the merge request.
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb
new file mode 100644
index 00000000000..5437e32e9f1
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestsImporter
+ include ParallelScheduling
+
+ def importer_class
+ PullRequestImporter
+ end
+
+ def representation_class
+ Representation::PullRequest
+ end
+
+ def sidekiq_worker_class
+ ImportPullRequestWorker
+ end
+
+ def id_for_already_imported_cache(pr)
+ pr.number
+ end
+
+ def each_object_to_import
+ super do |pr|
+ update_repository if update_repository?(pr)
+ yield pr
+ end
+ end
+
+ def update_repository
+ # We set this column _before_ fetching the repository, and this is
+ # deliberate. If we were to update this column after the fetch we may
+ # miss out on changes pushed during the fetch or between the fetch and
+ # updating the timestamp.
+ project.update_column(:last_repository_updated_at, Time.zone.now)
+
+ project.repository.fetch_remote('github', forced: false)
+
+ pname = project.path_with_namespace
+
+ Rails.logger
+ .info("GitHub importer finished updating repository for #{pname}")
+
+ repository_updates_counter.increment(project: pname)
+ end
+
+ def update_repository?(pr)
+ last_update = project.last_repository_updated_at || project.created_at
+
+ return false if pr.updated_at < last_update
+
+ # PRs may be updated without there actually being new commits, thus we
+ # check to make sure we only re-fetch if truly necessary.
+ !(commit_exists?(pr.head.sha) && commit_exists?(pr.base.sha))
+ end
+
+ def commit_exists?(sha)
+ project.repository.lookup(sha)
+ true
+ rescue Rugged::Error
+ false
+ end
+
+ def collection_method
+ :pull_requests
+ end
+
+ def collection_options
+ { state: 'all', sort: 'created', direction: 'asc' }
+ end
+
+ def repository_updates_counter
+ @repository_updates_counter ||= Gitlab::Metrics.counter(
+ :github_importer_repository_updates,
+ 'The number of times repositories have to be updated again'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/releases_importer.rb b/lib/gitlab/github_import/importer/releases_importer.rb
new file mode 100644
index 00000000000..100f459fdcc
--- /dev/null
+++ b/lib/gitlab/github_import/importer/releases_importer.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class ReleasesImporter
+ include BulkImporting
+
+ attr_reader :project, :client, :existing_tags
+
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(project, client)
+ @project = project
+ @client = client
+ @existing_tags = project.releases.pluck(:tag).to_set
+ end
+
+ def execute
+ bulk_insert(Release, build_releases)
+ end
+
+ def build_releases
+ build_database_rows(each_release)
+ end
+
+ def already_imported?(release)
+ existing_tags.include?(release.tag_name)
+ end
+
+ def build(release)
+ {
+ tag: release.tag_name,
+ description: description_for(release),
+ created_at: release.created_at,
+ updated_at: release.updated_at,
+ project_id: project.id
+ }
+ end
+
+ def each_release
+ client.releases(project.import_source)
+ end
+
+ def description_for(release)
+ if release.body.present?
+ release.body
+ else
+ "Release for tag #{release.tag_name}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
new file mode 100644
index 00000000000..0b67fc8db73
--- /dev/null
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class RepositoryImporter
+ include Gitlab::ShellAdapter
+
+ attr_reader :project, :client
+
+ def initialize(project, client)
+ @project = project
+ @client = client
+ end
+
+ # Returns true if we should import the wiki for the project.
+ def import_wiki?
+ client.repository(project.import_source)&.has_wiki &&
+ !project.wiki_repository_exists?
+ end
+
+ # Imports the repository data.
+ #
+ # This method will return true if the data was imported successfully or
+ # the repository had already been imported before.
+ def execute
+ imported =
+ # It's possible a repository has already been imported when running
+ # this code, e.g. because we had to retry this job after
+ # `import_wiki?` raised a rate limit error. In this case we'll skip
+ # re-importing the main repository.
+ if project.repository.empty_repo?
+ import_repository
+ else
+ true
+ end
+
+ update_clone_time if imported
+
+ imported = import_wiki_repository if import_wiki? && imported
+
+ imported
+ end
+
+ def import_repository
+ project.ensure_repository
+
+ configure_repository_remote
+
+ project.repository.fetch_remote('github', forced: true)
+
+ true
+ rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
+ fail_import("Failed to import the repository: #{e.message}")
+ end
+
+ def configure_repository_remote
+ return if project.repository.remote_exists?('github')
+
+ project.repository.add_remote('github', project.import_url)
+ project.repository.set_import_remote_as_mirror('github')
+
+ project.repository.add_remote_fetch_config(
+ 'github',
+ '+refs/pull/*/head:refs/merge-requests/*/head'
+ )
+ end
+
+ def import_wiki_repository
+ wiki_path = "#{project.disk_path}.wiki"
+ wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
+ storage_path = project.repository_storage_path
+
+ gitlab_shell.import_repository(storage_path, wiki_path, wiki_url)
+
+ true
+ rescue Gitlab::Shell::Error => e
+ if e.message !~ /repository not exported/
+ fail_import("Failed to import the wiki: #{e.message}")
+ else
+ true
+ end
+ end
+
+ def update_clone_time
+ project.update_column(:last_repository_updated_at, Time.zone.now)
+ end
+
+ def fail_import(message)
+ project.mark_import_as_failed(message)
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/issuable_finder.rb b/lib/gitlab/github_import/issuable_finder.rb
new file mode 100644
index 00000000000..211915f1d87
--- /dev/null
+++ b/lib/gitlab/github_import/issuable_finder.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # IssuableFinder can be used for caching and retrieving database IDs for
+ # issuable objects such as issues and pull requests. By caching these IDs we
+ # remove the need for running a lot of database queries when importing
+ # GitHub projects.
+ class IssuableFinder
+ attr_reader :project, :object
+
+ # The base cache key to use for storing/retrieving issuable IDs.
+ CACHE_KEY = 'github-import/issuable-finder/%{project}/%{type}/%{iid}'.freeze
+
+ # project - An instance of `Project`.
+ # object - The object to look up or set a database ID for.
+ def initialize(project, object)
+ @project = project
+ @object = object
+ end
+
+ # Returns the database ID for the object.
+ #
+ # This method will return `nil` if no ID could be found.
+ def database_id
+ val = Caching.read(cache_key)
+
+ val.to_i if val.present?
+ end
+
+ # Associates the given database ID with the current object.
+ #
+ # database_id - The ID of the corresponding database row.
+ def cache_database_id(database_id)
+ Caching.write(cache_key, database_id)
+ end
+
+ private
+
+ def cache_key
+ CACHE_KEY % {
+ project: project.id,
+ type: cache_key_type,
+ iid: cache_key_iid
+ }
+ end
+
+ # Returns the identifier to use for cache keys.
+ #
+ # For issues and pull requests this will be "Issue" or "MergeRequest"
+ # respectively. For diff notes this will return "MergeRequest", for
+ # regular notes it will either return "Issue" or "MergeRequest" depending
+ # on what type of object the note belongs to.
+ def cache_key_type
+ if object.respond_to?(:issuable_type)
+ object.issuable_type
+ elsif object.respond_to?(:noteable_type)
+ object.noteable_type
+ else
+ raise(
+ TypeError,
+ "Instances of #{object.class} are not supported"
+ )
+ end
+ end
+
+ def cache_key_iid
+ if object.respond_to?(:noteable_id)
+ object.noteable_id
+ elsif object.respond_to?(:iid)
+ object.iid
+ else
+ raise(
+ TypeError,
+ "Instances of #{object.class} are not supported"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/label_finder.rb b/lib/gitlab/github_import/label_finder.rb
new file mode 100644
index 00000000000..9be071141db
--- /dev/null
+++ b/lib/gitlab/github_import/label_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class LabelFinder
+ attr_reader :project
+
+ # The base cache key to use for storing/retrieving label IDs.
+ CACHE_KEY = 'github-import/label-finder/%{project}/%{name}'.freeze
+
+ # project - An instance of `Project`.
+ def initialize(project)
+ @project = project
+ end
+
+ # Returns the label ID for the given name.
+ def id_for(name)
+ Caching.read_integer(cache_key_for(name))
+ end
+
+ def build_cache
+ mapping = @project
+ .labels
+ .pluck(:id, :name)
+ .each_with_object({}) do |(id, name), hash|
+ hash[cache_key_for(name)] = id
+ end
+
+ Caching.write_multiple(mapping)
+ end
+
+ def cache_key_for(name)
+ CACHE_KEY % { project: project.id, name: name }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/markdown_text.rb b/lib/gitlab/github_import/markdown_text.rb
new file mode 100644
index 00000000000..b25c4f7becf
--- /dev/null
+++ b/lib/gitlab/github_import/markdown_text.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class MarkdownText
+ attr_reader :text, :author, :exists
+
+ def self.format(*args)
+ new(*args).to_s
+ end
+
+ # text - The Markdown text as a String.
+ # author - An instance of `Gitlab::GithubImport::Representation::User`
+ # exists - Boolean that indicates the user exists in the GitLab database.
+ def initialize(text, author, exists = false)
+ @text = text
+ @author = author
+ @exists = exists
+ end
+
+ def to_s
+ if exists
+ text
+ else
+ "*Created by: #{author.login}*\n\n#{text}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/milestone_finder.rb b/lib/gitlab/github_import/milestone_finder.rb
new file mode 100644
index 00000000000..208d15dc144
--- /dev/null
+++ b/lib/gitlab/github_import/milestone_finder.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class MilestoneFinder
+ attr_reader :project
+
+ # The base cache key to use for storing/retrieving milestone IDs.
+ CACHE_KEY = 'github-import/milestone-finder/%{project}/%{iid}'.freeze
+
+ # project - An instance of `Project`
+ def initialize(project)
+ @project = project
+ end
+
+ # issuable - An instance of `Gitlab::GithubImport::Representation::Issue`
+ # or `Gitlab::GithubImport::Representation::PullRequest`.
+ def id_for(issuable)
+ return unless issuable.milestone_number
+
+ Caching.read_integer(cache_key_for(issuable.milestone_number))
+ end
+
+ def build_cache
+ mapping = @project
+ .milestones
+ .pluck(:id, :iid)
+ .each_with_object({}) do |(id, iid), hash|
+ hash[cache_key_for(iid)] = id
+ end
+
+ Caching.write_multiple(mapping)
+ end
+
+ def cache_key_for(iid)
+ CACHE_KEY % { project: project.id, iid: iid }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/page_counter.rb b/lib/gitlab/github_import/page_counter.rb
new file mode 100644
index 00000000000..c3db2d0b469
--- /dev/null
+++ b/lib/gitlab/github_import/page_counter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # PageCounter can be used to keep track of the last imported page of a
+ # collection, allowing workers to resume where they left off in the event of
+ # an error.
+ class PageCounter
+ attr_reader :cache_key
+
+ # The base cache key to use for storing the last page number.
+ CACHE_KEY = 'github-importer/page-counter/%{project}/%{collection}'.freeze
+
+ def initialize(project, collection)
+ @cache_key = CACHE_KEY % { project: project.id, collection: collection }
+ end
+
+ # Sets the page number to the given value.
+ #
+ # Returns true if the page number was overwritten, false otherwise.
+ def set(page)
+ Caching.write_if_greater(cache_key, page)
+ end
+
+ # Returns the current value from the cache.
+ def current
+ Caching.read_integer(cache_key) || 1
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb
new file mode 100644
index 00000000000..6da11e6ef08
--- /dev/null
+++ b/lib/gitlab/github_import/parallel_importer.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # The ParallelImporter schedules the importing of a GitHub project using
+ # Sidekiq.
+ class ParallelImporter
+ attr_reader :project
+
+ def self.async?
+ true
+ end
+
+ def self.imports_repository?
+ true
+ end
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ jid = generate_jid
+
+ # The original import JID is the JID of the RepositoryImportWorker job,
+ # which will be removed once that job completes. Reusing that JID could
+ # result in StuckImportJobsWorker marking the job as stuck before we get
+ # to running Stage::ImportRepositoryWorker.
+ #
+ # We work around this by setting the JID to a custom generated one, then
+ # refreshing it in the various stages whenever necessary.
+ Gitlab::SidekiqStatus
+ .set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+
+ project.update_column(:import_jid, jid)
+
+ Stage::ImportRepositoryWorker
+ .perform_async(project.id)
+
+ true
+ end
+
+ def generate_jid
+ "github-importer/#{project.id}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
new file mode 100644
index 00000000000..d4d1357f5a3
--- /dev/null
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module ParallelScheduling
+ attr_reader :project, :client, :page_counter, :already_imported_cache_key
+
+ # The base cache key to use for tracking already imported objects.
+ ALREADY_IMPORTED_CACHE_KEY =
+ 'github-importer/already-imported/%{project}/%{collection}'.freeze
+
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ # parallel - When set to true the objects will be imported in parallel.
+ def initialize(project, client, parallel: true)
+ @project = project
+ @client = client
+ @parallel = parallel
+ @page_counter = PageCounter.new(project, collection_method)
+ @already_imported_cache_key = ALREADY_IMPORTED_CACHE_KEY %
+ { project: project.id, collection: collection_method }
+ end
+
+ def parallel?
+ @parallel
+ end
+
+ def execute
+ retval =
+ if parallel?
+ parallel_import
+ else
+ sequential_import
+ end
+
+ # Once we have completed all work we can remove our "already exists"
+ # cache so we don't put too much pressure on Redis.
+ #
+ # We don't immediately remove it since it's technically possible for
+ # other instances of this job to still run, instead we set the
+ # expiration time to a lower value. This prevents the other jobs from
+ # still scheduling duplicates while. Since all work has already been
+ # completed those jobs will just cycle through any remaining pages while
+ # not scheduling anything.
+ Caching.expire(already_imported_cache_key, 15.minutes.to_i)
+
+ retval
+ end
+
+ # Imports all the objects in sequence in the current thread.
+ def sequential_import
+ each_object_to_import do |object|
+ repr = representation_class.from_api_response(object)
+
+ importer_class.new(repr, project, client).execute
+ end
+ end
+
+ # Imports all objects in parallel by scheduling a Sidekiq job for every
+ # individual object.
+ def parallel_import
+ waiter = JobWaiter.new
+
+ each_object_to_import do |object|
+ repr = representation_class.from_api_response(object)
+
+ sidekiq_worker_class
+ .perform_async(project.id, repr.to_hash, waiter.key)
+
+ waiter.jobs_remaining += 1
+ end
+
+ waiter
+ end
+
+ # The method that will be called for traversing through all the objects to
+ # import, yielding them to the supplied block.
+ def each_object_to_import
+ repo = project.import_source
+
+ # We inject the page number here to make sure that all importers always
+ # start where they left off. Simply starting over wouldn't work for
+ # repositories with a lot of data (e.g. tens of thousands of comments).
+ options = collection_options.merge(page: page_counter.current)
+
+ client.each_page(collection_method, repo, options) do |page|
+ # Technically it's possible that the same work is performed multiple
+ # times, as Sidekiq doesn't guarantee there will ever only be one
+ # instance of a job. In such a scenario it's possible for one job to
+ # have a lower page number (e.g. 5) compared to another (e.g. 10). In
+ # this case we skip over all the objects until we have caught up,
+ # reducing the number of duplicate jobs scheduled by the provided
+ # block.
+ next unless page_counter.set(page.number)
+
+ page.objects.each do |object|
+ next if already_imported?(object)
+
+ yield object
+
+ # We mark the object as imported immediately so we don't end up
+ # scheduling it multiple times.
+ mark_as_imported(object)
+ end
+ end
+ end
+
+ # Returns true if the given object has already been imported, false
+ # otherwise.
+ #
+ # object - The object to check.
+ def already_imported?(object)
+ id = id_for_already_imported_cache(object)
+
+ Caching.set_includes?(already_imported_cache_key, id)
+ end
+
+ # Marks the given object as "already imported".
+ def mark_as_imported(object)
+ id = id_for_already_imported_cache(object)
+
+ Caching.set_add(already_imported_cache_key, id)
+ end
+
+ # Returns the ID to use for the cache used for checking if an object has
+ # already been imported or not.
+ #
+ # object - The object we may want to import.
+ def id_for_already_imported_cache(object)
+ raise NotImplementedError
+ end
+
+ # The class used for converting API responses to Hashes when performing
+ # the import.
+ def representation_class
+ raise NotImplementedError
+ end
+
+ # The class to use for importing objects when importing them sequentially.
+ def importer_class
+ raise NotImplementedError
+ end
+
+ # The Sidekiq worker class used for scheduling the importing of objects in
+ # parallel.
+ def sidekiq_worker_class
+ raise NotImplementedError
+ end
+
+ # The name of the method to call to retrieve the data to import.
+ def collection_method
+ raise NotImplementedError
+ end
+
+ # Any options to be passed to the method used for retrieving the data to
+ # import.
+ def collection_options
+ {}
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/rate_limit_error.rb b/lib/gitlab/github_import/rate_limit_error.rb
new file mode 100644
index 00000000000..cc2de909c29
--- /dev/null
+++ b/lib/gitlab/github_import/rate_limit_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # Error that will be raised when we're about to reach (or have reached) the
+ # GitHub API's rate limit.
+ RateLimitError = Class.new(StandardError)
+ end
+end
diff --git a/lib/gitlab/github_import/representation.rb b/lib/gitlab/github_import/representation.rb
new file mode 100644
index 00000000000..639477ef2a2
--- /dev/null
+++ b/lib/gitlab/github_import/representation.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ TIMESTAMP_KEYS = %i[created_at updated_at merged_at].freeze
+
+ # Converts a Hash with String based keys to one that can be used by the
+ # various Representation classes.
+ #
+ # Example:
+ #
+ # Representation.symbolize_hash('number' => 10) # => { number: 10 }
+ def self.symbolize_hash(raw_hash = nil)
+ hash = raw_hash.deep_symbolize_keys
+
+ TIMESTAMP_KEYS.each do |key|
+ hash[key] = Time.parse(hash[key]) if hash[key].is_a?(String)
+ end
+
+ hash
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb
new file mode 100644
index 00000000000..bb7439a0641
--- /dev/null
+++ b/lib/gitlab/github_import/representation/diff_note.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class DiffNote
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :noteable_type, :noteable_id, :commit_id, :file_path,
+ :diff_hunk, :author, :note, :created_at, :updated_at,
+ :github_id
+
+ NOTEABLE_ID_REGEX = /\/pull\/(?<iid>\d+)/i
+
+ # Builds a diff note from a GitHub API response.
+ #
+ # note - An instance of `Sawyer::Resource` containing the note details.
+ def self.from_api_response(note)
+ matches = note.html_url.match(NOTEABLE_ID_REGEX)
+
+ unless matches
+ raise(
+ ArgumentError,
+ "The note URL #{note.html_url.inspect} is not supported"
+ )
+ end
+
+ user = Representation::User.from_api_response(note.user) if note.user
+ hash = {
+ noteable_type: 'MergeRequest',
+ noteable_id: matches[:iid].to_i,
+ file_path: note.path,
+ commit_id: note.commit_id,
+ diff_hunk: note.diff_hunk,
+ author: user,
+ note: note.body,
+ created_at: note.created_at,
+ updated_at: note.updated_at,
+ github_id: note.id
+ }
+
+ new(hash)
+ end
+
+ # Builds a new note using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ new(hash)
+ end
+
+ # attributes - A Hash containing the raw note details. The keys of this
+ # Hash must be Symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def line_code
+ diff_line = Gitlab::Diff::Parser.new.parse(diff_hunk.lines).to_a.last
+
+ Gitlab::Git
+ .diff_line_code(file_path, diff_line.new_pos, diff_line.old_pos)
+ end
+
+ # Returns a Hash that can be used to populate `notes.st_diff`, removing
+ # the need for requesting Git data for every diff note.
+ def diff_hash
+ {
+ diff: diff_hunk,
+ new_path: file_path,
+ old_path: file_path,
+
+ # These fields are not displayed for LegacyDiffNote notes, so it
+ # doesn't really matter what we set them to.
+ a_mode: '100644',
+ b_mode: '100644',
+ new_file: false
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/expose_attribute.rb b/lib/gitlab/github_import/representation/expose_attribute.rb
new file mode 100644
index 00000000000..c3405759631
--- /dev/null
+++ b/lib/gitlab/github_import/representation/expose_attribute.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ module ExposeAttribute
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Defines getter methods for the given attribute names.
+ #
+ # Example:
+ #
+ # expose_attribute :iid, :title
+ def expose_attribute(*names)
+ names.each do |name|
+ name = name.to_sym
+
+ define_method(name) { attributes[name] }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/issue.rb b/lib/gitlab/github_import/representation/issue.rb
new file mode 100644
index 00000000000..f3071b3e2b3
--- /dev/null
+++ b/lib/gitlab/github_import/representation/issue.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class Issue
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :iid, :title, :description, :milestone_number,
+ :created_at, :updated_at, :state, :assignees,
+ :label_names, :author
+
+ # Builds an issue from a GitHub API response.
+ #
+ # issue - An instance of `Sawyer::Resource` containing the issue
+ # details.
+ def self.from_api_response(issue)
+ user =
+ if issue.user
+ Representation::User.from_api_response(issue.user)
+ end
+
+ hash = {
+ iid: issue.number,
+ title: issue.title,
+ description: issue.body,
+ milestone_number: issue.milestone&.number,
+ state: issue.state == 'open' ? :opened : :closed,
+ assignees: issue.assignees.map do |u|
+ Representation::User.from_api_response(u)
+ end,
+ label_names: issue.labels.map(&:name),
+ author: user,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at,
+ pull_request: issue.pull_request ? true : false
+ }
+
+ new(hash)
+ end
+
+ # Builds a new issue using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+
+ hash[:state] = hash[:state].to_sym
+ hash[:assignees].map! { |u| Representation::User.from_json_hash(u) }
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ new(hash)
+ end
+
+ # attributes - A hash containing the raw issue details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def truncated_title
+ title.truncate(255)
+ end
+
+ def labels?
+ label_names && label_names.any?
+ end
+
+ def pull_request?
+ attributes[:pull_request]
+ end
+
+ def issuable_type
+ pull_request? ? 'MergeRequest' : 'Issue'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/note.rb b/lib/gitlab/github_import/representation/note.rb
new file mode 100644
index 00000000000..a68bc4c002f
--- /dev/null
+++ b/lib/gitlab/github_import/representation/note.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class Note
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :noteable_id, :noteable_type, :author, :note,
+ :created_at, :updated_at, :github_id
+
+ NOTEABLE_TYPE_REGEX = /\/(?<type>(pull|issues))\/(?<iid>\d+)/i
+
+ # Builds a note from a GitHub API response.
+ #
+ # note - An instance of `Sawyer::Resource` containing the note details.
+ def self.from_api_response(note)
+ matches = note.html_url.match(NOTEABLE_TYPE_REGEX)
+
+ if !matches || !matches[:type]
+ raise(
+ ArgumentError,
+ "The note URL #{note.html_url.inspect} is not supported"
+ )
+ end
+
+ noteable_type =
+ if matches[:type] == 'pull'
+ 'MergeRequest'
+ else
+ 'Issue'
+ end
+
+ user = Representation::User.from_api_response(note.user) if note.user
+ hash = {
+ noteable_type: noteable_type,
+ noteable_id: matches[:iid].to_i,
+ author: user,
+ note: note.body,
+ created_at: note.created_at,
+ updated_at: note.updated_at,
+ github_id: note.id
+ }
+
+ new(hash)
+ end
+
+ # Builds a new note using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ new(hash)
+ end
+
+ # attributes - A Hash containing the raw note details. The keys of this
+ # Hash must be Symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ alias_method :issuable_type, :noteable_type
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/pull_request.rb b/lib/gitlab/github_import/representation/pull_request.rb
new file mode 100644
index 00000000000..593b491a837
--- /dev/null
+++ b/lib/gitlab/github_import/representation/pull_request.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class PullRequest
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :iid, :title, :description, :source_branch,
+ :source_branch_sha, :target_branch, :target_branch_sha,
+ :milestone_number, :author, :assignee, :created_at,
+ :updated_at, :merged_at, :source_repository_id,
+ :target_repository_id, :source_repository_owner
+
+ # Builds a PR from a GitHub API response.
+ #
+ # issue - An instance of `Sawyer::Resource` containing the PR details.
+ def self.from_api_response(pr)
+ assignee =
+ if pr.assignee
+ Representation::User.from_api_response(pr.assignee)
+ end
+
+ user = Representation::User.from_api_response(pr.user) if pr.user
+ hash = {
+ iid: pr.number,
+ title: pr.title,
+ description: pr.body,
+ source_branch: pr.head.ref,
+ target_branch: pr.base.ref,
+ source_branch_sha: pr.head.sha,
+ target_branch_sha: pr.base.sha,
+ source_repository_id: pr.head&.repo&.id,
+ target_repository_id: pr.base&.repo&.id,
+ source_repository_owner: pr.head&.user&.login,
+ state: pr.state == 'open' ? :opened : :closed,
+ milestone_number: pr.milestone&.number,
+ author: user,
+ assignee: assignee,
+ created_at: pr.created_at,
+ updated_at: pr.updated_at,
+ merged_at: pr.merged_at
+ }
+
+ new(hash)
+ end
+
+ # Builds a new PR using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+
+ hash[:state] = hash[:state].to_sym
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ # Assignees are optional so we only convert it from a Hash if one was
+ # set.
+ hash[:assignee] &&= Representation::User
+ .from_json_hash(hash[:assignee])
+
+ new(hash)
+ end
+
+ # attributes - A Hash containing the raw PR details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def truncated_title
+ title.truncate(255)
+ end
+
+ # Returns a formatted source branch.
+ #
+ # For cross-project pull requests the branch name will be in the format
+ # `owner-name:branch-name`.
+ def formatted_source_branch
+ if cross_project? && source_repository_owner
+ "#{source_repository_owner}:#{source_branch}"
+ elsif source_branch == target_branch
+ # Sometimes the source and target branch are the same, but GitLab
+ # doesn't support this. This can happen when both the user and
+ # source repository have been deleted, and the PR was submitted from
+ # the fork's master branch.
+ "#{source_branch}-#{iid}"
+ else
+ source_branch
+ end
+ end
+
+ def state
+ if merged_at
+ :merged
+ else
+ attributes[:state]
+ end
+ end
+
+ def cross_project?
+ return true unless source_repository_id
+
+ source_repository_id != target_repository_id
+ end
+
+ def issuable_type
+ 'MergeRequest'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/to_hash.rb b/lib/gitlab/github_import/representation/to_hash.rb
new file mode 100644
index 00000000000..4a0f36ab8f0
--- /dev/null
+++ b/lib/gitlab/github_import/representation/to_hash.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ module ToHash
+ # Converts the current representation to a Hash. The keys of this Hash
+ # will be Symbols.
+ def to_hash
+ hash = {}
+
+ attributes.each do |key, value|
+ hash[key] = convert_value_for_to_hash(value)
+ end
+
+ hash
+ end
+
+ def convert_value_for_to_hash(value)
+ if value.is_a?(Array)
+ value.map { |v| convert_value_for_to_hash(v) }
+ elsif value.respond_to?(:to_hash)
+ value.to_hash
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/user.rb b/lib/gitlab/github_import/representation/user.rb
new file mode 100644
index 00000000000..e00dcfca33d
--- /dev/null
+++ b/lib/gitlab/github_import/representation/user.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class User
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :id, :login
+
+ # Builds a user from a GitHub API response.
+ #
+ # user - An instance of `Sawyer::Resource` containing the user details.
+ def self.from_api_response(user)
+ new(id: user.id, login: user.login)
+ end
+
+ # Builds a user using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ new(Representation.symbolize_hash(raw_hash))
+ end
+
+ # attributes - A Hash containing the user details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/sequential_importer.rb b/lib/gitlab/github_import/sequential_importer.rb
new file mode 100644
index 00000000000..4f7324536a0
--- /dev/null
+++ b/lib/gitlab/github_import/sequential_importer.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # The SequentialImporter imports a GitHub project in a single thread,
+ # without using Sidekiq. This makes it useful for testing purposes as well
+ # as Rake tasks, but it should be avoided for anything else in favour of the
+ # parallel importer.
+ class SequentialImporter
+ attr_reader :project, :client
+
+ SEQUENTIAL_IMPORTERS = [
+ Importer::LabelsImporter,
+ Importer::MilestonesImporter,
+ Importer::ReleasesImporter
+ ].freeze
+
+ PARALLEL_IMPORTERS = [
+ Importer::PullRequestsImporter,
+ Importer::IssuesImporter,
+ Importer::DiffNotesImporter,
+ Importer::NotesImporter
+ ].freeze
+
+ # project - The project to import the data into.
+ # token - The token to use for the GitHub API.
+ def initialize(project, token: nil)
+ @project = project
+ @client = GithubImport
+ .new_client_for(project, token: token, parallel: false)
+ end
+
+ def execute
+ Importer::RepositoryImporter.new(project, client).execute
+
+ SEQUENTIAL_IMPORTERS.each do |klass|
+ klass.new(project, client).execute
+ end
+
+ PARALLEL_IMPORTERS.each do |klass|
+ klass.new(project, client, parallel: false).execute
+ end
+
+ project.repository.after_import
+
+ true
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
new file mode 100644
index 00000000000..be1259662a7
--- /dev/null
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # Class that can be used for finding a GitLab user ID based on a GitHub user
+ # ID or username.
+ #
+ # Any found user IDs are cached in Redis to reduce the number of SQL queries
+ # executed over time. Valid keys are refreshed upon access so frequently
+ # used keys stick around.
+ #
+ # Lookups are cached even if no ID was found to remove the need for querying
+ # the database when most queries are not going to return results anyway.
+ class UserFinder
+ attr_reader :project, :client
+
+ # The base cache key to use for caching user IDs for a given GitHub user
+ # ID.
+ ID_CACHE_KEY = 'github-import/user-finder/user-id/%s'.freeze
+
+ # The base cache key to use for caching user IDs for a given GitHub email
+ # address.
+ ID_FOR_EMAIL_CACHE_KEY =
+ 'github-import/user-finder/id-for-email/%s'.freeze
+
+ # The base cache key to use for caching the Email addresses of GitHub
+ # usernames.
+ EMAIL_FOR_USERNAME_CACHE_KEY =
+ 'github-import/user-finder/email-for-username/%s'.freeze
+
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(project, client)
+ @project = project
+ @client = client
+ end
+
+ # Returns the GitLab user ID of an object's author.
+ #
+ # If the object has no author ID we'll use the ID of the GitLab ghost
+ # user.
+ def author_id_for(object)
+ id =
+ if object&.author
+ user_id_for(object.author)
+ else
+ GithubImport.ghost_user_id
+ end
+
+ if id
+ [id, true]
+ else
+ [project.creator_id, false]
+ end
+ end
+
+ # Returns the GitLab user ID of an issuable's assignee.
+ def assignee_id_for(issuable)
+ user_id_for(issuable.assignee) if issuable.assignee
+ end
+
+ # Returns the GitLab user ID for a GitHub user.
+ #
+ # user - An instance of `Gitlab::GithubImport::Representation::User`.
+ def user_id_for(user)
+ find(user.id, user.login)
+ end
+
+ # Returns the GitLab ID for the given GitHub ID or username.
+ #
+ # id - The ID of the GitHub user.
+ # username - The username of the GitHub user.
+ def find(id, username)
+ email = email_for_github_username(username)
+ cached, found_id = find_from_cache(id, email)
+
+ return found_id if found_id
+
+ # We only want to query the database if necessary. If previous lookups
+ # didn't yield a user ID we won't query the database again until the
+ # keys expire.
+ find_id_from_database(id, email) unless cached
+ end
+
+ # Finds a user ID from the cache for a given GitHub ID or Email.
+ def find_from_cache(id, email = nil)
+ id_exists, id_for_github_id = cached_id_for_github_id(id)
+
+ return [id_exists, id_for_github_id] if id_for_github_id
+
+ # Just in case no Email address could be retrieved (for whatever reason)
+ return [false] unless email
+
+ cached_id_for_github_email(email)
+ end
+
+ # Finds a GitLab user ID from the database for a given GitHub user ID or
+ # Email.
+ def find_id_from_database(id, email)
+ id_for_github_id(id) || id_for_github_email(email)
+ end
+
+ def email_for_github_username(username)
+ cache_key = EMAIL_FOR_USERNAME_CACHE_KEY % username
+ email = Caching.read(cache_key)
+
+ unless email
+ user = client.user(username)
+ email = Caching.write(cache_key, user.email) if user
+ end
+
+ email
+ end
+
+ def cached_id_for_github_id(id)
+ read_id_from_cache(ID_CACHE_KEY % id)
+ end
+
+ def cached_id_for_github_email(email)
+ read_id_from_cache(ID_FOR_EMAIL_CACHE_KEY % email)
+ end
+
+ # Queries and caches the GitLab user ID for a GitHub user ID, if one was
+ # found.
+ def id_for_github_id(id)
+ gitlab_id = query_id_for_github_id(id) || nil
+
+ Caching.write(ID_CACHE_KEY % id, gitlab_id)
+ end
+
+ # Queries and caches the GitLab user ID for a GitHub email, if one was
+ # found.
+ def id_for_github_email(email)
+ gitlab_id = query_id_for_github_email(email) || nil
+
+ Caching.write(ID_FOR_EMAIL_CACHE_KEY % email, gitlab_id)
+ end
+
+ def query_id_for_github_id(id)
+ User.for_github_id(id).pluck(:id).first
+ end
+
+ def query_id_for_github_email(email)
+ User.by_any_email(email).pluck(:id).first
+ end
+
+ # Reads an ID from the cache.
+ #
+ # The return value is an Array with two values:
+ #
+ # 1. A boolean indicating if the key was present or not.
+ # 2. The ID as an Integer, or nil in case no ID could be found.
+ def read_id_from_cache(key)
+ value = Caching.read(key)
+ exists = !value.nil?
+ number = value.to_i
+
+ # The cache key may be empty to indicate a previously looked up user for
+ # which we couldn't find an ID.
+ [exists, number.positive? ? number : nil]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index f1007daab5d..075b3982608 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -65,6 +65,7 @@ module Gitlab
y << item
end
break if items.empty? || items.size < per_page
+
page += 1
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 3a666c2268b..dfcdfc307b6 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -20,7 +20,7 @@ module Gitlab
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab::REVISION
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
- gon.sprite_icons = ActionController::Base.helpers.asset_path('icons.svg')
+ gon.sprite_icons = IconsHelper.sprite_icon_path
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
index de9cab80a02..e29dd0d5b0e 100644
--- a/lib/gitlab/hook_data/issue_builder.rb
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -4,7 +4,6 @@ module Gitlab
SAFE_HOOK_ATTRIBUTES = %i[
assignee_id
author_id
- branch_name
closed_at
confidential
created_at
@@ -29,6 +28,7 @@ module Gitlab
SAFE_HOOK_RELATIONS = %i[
assignees
labels
+ total_time_spent
].freeze
attr_accessor :issue
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
index eaef19c9d04..ae9b68eb648 100644
--- a/lib/gitlab/hook_data/merge_request_builder.rb
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -19,7 +19,6 @@ module Gitlab
merge_user_id
merge_when_pipeline_succeeds
milestone_id
- ref_fetched
source_branch
source_project_id
state
@@ -34,6 +33,7 @@ module Gitlab
SAFE_HOOK_RELATIONS = %i[
assignee
labels
+ total_time_spent
].freeze
attr_accessor :merge_request
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index dec8b4c5acd..263599831bf 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -19,6 +19,7 @@ project_tree:
- milestone:
- events:
- :push_event_payload
+ - :issue_assignees
- snippets:
- :award_emoji
- notes:
@@ -53,7 +54,6 @@ project_tree:
- :auto_devops
- :triggers
- :pipeline_schedules
- - :cluster
- :services
- :hooks
- protected_branches:
@@ -62,6 +62,7 @@ project_tree:
- protected_tags:
- :create_access_levels
- :project_feature
+ - :custom_attributes
# Only include the following attributes for the models specified.
included_attributes:
@@ -113,6 +114,7 @@ excluded_attributes:
- :milestone_id
- :ref_fetched
- :merge_jid
+ - :latest_merge_request_diff_id
award_emoji:
- :awardable_id
statuses:
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index fbdd74788bc..c14646b0611 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -1,6 +1,10 @@
module Gitlab
module ImportExport
class Importer
+ def self.imports_repository?
+ true
+ end
+
def initialize(project)
@archive_file = project.import_source
@current_user = project.creator
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
index 81a213e8321..f3d7407383c 100644
--- a/lib/gitlab/import_export/merge_request_parser.rb
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -1,7 +1,7 @@
module Gitlab
module ImportExport
class MergeRequestParser
- FORKED_PROJECT_ID = -1
+ FORKED_PROJECT_ID = nil
def initialize(project, diff_head_sha, merge_request, relation_hash)
@project = project
@@ -26,7 +26,7 @@ module Gitlab
end
def fetch_ref
- @project.repository.fetch_ref(@project.repository.path, @diff_head_sha, @merge_request.source_branch)
+ @project.repository.fetch_ref(@project.repository, source_ref: @diff_head_sha, target_ref: @merge_request.source_branch)
end
def branch_exists?(branch_name)
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 469b230377d..2b34ceb5831 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -8,8 +8,6 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
- cluster: 'Gcp::Cluster',
- clusters: 'Gcp::Cluster',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
@@ -17,7 +15,8 @@ module Gitlab
labels: :project_labels,
priorities: :label_priorities,
auto_devops: :project_auto_devops,
- label: :project_label }.freeze
+ label: :project_label,
+ custom_attributes: 'ProjectCustomAttribute' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 5404dc11a87..eeb03625479 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -8,14 +8,14 @@ module Gitlab
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
- ImportSource.new('github', 'GitHub', Github::Import),
+ ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
ImportSource.new('git', 'Repo by URL', nil),
ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
- ImportSource.new('gitea', 'Gitea', Gitlab::GithubImport::Importer)
+ ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer)
].freeze
class << self
diff --git a/lib/gitlab/issuable_metadata.rb b/lib/gitlab/issuable_metadata.rb
index 977c05910d3..0c9de72329c 100644
--- a/lib/gitlab/issuable_metadata.rb
+++ b/lib/gitlab/issuable_metadata.rb
@@ -1,6 +1,14 @@
module Gitlab
module IssuableMetadata
def issuable_meta_data(issuable_collection, collection_type)
+ # ActiveRecord uses Object#extend for null relations.
+ if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) &&
+ issuable_collection.respond_to?(:limit_value) &&
+ issuable_collection.limit_value.nil?
+
+ raise 'Collection must have a limit applied for preloading meta-data'
+ end
+
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index 4d6bbda15f3..f654508c391 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -19,11 +19,13 @@ module Gitlab
Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) }
end
- attr_reader :key, :jobs_remaining, :finished
+ attr_reader :key, :finished
+ attr_accessor :jobs_remaining
# jobs_remaining - the number of jobs left to wait for
- def initialize(jobs_remaining)
- @key = "gitlab:job_waiter:#{SecureRandom.uuid}"
+ # key - The key of this waiter.
+ def initialize(jobs_remaining = 0, key = "gitlab:job_waiter:#{SecureRandom.uuid}")
+ @key = key
@jobs_remaining = jobs_remaining
@finished = []
end
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
new file mode 100644
index 00000000000..7a50f07f3c5
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -0,0 +1,96 @@
+module Gitlab
+ module Kubernetes
+ class Helm
+ HELM_VERSION = '2.7.0'.freeze
+ NAMESPACE = 'gitlab-managed-apps'.freeze
+ INSTALL_DEPS = <<-EOS.freeze
+ set -eo pipefail
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ EOS
+
+ InstallCommand = Struct.new(:name, :install_helm, :chart) do
+ def pod_name
+ "install-#{name}"
+ end
+ end
+
+ def initialize(kubeclient)
+ @kubeclient = kubeclient
+ @namespace = Namespace.new(NAMESPACE, kubeclient)
+ end
+
+ def install(command)
+ @namespace.ensure_exists!
+ @kubeclient.create_pod(pod_resource(command))
+ end
+
+ ##
+ # Returns Pod phase
+ #
+ # https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
+ #
+ # values: "Pending", "Running", "Succeeded", "Failed", "Unknown"
+ #
+ def installation_status(pod_name)
+ @kubeclient.get_pod(pod_name, @namespace.name).status.phase
+ end
+
+ def installation_log(pod_name)
+ @kubeclient.get_pod_log(pod_name, @namespace.name).body
+ end
+
+ def delete_installation_pod!(pod_name)
+ @kubeclient.delete_pod(pod_name, @namespace.name)
+ end
+
+ private
+
+ def pod_resource(command)
+ labels = { 'gitlab.org/action': 'install', 'gitlab.org/application': command.name }
+ metadata = { name: command.pod_name, namespace: @namespace.name, labels: labels }
+ container = {
+ name: 'helm',
+ image: 'alpine:3.6',
+ env: generate_pod_env(command),
+ command: %w(/bin/sh),
+ args: %w(-c $(COMMAND_SCRIPT))
+ }
+ spec = { containers: [container], restartPolicy: 'Never' }
+
+ ::Kubeclient::Resource.new(metadata: metadata, spec: spec)
+ end
+
+ def generate_pod_env(command)
+ {
+ HELM_VERSION: HELM_VERSION,
+ TILLER_NAMESPACE: @namespace.name,
+ COMMAND_SCRIPT: generate_script(command)
+ }.map { |key, value| { name: key, value: value } }
+ end
+
+ def generate_script(command)
+ [
+ INSTALL_DEPS,
+ helm_init_command(command),
+ helm_install_command(command)
+ ].join("\n")
+ end
+
+ def helm_init_command(command)
+ if command.install_helm
+ 'helm init >/dev/null'
+ else
+ 'helm init --client-only >/dev/null'
+ end
+ end
+
+ def helm_install_command(command)
+ return if command.chart.nil?
+
+ "helm install #{command.chart} --name #{command.name} --namespace #{@namespace.name} >/dev/null"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb
new file mode 100644
index 00000000000..fbbddb7bffa
--- /dev/null
+++ b/lib/gitlab/kubernetes/namespace.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Kubernetes
+ class Namespace
+ attr_accessor :name
+
+ def initialize(name, client)
+ @name = name
+ @client = client
+ end
+
+ def exists?
+ @client.get_namespace(name)
+ rescue ::KubeException => ke
+ raise ke unless ke.error_code == 404
+
+ false
+ end
+
+ def create!
+ resource = ::Kubeclient::Resource.new(metadata: { name: name })
+
+ @client.create_namespace(resource)
+ end
+
+ def ensure_exists!
+ exists? || create!
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/pod.rb b/lib/gitlab/kubernetes/pod.rb
new file mode 100644
index 00000000000..f3842cdf762
--- /dev/null
+++ b/lib/gitlab/kubernetes/pod.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ module Kubernetes
+ module Pod
+ PENDING = 'Pending'.freeze
+ RUNNING = 'Running'.freeze
+ SUCCEEDED = 'Succeeded'.freeze
+ FAILED = 'Failed'.freeze
+ UNKNOWN = 'Unknown'.freeze
+ PHASES = [PENDING, RUNNING, SUCCEEDED, FAILED, UNKNOWN].freeze
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 3123da17fd9..1bd0965679a 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -4,7 +4,7 @@ module Gitlab
module LDAP
class AuthHash < Gitlab::OAuth::AuthHash
def uid
- Gitlab::LDAP::Person.normalize_dn(super)
+ @uid ||= Gitlab::LDAP::Person.normalize_dn(super)
end
private
diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb
index ed1de73f8c6..7274d1c3b43 100644
--- a/lib/gitlab/ldap/authentication.rb
+++ b/lib/gitlab/ldap/authentication.rb
@@ -62,6 +62,7 @@ module Gitlab
def user
return nil unless ldap_user
+
Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
end
end
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 1793097363e..3945df27eed 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -9,10 +9,8 @@ module Gitlab
class User < Gitlab::OAuth::User
class << self
def find_by_uid_and_provider(uid, provider)
- # LDAP distinguished name is case-insensitive
- identity = ::Identity
- .where(provider: provider)
- .iwhere(extern_uid: uid).last
+ identity = ::Identity.with_extern_uid(provider, uid).take
+
identity && identity.user
end
end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/legacy_github_import/base_formatter.rb
index f330041cc00..2f07fde406c 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/legacy_github_import/base_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class BaseFormatter
attr_reader :client, :formatter, :project, :raw_data
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/legacy_github_import/branch_formatter.rb
index 8aa885fb811..80fe1d67209 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/legacy_github_import/branch_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class BranchFormatter < BaseFormatter
delegate :repo, :sha, :ref, to: :raw_data
diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb
new file mode 100644
index 00000000000..53c910d44bd
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/client.rb
@@ -0,0 +1,148 @@
+module Gitlab
+ module LegacyGithubImport
+ class Client
+ GITHUB_SAFE_REMAINING_REQUESTS = 100
+ GITHUB_SAFE_SLEEP_TIME = 500
+
+ attr_reader :access_token, :host, :api_version
+
+ def initialize(access_token, host: nil, api_version: 'v3')
+ @access_token = access_token
+ @host = host.to_s.sub(%r{/+\z}, '')
+ @api_version = api_version
+ @users = {}
+
+ if access_token
+ ::Octokit.auto_paginate = false
+ end
+ end
+
+ def api
+ @api ||= ::Octokit::Client.new(
+ access_token: access_token,
+ api_endpoint: api_endpoint,
+ # If there is no config, we're connecting to github.com and we
+ # should verify ssl.
+ connection_options: {
+ ssl: { verify: config ? config['verify_ssl'] : true }
+ }
+ )
+ end
+
+ def client
+ unless config
+ raise Projects::ImportService::Error,
+ 'OAuth configuration for GitHub missing.'
+ end
+
+ @client ||= ::OAuth2::Client.new(
+ config.app_id,
+ config.app_secret,
+ github_options.merge(ssl: { verify: config['verify_ssl'] })
+ )
+ end
+
+ def authorize_url(redirect_uri)
+ client.auth_code.authorize_url({
+ redirect_uri: redirect_uri,
+ scope: "repo, user, user:email"
+ })
+ end
+
+ def get_token(code)
+ client.auth_code.get_token(code).token
+ end
+
+ def method_missing(method, *args, &block)
+ if api.respond_to?(method)
+ request(method, *args, &block)
+ else
+ super(method, *args, &block)
+ end
+ end
+
+ def respond_to?(method)
+ api.respond_to?(method) || super
+ end
+
+ def user(login)
+ return nil unless login.present?
+ return @users[login] if @users.key?(login)
+
+ @users[login] = api.user(login)
+ end
+
+ private
+
+ def api_endpoint
+ if host.present? && api_version.present?
+ "#{host}/api/#{api_version}"
+ else
+ github_options[:site]
+ end
+ end
+
+ def config
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" }
+ end
+
+ def github_options
+ if config
+ config["args"]["client_options"].deep_symbolize_keys
+ else
+ OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ end
+ end
+
+ def rate_limit
+ api.rate_limit!
+ # GitHub Rate Limit API returns 404 when the rate limit is
+ # disabled. In this case we just want to return gracefully
+ # instead of spitting out an error.
+ rescue Octokit::NotFound
+ nil
+ end
+
+ def has_rate_limit?
+ return @has_rate_limit if defined?(@has_rate_limit)
+
+ @has_rate_limit = rate_limit.present?
+ end
+
+ def rate_limit_exceed?
+ has_rate_limit? && rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS
+ end
+
+ def rate_limit_sleep_time
+ rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
+ end
+
+ def request(method, *args, &block)
+ sleep rate_limit_sleep_time if rate_limit_exceed?
+
+ data = api.__send__(method, *args) # rubocop:disable GitlabSecurity/PublicSend
+ return data unless data.is_a?(Array)
+
+ last_response = api.last_response
+
+ if block_given?
+ yield data
+ # api.last_response could change while we're yielding (e.g. fetching labels for each PR)
+ # so we cache our own last response
+ each_response_page(last_response, &block)
+ else
+ each_response_page(last_response) { |page| data.concat(page) }
+ data
+ end
+ end
+
+ def each_response_page(last_response)
+ while last_response.rels[:next]
+ sleep rate_limit_sleep_time if rate_limit_exceed?
+ last_response = last_response.rels[:next].get
+ yield last_response.data if last_response.data.is_a?(Array)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/legacy_github_import/comment_formatter.rb
index 8911b81ec9a..d2c7a8ae9f4 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/legacy_github_import/comment_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class CommentFormatter < BaseFormatter
attr_writer :author_id
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index b8c07460ebb..4d096e5a741 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class Importer
include Gitlab::ShellAdapter
@@ -15,6 +15,7 @@ module Gitlab
def client
return @client if defined?(@client)
+
unless credentials
raise Projects::ImportService::Error,
"Unable to find project import data credentials for project ID: #{@project.id}"
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/legacy_github_import/issuable_formatter.rb
index 27b171d6ddb..de55382d3ad 100644
--- a/lib/gitlab/github_import/issuable_formatter.rb
+++ b/lib/gitlab/legacy_github_import/issuable_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class IssuableFormatter < BaseFormatter
attr_writer :assignee_id, :author_id
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/legacy_github_import/issue_formatter.rb
index 977cd0423ba..4c8825ccf19 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/legacy_github_import/issue_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class IssueFormatter < IssuableFormatter
def attributes
{
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/legacy_github_import/label_formatter.rb
index 211ccdc51bb..c3eed12e739 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/legacy_github_import/label_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class LabelFormatter < BaseFormatter
def attributes
{
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/legacy_github_import/milestone_formatter.rb
index dd782eff059..a565294384d 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/legacy_github_import/milestone_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class MilestoneFormatter < BaseFormatter
def attributes
{
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb
index a55adc9b1c8..41e7eac4d08 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/legacy_github_import/project_creator.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class ProjectCreator
include Gitlab::CurrentSettings
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/legacy_github_import/pull_request_formatter.rb
index 150afa31432..94c2e99066a 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/legacy_github_import/pull_request_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class PullRequestFormatter < IssuableFormatter
delegate :user, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
delegate :user, :exists?, :project, :ref, :repo, :sha, :short_sha, to: :target_branch, prefix: true
diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/legacy_github_import/release_formatter.rb
index 1ad702a6058..3ed9d4f76da 100644
--- a/lib/gitlab/github_import/release_formatter.rb
+++ b/lib/gitlab/legacy_github_import/release_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class ReleaseFormatter < BaseFormatter
def attributes
{
diff --git a/lib/gitlab/github_import/user_formatter.rb b/lib/gitlab/legacy_github_import/user_formatter.rb
index 04c2964da20..6d8055622f1 100644
--- a/lib/gitlab/github_import/user_formatter.rb
+++ b/lib/gitlab/legacy_github_import/user_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class UserFormatter
attr_reader :client, :raw
diff --git a/lib/gitlab/github_import/wiki_formatter.rb b/lib/gitlab/legacy_github_import/wiki_formatter.rb
index ca8d96f5650..27f45875c7c 100644
--- a/lib/gitlab/github_import/wiki_formatter.rb
+++ b/lib/gitlab/legacy_github_import/wiki_formatter.rb
@@ -1,5 +1,5 @@
module Gitlab
- module GithubImport
+ module LegacyGithubImport
class WikiFormatter
attr_reader :project
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
index 8e57ba831c5..ead5d566871 100644
--- a/lib/gitlab/lfs_token.rb
+++ b/lib/gitlab/lfs_token.rb
@@ -27,6 +27,10 @@ module Gitlab
end
end
+ def deploy_key_pushable?(project)
+ actor.is_a?(DeployKey) && actor.can_push_to?(project)
+ end
+
def user?
actor.is_a?(User)
end
diff --git a/lib/gitlab/metrics/background_transaction.rb b/lib/gitlab/metrics/background_transaction.rb
new file mode 100644
index 00000000000..5919ebb1493
--- /dev/null
+++ b/lib/gitlab/metrics/background_transaction.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Metrics
+ class BackgroundTransaction < Transaction
+ def initialize(worker_class)
+ super()
+ @worker_class = worker_class
+ end
+
+ def labels
+ { controller: @worker_class.name, action: 'perform' }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/base_sampler.rb b/lib/gitlab/metrics/base_sampler.rb
deleted file mode 100644
index 716d20bb91a..00000000000
--- a/lib/gitlab/metrics/base_sampler.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'logger'
-module Gitlab
- module Metrics
- class BaseSampler < Daemon
- # interval - The sampling interval in seconds.
- def initialize(interval)
- interval_half = interval.to_f / 2
-
- @interval = interval
- @interval_steps = (-interval_half..interval_half).step(0.1).to_a
-
- super()
- end
-
- def safe_sample
- sample
- rescue => e
- Rails.logger.warn("#{self.class}: #{e}, stopping")
- stop
- end
-
- def sample
- raise NotImplementedError
- end
-
- # Returns the sleep interval with a random adjustment.
- #
- # The random adjustment is put in place to ensure we:
- #
- # 1. Don't generate samples at the exact same interval every time (thus
- # potentially missing anything that happens in between samples).
- # 2. Don't sample data at the same interval two times in a row.
- def sleep_interval
- while (step = @interval_steps.sample)
- if step != @last_step
- @last_step = step
-
- return @interval + @last_step
- end
- end
- end
-
- private
-
- attr_reader :running
-
- def start_working
- @running = true
- sleep(sleep_interval)
-
- while running
- safe_sample
-
- sleep(sleep_interval)
- end
- end
-
- def stop_working
- @running = false
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index 7b06bb953aa..bdf7910b7c7 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -11,6 +11,8 @@ module Gitlab
settings[:enabled] || false
end
+ # Prometheus histogram buckets used for arbitrary code measurements
+ EXECUTION_MEASUREMENT_BUCKETS = [0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1].freeze
RAILS_ROOT = Rails.root.to_s
METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
PATH_REGEX = /^#{RAILS_ROOT}\/?/
@@ -99,24 +101,27 @@ module Gitlab
cpu_stop = System.cpu_time
real_stop = Time.now.to_f
- real_time = (real_stop - real_start) * 1000.0
+ real_time = (real_stop - real_start)
cpu_time = cpu_stop - cpu_start
- trans.increment("#{name}_real_time", real_time)
- trans.increment("#{name}_cpu_time", cpu_time)
- trans.increment("#{name}_call_count", 1)
+ Gitlab::Metrics.histogram("gitlab_#{name}_real_duration_seconds".to_sym,
+ "Measure #{name}",
+ Transaction::BASE_LABELS,
+ EXECUTION_MEASUREMENT_BUCKETS)
+ .observe(trans.labels, real_time)
- retval
- end
+ Gitlab::Metrics.histogram("gitlab_#{name}_cpu_duration_seconds".to_sym,
+ "Measure #{name}",
+ Transaction::BASE_LABELS,
+ EXECUTION_MEASUREMENT_BUCKETS)
+ .observe(trans.labels, cpu_time / 1000.0)
- # Adds a tag to the current transaction (if any)
- #
- # name - The name of the tag to add.
- # value - The value of the tag.
- def tag_transaction(name, value)
- trans = current_transaction
+ # InfluxDB stores the _real_time time values as milliseconds
+ trans.increment("#{name}_real_time", real_time * 1000, false)
+ trans.increment("#{name}_cpu_time", cpu_time, false)
+ trans.increment("#{name}_call_count", 1, false)
- trans&.add_tag(name, value)
+ retval
end
# Sets the action of the current transaction (if any)
diff --git a/lib/gitlab/metrics/influx_sampler.rb b/lib/gitlab/metrics/influx_sampler.rb
deleted file mode 100644
index 6db1dd755b7..00000000000
--- a/lib/gitlab/metrics/influx_sampler.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-module Gitlab
- module Metrics
- # Class that sends certain metrics to InfluxDB at a specific interval.
- #
- # This class is used to gather statistics that can't be directly associated
- # with a transaction such as system memory usage, garbage collection
- # statistics, etc.
- class InfluxSampler < BaseSampler
- # interval - The sampling interval in seconds.
- def initialize(interval = Metrics.settings[:sample_interval])
- super(interval)
- @last_step = nil
-
- @metrics = []
-
- @last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
- @last_major_gc = Delta.new(GC.stat[:major_gc_count])
-
- if Gitlab::Metrics.mri?
- require 'allocations'
-
- Allocations.start
- end
- end
-
- def sample
- sample_memory_usage
- sample_file_descriptors
- sample_objects
- sample_gc
-
- flush
- ensure
- GC::Profiler.clear
- @metrics.clear
- end
-
- def flush
- Metrics.submit_metrics(@metrics.map(&:to_hash))
- end
-
- def sample_memory_usage
- add_metric('memory_usage', value: System.memory_usage)
- end
-
- def sample_file_descriptors
- add_metric('file_descriptors', value: System.file_descriptor_count)
- end
-
- if Metrics.mri?
- def sample_objects
- sample = Allocations.to_hash
- counts = sample.each_with_object({}) do |(klass, count), hash|
- name = klass.name
-
- next unless name
-
- hash[name] = count
- end
-
- # Symbols aren't allocated so we'll need to add those manually.
- counts['Symbol'] = Symbol.all_symbols.length
-
- counts.each do |name, count|
- add_metric('object_counts', { count: count }, type: name)
- end
- end
- else
- def sample_objects
- end
- end
-
- def sample_gc
- time = GC::Profiler.total_time * 1000.0
- stats = GC.stat.merge(total_time: time)
-
- # We want the difference of GC runs compared to the last sample, not the
- # total amount since the process started.
- stats[:minor_gc_count] =
- @last_minor_gc.compared_with(stats[:minor_gc_count])
-
- stats[:major_gc_count] =
- @last_major_gc.compared_with(stats[:major_gc_count])
-
- stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
-
- add_metric('gc_statistics', stats)
- end
-
- def add_metric(series, values, tags = {})
- prefix = sidekiq? ? 'sidekiq_' : 'rails_'
-
- @metrics << Metric.new("#{prefix}#{series}", values, tags)
- end
-
- def sidekiq?
- Sidekiq.server?
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 6aa38542cb4..023e9963493 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -118,19 +118,21 @@ module Gitlab
def self.instrument(type, mod, name)
return unless Metrics.enabled?
- name = name.to_sym
+ name = name.to_sym
target = type == :instance ? mod : mod.singleton_class
if type == :instance
target = mod
- label = "#{mod.name}##{name}"
+ method_name = "##{name}"
method = mod.instance_method(name)
else
target = mod.singleton_class
- label = "#{mod.name}.#{name}"
+ method_name = ".#{name}"
method = mod.method(name)
end
+ label = "#{mod.name}#{method_name}"
+
unless instrumented?(target)
target.instance_variable_set(PROXY_IVAR, Module.new)
end
@@ -153,7 +155,8 @@ module Gitlab
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
if trans = Gitlab::Metrics::Instrumentation.transaction
- trans.method_call_for(#{label.to_sym.inspect}).measure { super }
+ trans.method_call_for(#{label.to_sym.inspect}, #{mod.name.inspect}, "#{method_name}")
+ .measure { super }
else
super
end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
index d3465e5ec19..90235095306 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -2,15 +2,45 @@ module Gitlab
module Metrics
# Class for tracking timing information about method calls
class MethodCall
- attr_reader :real_time, :cpu_time, :call_count
+ MUTEX = Mutex.new
+ BASE_LABELS = { module: nil, method: nil }.freeze
+ attr_reader :real_time, :cpu_time, :call_count, :labels
+
+ def self.call_real_duration_histogram
+ return @call_real_duration_histogram if @call_real_duration_histogram
+
+ MUTEX.synchronize do
+ @call_real_duration_histogram ||= Gitlab::Metrics.histogram(
+ :gitlab_method_call_real_duration_seconds,
+ 'Method calls real duration',
+ Transaction::BASE_LABELS.merge(BASE_LABELS),
+ [0.1, 0.2, 0.5, 1, 2, 5, 10]
+ )
+ end
+ end
+
+ def self.call_cpu_duration_histogram
+ return @call_cpu_duration_histogram if @call_cpu_duration_histogram
+
+ MUTEX.synchronize do
+ @call_duration_histogram ||= Gitlab::Metrics.histogram(
+ :gitlab_method_call_cpu_duration_seconds,
+ 'Method calls cpu duration',
+ Transaction::BASE_LABELS.merge(BASE_LABELS),
+ [0.1, 0.2, 0.5, 1, 2, 5, 10]
+ )
+ end
+ end
# name - The full name of the method (including namespace) such as
# `User#sign_in`.
#
- # series - The series to use for storing the data.
- def initialize(name, series)
+ def initialize(name, module_name, method_name, transaction)
+ @module_name = module_name
+ @method_name = method_name
+ @transaction = transaction
@name = name
- @series = series
+ @labels = { module: @module_name, method: @method_name }
@real_time = 0
@cpu_time = 0
@call_count = 0
@@ -22,21 +52,27 @@ module Gitlab
start_cpu = System.cpu_time
retval = yield
- @real_time += System.monotonic_time - start_real
- @cpu_time += System.cpu_time - start_cpu
+ real_time = System.monotonic_time - start_real
+ cpu_time = System.cpu_time - start_cpu
+
+ @real_time += real_time
+ @cpu_time += cpu_time
@call_count += 1
+ self.class.call_real_duration_histogram.observe(@transaction.labels.merge(labels), real_time / 1000.0)
+ self.class.call_cpu_duration_histogram.observe(@transaction.labels.merge(labels), cpu_time / 1000.0)
+
retval
end
# Returns a Metric instance of the current method call.
def to_metric
Metric.new(
- @series,
+ Instrumentation.series,
{
- duration: real_time,
+ duration: real_time,
cpu_duration: cpu_time,
- call_count: call_count
+ call_count: call_count
},
method: @name
)
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index 460dab47276..09103b4ca2d 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -5,6 +5,9 @@ module Gitlab
module Prometheus
include Gitlab::CurrentSettings
+ REGISTRY_MUTEX = Mutex.new
+ PROVIDER_MUTEX = Mutex.new
+
def metrics_folder_present?
multiprocess_files_dir = ::Prometheus::Client.configuration.multiprocess_files_dir
@@ -20,23 +23,38 @@ module Gitlab
end
def registry
- @registry ||= ::Prometheus::Client.registry
+ return @registry if @registry
+
+ REGISTRY_MUTEX.synchronize do
+ @registry ||= ::Prometheus::Client.registry
+ end
end
def counter(name, docstring, base_labels = {})
- provide_metric(name) || registry.counter(name, docstring, base_labels)
+ safe_provide_metric(:counter, name, docstring, base_labels)
end
def summary(name, docstring, base_labels = {})
- provide_metric(name) || registry.summary(name, docstring, base_labels)
+ safe_provide_metric(:summary, name, docstring, base_labels)
end
def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
- provide_metric(name) || registry.gauge(name, docstring, base_labels, multiprocess_mode)
+ safe_provide_metric(:gauge, name, docstring, base_labels, multiprocess_mode)
end
def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS)
- provide_metric(name) || registry.histogram(name, docstring, base_labels, buckets)
+ safe_provide_metric(:histogram, name, docstring, base_labels, buckets)
+ end
+
+ private
+
+ def safe_provide_metric(method, name, *args)
+ metric = provide_metric(name)
+ return metric if metric
+
+ PROVIDER_MUTEX.synchronize do
+ provide_metric(name) || registry.method(method).call(name, *args)
+ end
end
def provide_metric(name)
@@ -47,8 +65,6 @@ module Gitlab
end
end
- private
-
def prometheus_metrics_enabled_unmemoized
metrics_folder_present? && current_application_settings[:prometheus_metrics_enabled] || false
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index adc0db1a874..2d45765df3f 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -2,20 +2,6 @@ module Gitlab
module Metrics
# Rack middleware for tracking Rails and Grape requests.
class RackMiddleware
- CONTROLLER_KEY = 'action_controller.instance'.freeze
- ENDPOINT_KEY = 'api.endpoint'.freeze
- CONTENT_TYPES = {
- 'text/html' => :html,
- 'text/plain' => :txt,
- 'application/json' => :json,
- 'text/js' => :js,
- 'application/atom+xml' => :atom,
- 'image/png' => :png,
- 'image/jpeg' => :jpeg,
- 'image/gif' => :gif,
- 'image/svg+xml' => :svg
- }.freeze
-
def initialize(app)
@app = app
end
@@ -35,12 +21,6 @@ module Gitlab
# Even in the event of an error we want to submit any metrics we
# might've gathered up to this point.
ensure
- if env[CONTROLLER_KEY]
- tag_controller(trans, env)
- elsif env[ENDPOINT_KEY]
- tag_endpoint(trans, env)
- end
-
trans.finish
end
@@ -48,60 +28,19 @@ module Gitlab
end
def transaction_from_env(env)
- trans = Transaction.new
+ trans = WebTransaction.new(env)
- trans.set(:request_uri, filtered_path(env))
- trans.set(:request_method, env['REQUEST_METHOD'])
+ trans.set(:request_uri, filtered_path(env), false)
+ trans.set(:request_method, env['REQUEST_METHOD'], false)
trans
end
- def tag_controller(trans, env)
- controller = env[CONTROLLER_KEY]
- action = "#{controller.class.name}##{controller.action_name}"
- suffix = CONTENT_TYPES[controller.content_type]
-
- if suffix && suffix != :html
- action += ".#{suffix}"
- end
-
- trans.action = action
- end
-
- def tag_endpoint(trans, env)
- endpoint = env[ENDPOINT_KEY]
-
- begin
- route = endpoint.route
- rescue
- # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
- # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response
- # so we're rescuing exceptions and bailing out
- end
-
- if route
- path = endpoint_paths_cache[route.request_method][route.path]
- trans.action = "Grape##{route.request_method} #{path}"
- end
- end
-
private
def filtered_path(env)
ActionDispatch::Request.new(env).filtered_path.presence || env['REQUEST_URI']
end
-
- def endpoint_paths_cache
- @endpoint_paths_cache ||= Hash.new do |hash, http_method|
- hash[http_method] = Hash.new do |inner_hash, raw_path|
- inner_hash[raw_path] = endpoint_instrumentable_path(raw_path)
- end
- end
- end
-
- def endpoint_instrumentable_path(raw_path)
- raw_path.sub('(.:format)', '').sub('/:version', '')
- end
end
end
end
diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb
new file mode 100644
index 00000000000..37f90c4673d
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/base_sampler.rb
@@ -0,0 +1,64 @@
+require 'logger'
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class BaseSampler < Daemon
+ # interval - The sampling interval in seconds.
+ def initialize(interval)
+ interval_half = interval.to_f / 2
+
+ @interval = interval
+ @interval_steps = (-interval_half..interval_half).step(0.1).to_a
+
+ super()
+ end
+
+ def safe_sample
+ sample
+ rescue => e
+ Rails.logger.warn("#{self.class}: #{e}, stopping")
+ stop
+ end
+
+ def sample
+ raise NotImplementedError
+ end
+
+ # Returns the sleep interval with a random adjustment.
+ #
+ # The random adjustment is put in place to ensure we:
+ #
+ # 1. Don't generate samples at the exact same interval every time (thus
+ # potentially missing anything that happens in between samples).
+ # 2. Don't sample data at the same interval two times in a row.
+ def sleep_interval
+ while step = @interval_steps.sample
+ if step != @last_step
+ @last_step = step
+
+ return @interval + @last_step
+ end
+ end
+ end
+
+ private
+
+ attr_reader :running
+
+ def start_working
+ @running = true
+ sleep(sleep_interval)
+ while running
+ safe_sample
+ sleep(sleep_interval)
+ end
+ end
+
+ def stop_working
+ @running = false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb
new file mode 100644
index 00000000000..f4f9b5ca792
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/influx_sampler.rb
@@ -0,0 +1,103 @@
+module Gitlab
+ module Metrics
+ module Samplers
+ # Class that sends certain metrics to InfluxDB at a specific interval.
+ #
+ # This class is used to gather statistics that can't be directly associated
+ # with a transaction such as system memory usage, garbage collection
+ # statistics, etc.
+ class InfluxSampler < BaseSampler
+ # interval - The sampling interval in seconds.
+ def initialize(interval = Metrics.settings[:sample_interval])
+ super(interval)
+ @last_step = nil
+
+ @metrics = []
+
+ @last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
+ @last_major_gc = Delta.new(GC.stat[:major_gc_count])
+
+ if Gitlab::Metrics.mri?
+ require 'allocations'
+
+ Allocations.start
+ end
+ end
+
+ def sample
+ sample_memory_usage
+ sample_file_descriptors
+ sample_objects
+ sample_gc
+
+ flush
+ ensure
+ GC::Profiler.clear
+ @metrics.clear
+ end
+
+ def flush
+ Metrics.submit_metrics(@metrics.map(&:to_hash))
+ end
+
+ def sample_memory_usage
+ add_metric('memory_usage', value: System.memory_usage)
+ end
+
+ def sample_file_descriptors
+ add_metric('file_descriptors', value: System.file_descriptor_count)
+ end
+
+ if Metrics.mri?
+ def sample_objects
+ sample = Allocations.to_hash
+ counts = sample.each_with_object({}) do |(klass, count), hash|
+ name = klass.name
+
+ next unless name
+
+ hash[name] = count
+ end
+
+ # Symbols aren't allocated so we'll need to add those manually.
+ counts['Symbol'] = Symbol.all_symbols.length
+
+ counts.each do |name, count|
+ add_metric('object_counts', { count: count }, type: name)
+ end
+ end
+ else
+ def sample_objects
+ end
+ end
+
+ def sample_gc
+ time = GC::Profiler.total_time * 1000.0
+ stats = GC.stat.merge(total_time: time)
+
+ # We want the difference of GC runs compared to the last sample, not the
+ # total amount since the process started.
+ stats[:minor_gc_count] =
+ @last_minor_gc.compared_with(stats[:minor_gc_count])
+
+ stats[:major_gc_count] =
+ @last_major_gc.compared_with(stats[:major_gc_count])
+
+ stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
+
+ add_metric('gc_statistics', stats)
+ end
+
+ def add_metric(series, values, tags = {})
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
new file mode 100644
index 00000000000..436a9e9550d
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -0,0 +1,111 @@
+require 'prometheus/client/support/unicorn'
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class RubySampler < BaseSampler
+ def metrics
+ @metrics ||= init_metrics
+ end
+
+ def with_prefix(prefix, name)
+ "ruby_#{prefix}_#{name}".to_sym
+ end
+
+ def to_doc_string(name)
+ name.to_s.humanize
+ end
+
+ def labels
+ {}
+ end
+
+ def initialize(interval)
+ super(interval)
+
+ if Metrics.mri?
+ require 'allocations'
+
+ Allocations.start
+ end
+ end
+
+ def init_metrics
+ metrics = {}
+ metrics[:sampler_duration] = Metrics.histogram(with_prefix(:sampler_duration, :seconds), 'Sampler time', {})
+ metrics[:total_time] = Metrics.gauge(with_prefix(:gc, :time_total), 'Total GC time', labels, :livesum)
+ GC.stat.keys.each do |key|
+ metrics[key] = Metrics.gauge(with_prefix(:gc, key), to_doc_string(key), labels, :livesum)
+ end
+
+ metrics[:objects_total] = Metrics.gauge(with_prefix(:objects, :total), 'Objects total', labels.merge(class: nil), :livesum)
+ metrics[:memory_usage] = Metrics.gauge(with_prefix(:memory, :usage_total), 'Memory used total', labels, :livesum)
+ metrics[:file_descriptors] = Metrics.gauge(with_prefix(:file, :descriptors_total), 'File descriptors total', labels, :livesum)
+
+ metrics
+ end
+
+ def sample
+ start_time = System.monotonic_time
+ sample_gc
+ sample_objects
+
+ metrics[:memory_usage].set(labels, System.memory_usage)
+ metrics[:file_descriptors].set(labels, System.file_descriptor_count)
+
+ metrics[:sampler_duration].observe(labels.merge(worker_label), (System.monotonic_time - start_time) / 1000.0)
+ ensure
+ GC::Profiler.clear
+ end
+
+ private
+
+ def sample_gc
+ metrics[:total_time].set(labels, GC::Profiler.total_time * 1000)
+
+ GC.stat.each do |key, value|
+ metrics[key].set(labels, value)
+ end
+ end
+
+ def sample_objects
+ list_objects.each do |name, count|
+ metrics[:objects_total].set(labels.merge(class: name), count)
+ end
+ end
+
+ if Metrics.mri?
+ def list_objects
+ sample = Allocations.to_hash
+ counts = sample.each_with_object({}) do |(klass, count), hash|
+ name = klass.name
+
+ next unless name
+
+ hash[name] = count
+ end
+
+ # Symbols aren't allocated so we'll need to add those manually.
+ counts['Symbol'] = Symbol.all_symbols.length
+ counts
+ end
+ else
+ def list_objects
+ end
+ end
+
+ def worker_label
+ return {} unless defined?(Unicorn::Worker)
+
+ worker_no = ::Prometheus::Client::Support::Unicorn.worker_id
+
+ if worker_no
+ { unicorn: worker_no }
+ else
+ { unicorn: 'master' }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
new file mode 100644
index 00000000000..ea325651fbb
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module Metrics
+ module Samplers
+ class UnicornSampler < BaseSampler
+ def initialize(interval)
+ super(interval)
+ end
+
+ def unicorn_active_connections
+ @unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
+ end
+
+ def unicorn_queued_connections
+ @unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
+ end
+
+ def enabled?
+ # Raindrops::Linux.tcp_listener_stats is only present on Linux
+ unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats)
+ end
+
+ def sample
+ Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
+ unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active)
+ unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued)
+ end
+
+ Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
+ unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active)
+ unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued)
+ end
+ end
+
+ private
+
+ def tcp_listeners
+ @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
+ end
+
+ def unix_listeners
+ @unix_listeners ||= Unicorn.listener_names - tcp_listeners
+ end
+
+ def unicorn_with_listeners?
+ defined?(Unicorn) && Unicorn.listener_names.any?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index f9dd8e41912..df4bdf16847 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -5,7 +5,7 @@ module Gitlab
# This middleware is intended to be used as a server-side middleware.
class SidekiqMiddleware
def call(worker, message, queue)
- trans = Transaction.new("#{worker.class.name}#perform")
+ trans = BackgroundTransaction.new(worker.class)
begin
# Old gitlad-shell messages don't provide enqueued_at/created_at attributes
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index d435a33e9c7..3da474fc1ec 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -15,10 +15,24 @@ module Gitlab
private
+ def metric_view_rendering_duration_seconds
+ @metric_view_rendering_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_view_rendering_duration_seconds,
+ 'View rendering time',
+ Transaction::BASE_LABELS.merge({ path: nil }),
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
+
def track(event)
values = values_for(event)
tags = tags_for(event)
+ metric_view_rendering_duration_seconds.observe(
+ current_transaction.labels.merge(tags),
+ event.duration
+ )
+
current_transaction.increment(:view_duration, event.duration)
current_transaction.add_metric(SERIES, values, tags)
end
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index 96cad941d5c..ead1acb8d44 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -8,8 +8,10 @@ module Gitlab
def sql(event)
return unless current_transaction
- current_transaction.increment(:sql_duration, event.duration)
- current_transaction.increment(:sql_count, 1)
+ metric_sql_duration_seconds.observe(current_transaction.labels, event.duration / 1000.0)
+
+ current_transaction.increment(:sql_duration, event.duration, false)
+ current_transaction.increment(:sql_count, 1, false)
end
private
@@ -17,6 +19,15 @@ module Gitlab
def current_transaction
Transaction.current
end
+
+ def metric_sql_duration_seconds
+ @metric_sql_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_sql_duration_seconds,
+ 'SQL time',
+ Transaction::BASE_LABELS,
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb
index aaed2184f44..efd3c9daf79 100644
--- a/lib/gitlab/metrics/subscribers/rails_cache.rb
+++ b/lib/gitlab/metrics/subscribers/rails_cache.rb
@@ -7,28 +7,29 @@ module Gitlab
attach_to :active_support
def cache_read(event)
- increment(:cache_read, event.duration)
+ observe(:read, event.duration)
return unless current_transaction
return if event.payload[:super_operation] == :fetch
if event.payload[:hit]
- current_transaction.increment(:cache_read_hit_count, 1)
+ current_transaction.increment(:cache_read_hit_count, 1, false)
else
- current_transaction.increment(:cache_read_miss_count, 1)
+ metric_cache_misses_total.increment(current_transaction.labels)
+ current_transaction.increment(:cache_read_miss_count, 1, false)
end
end
def cache_write(event)
- increment(:cache_write, event.duration)
+ observe(:write, event.duration)
end
def cache_delete(event)
- increment(:cache_delete, event.duration)
+ observe(:delete, event.duration)
end
def cache_exist?(event)
- increment(:cache_exists, event.duration)
+ observe(:exists, event.duration)
end
def cache_fetch_hit(event)
@@ -40,16 +41,18 @@ module Gitlab
def cache_generate(event)
return unless current_transaction
+ metric_cache_misses_total.increment(current_transaction.labels)
current_transaction.increment(:cache_read_miss_count, 1)
end
- def increment(key, duration)
+ def observe(key, duration)
return unless current_transaction
- current_transaction.increment(:cache_duration, duration)
- current_transaction.increment(:cache_count, 1)
- current_transaction.increment("#{key}_duration".to_sym, duration)
- current_transaction.increment("#{key}_count".to_sym, 1)
+ metric_cache_operation_duration_seconds.observe(current_transaction.labels.merge({ operation: key }), duration / 1000.0)
+ current_transaction.increment(:cache_duration, duration, false)
+ current_transaction.increment(:cache_count, 1, false)
+ current_transaction.increment("cache_#{key}_duration".to_sym, duration, false)
+ current_transaction.increment("cache_#{key}_count".to_sym, 1, false)
end
private
@@ -57,6 +60,23 @@ module Gitlab
def current_transaction
Transaction.current
end
+
+ def metric_cache_operation_duration_seconds
+ @metric_cache_operation_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_cache_operation_duration_seconds,
+ 'Cache access time',
+ Transaction::BASE_LABELS.merge({ action: nil }),
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
+
+ def metric_cache_misses_total
+ @metric_cache_misses_total ||= Gitlab::Metrics.counter(
+ :gitlab_cache_misses_total,
+ 'Cache read miss',
+ Transaction::BASE_LABELS
+ )
+ end
end
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index aba3e0df382..c2cbd3c16a1 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -46,14 +46,14 @@ module Gitlab
# Returns the current real time in a given precision.
#
- # Returns the time as a Float.
+ # Returns the time as a Fixnum.
def self.real_time(precision = :millisecond)
Process.clock_gettime(Process::CLOCK_REALTIME, precision)
end
# Returns the current monotonic clock time in a given precision.
#
- # Returns the time as a Float.
+ # Returns the time as a Fixnum.
def self.monotonic_time(precision = :millisecond)
Process.clock_gettime(Process::CLOCK_MONOTONIC, precision)
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 4f9fb1c7853..ee3afc5ffdb 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -2,34 +2,33 @@ module Gitlab
module Metrics
# Class for storing metrics information of a single transaction.
class Transaction
+ # base labels shared among all transactions
+ BASE_LABELS = { controller: nil, action: nil }.freeze
+
THREAD_KEY = :_gitlab_metrics_transaction
+ METRICS_MUTEX = Mutex.new
# The series to store events (e.g. Git pushes) in.
EVENT_SERIES = 'events'.freeze
attr_reader :tags, :values, :method, :metrics
- attr_accessor :action
-
def self.current
Thread.current[THREAD_KEY]
end
- # action - A String describing the action performed, usually the class
- # plus method name.
- def initialize(action = nil)
+ def initialize
@metrics = []
@methods = {}
- @started_at = nil
+ @started_at = nil
@finished_at = nil
@values = Hash.new(0)
- @tags = {}
- @action = action
+ @tags = {}
@memory_before = 0
- @memory_after = 0
+ @memory_after = 0
end
def duration
@@ -44,12 +43,15 @@ module Gitlab
Thread.current[THREAD_KEY] = self
@memory_before = System.memory_usage
- @started_at = System.monotonic_time
+ @started_at = System.monotonic_time
yield
ensure
@memory_after = System.memory_usage
- @finished_at = System.monotonic_time
+ @finished_at = System.monotonic_time
+
+ self.class.metric_transaction_duration_seconds.observe(labels, duration * 1000)
+ self.class.metric_transaction_allocated_memory_bytes.observe(labels, allocated_memory * 1024.0)
Thread.current[THREAD_KEY] = nil
end
@@ -66,33 +68,29 @@ module Gitlab
# event_name - The name of the event (e.g. "git_push").
# tags - A set of tags to attach to the event.
def add_event(event_name, tags = {})
- @metrics << Metric.new(EVENT_SERIES,
- { count: 1 },
- { event: event_name }.merge(tags),
- :event)
+ self.class.metric_event_counter(event_name, tags).increment(tags.merge(labels))
+ @metrics << Metric.new(EVENT_SERIES, { count: 1 }, tags.merge(event: event_name), :event)
end
# Returns a MethodCall object for the given name.
- def method_call_for(name)
+ def method_call_for(name, module_name, method_name)
unless method = @methods[name]
- @methods[name] = method = MethodCall.new(name, Instrumentation.series)
+ @methods[name] = method = MethodCall.new(name, module_name, method_name, self)
end
method
end
- def increment(name, value)
+ def increment(name, value, use_prometheus = true)
+ self.class.metric_transaction_counter(name).increment(labels, value) if use_prometheus
@values[name] += value
end
- def set(name, value)
+ def set(name, value, use_prometheus = true)
+ self.class.metric_transaction_gauge(name).set(labels, value) if use_prometheus
@values[name] = value
end
- def add_tag(key, value)
- @tags[key] = value
- end
-
def finish
track_self
submit
@@ -117,14 +115,83 @@ module Gitlab
submit_hashes = submit.map do |metric|
hash = metric.to_hash
-
- hash[:tags][:action] ||= @action if @action && !metric.event?
+ hash[:tags][:action] ||= action if action && !metric.event?
hash
end
Metrics.submit_metrics(submit_hashes)
end
+
+ def labels
+ BASE_LABELS
+ end
+
+ # returns string describing the action performed, usually the class plus method name.
+ def action
+ "#{labels[:controller]}##{labels[:action]}" if labels && !labels.empty?
+ end
+
+ def self.metric_transaction_duration_seconds
+ return @metric_transaction_duration_seconds if @metric_transaction_duration_seconds
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_transaction_duration_seconds,
+ 'Transaction duration',
+ BASE_LABELS,
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
+ end
+
+ def self.metric_transaction_allocated_memory_bytes
+ return @metric_transaction_allocated_memory_bytes if @metric_transaction_allocated_memory_bytes
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_allocated_memory_bytes ||= Gitlab::Metrics.histogram(
+ :gitlab_transaction_allocated_memory_bytes,
+ 'Transaction allocated memory bytes',
+ BASE_LABELS,
+ [1000, 10000, 20000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 100000000]
+ )
+ end
+ end
+
+ def self.metric_event_counter(event_name, tags)
+ return @metric_event_counters[event_name] if @metric_event_counters&.has_key?(event_name)
+
+ METRICS_MUTEX.synchronize do
+ @metric_event_counters ||= {}
+ @metric_event_counters[event_name] ||= Gitlab::Metrics.counter(
+ "gitlab_transaction_event_#{event_name}_total".to_sym,
+ "Transaction event #{event_name} counter",
+ tags.merge(BASE_LABELS)
+ )
+ end
+ end
+
+ def self.metric_transaction_counter(name)
+ return @metric_transaction_counters[name] if @metric_transaction_counters&.has_key?(name)
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_counters ||= {}
+ @metric_transaction_counters[name] ||= Gitlab::Metrics.counter(
+ "gitlab_transaction_#{name}_total".to_sym, "Transaction #{name} counter", BASE_LABELS
+ )
+ end
+ end
+
+ def self.metric_transaction_gauge(name)
+ return @metric_transaction_gauges[name] if @metric_transaction_gauges&.has_key?(name)
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_gauges ||= {}
+ @metric_transaction_gauges[name] ||= Gitlab::Metrics.gauge(
+ "gitlab_transaction_#{name}".to_sym, "Transaction gauge #{name}", BASE_LABELS, :livesum
+ )
+ end
+ end
end
end
end
diff --git a/lib/gitlab/metrics/unicorn_sampler.rb b/lib/gitlab/metrics/unicorn_sampler.rb
deleted file mode 100644
index f6987252039..00000000000
--- a/lib/gitlab/metrics/unicorn_sampler.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module Gitlab
- module Metrics
- class UnicornSampler < BaseSampler
- def initialize(interval)
- super(interval)
- end
-
- def unicorn_active_connections
- @unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
- end
-
- def unicorn_queued_connections
- @unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
- end
-
- def enabled?
- # Raindrops::Linux.tcp_listener_stats is only present on Linux
- unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats)
- end
-
- def sample
- Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active)
- unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued)
- end
-
- Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active)
- unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued)
- end
- end
-
- private
-
- def tcp_listeners
- @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
- end
-
- def unix_listeners
- @unix_listeners ||= Unicorn.listener_names - tcp_listeners
- end
-
- def unicorn_with_listeners?
- defined?(Unicorn) && Unicorn.listener_names.any?
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
new file mode 100644
index 00000000000..89ff02a96d6
--- /dev/null
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Metrics
+ class WebTransaction < Transaction
+ CONTROLLER_KEY = 'action_controller.instance'.freeze
+ ENDPOINT_KEY = 'api.endpoint'.freeze
+
+ CONTENT_TYPES = {
+ 'text/html' => :html,
+ 'text/plain' => :txt,
+ 'application/json' => :json,
+ 'text/js' => :js,
+ 'application/atom+xml' => :atom,
+ 'image/png' => :png,
+ 'image/jpeg' => :jpeg,
+ 'image/gif' => :gif,
+ 'image/svg+xml' => :svg
+ }.freeze
+
+ def initialize(env)
+ super()
+ @env = env
+ end
+
+ def labels
+ return @labels if @labels
+
+ # memoize transaction labels only source env variables were present
+ @labels = if @env[CONTROLLER_KEY]
+ labels_from_controller || {}
+ elsif @env[ENDPOINT_KEY]
+ labels_from_endpoint || {}
+ end
+
+ @labels || {}
+ end
+
+ private
+
+ def labels_from_controller
+ controller = @env[CONTROLLER_KEY]
+
+ action = "#{controller.action_name}"
+ suffix = CONTENT_TYPES[controller.content_type]
+
+ if suffix && suffix != :html
+ action += ".#{suffix}"
+ end
+
+ { controller: controller.class.name, action: action }
+ end
+
+ def labels_from_endpoint
+ endpoint = @env[ENDPOINT_KEY]
+
+ begin
+ route = endpoint.route
+ rescue
+ # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
+ # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response
+ # so we're rescuing exceptions and bailing out
+ end
+
+ if route
+ path = endpoint_paths_cache[route.request_method][route.path]
+ { controller: 'Grape', action: "#{route.request_method} #{path}" }
+ end
+ end
+
+ def endpoint_paths_cache
+ @endpoint_paths_cache ||= Hash.new do |hash, http_method|
+ hash[http_method] = Hash.new do |inner_hash, raw_path|
+ inner_hash[raw_path] = endpoint_instrumentable_path(raw_path)
+ end
+ end
+ end
+
+ def endpoint_instrumentable_path(raw_path)
+ raw_path.sub('(.:format)', '').sub('/:version', '')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index f42168c720e..c6a56277922 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -4,6 +4,7 @@ module Gitlab
module Middleware
class Go
include ActionView::Helpers::TagHelper
+ include Gitlab::CurrentSettings
PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze
@@ -37,10 +38,19 @@ module Gitlab
end
def go_body(path)
- project_url = URI.join(Gitlab.config.gitlab.url, path)
+ config = Gitlab.config
+ project_url = URI.join(config.gitlab.url, path)
import_prefix = strip_url(project_url.to_s)
- meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{project_url}.git"
+ repository_url = if current_application_settings.enabled_git_access_protocol == 'ssh'
+ shell = config.gitlab_shell
+ port = ":#{shell.ssh_port}" unless shell.ssh_port == 22
+ "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git"
+ else
+ "#{project_url}.git"
+ end
+
+ meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{repository_url}"
head_tag = content_tag :head, meta_tag
content_tag :html, head_tag
end
@@ -55,6 +65,7 @@ module Gitlab
project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
return unless project_path_match
+
path = project_path_match[1]
# Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
index 63c3372da51..bc70b2459ef 100644
--- a/lib/gitlab/middleware/rails_queue_duration.rb
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -14,11 +14,22 @@ module Gitlab
proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
if trans && proxy_start
# Time in milliseconds since gitlab-workhorse started the request
- trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000)
+ duration = Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000
+ trans.set(:rails_queue_duration, duration)
+ metric_rails_queue_duration_seconds.observe(trans.labels, duration / 1_000)
end
@app.call(env)
end
+
+ private
+
+ def metric_rails_queue_duration_seconds
+ @metric_rails_queue_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_rails_queue_duration_seconds,
+ Gitlab::Metrics::Transaction::BASE_LABELS
+ )
+ end
end
end
end
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index 0de0cddcce4..c26656704d7 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -12,6 +12,7 @@ module Gitlab
def call(env)
@env = env
+ @route_hash = nil
if disallowed_request? && Gitlab::Database.read_only?
Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
@@ -57,7 +58,7 @@ module Gitlab
end
def last_visited_url
- @env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url
+ @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url
end
def route_hash
@@ -65,11 +66,7 @@ module Gitlab
end
def whitelisted_routes
- logout_route || grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
- end
-
- def logout_route
- route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
+ grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
end
def sidekiq_route
@@ -77,11 +74,17 @@ module Gitlab
end
def grack_route
- request.path.end_with?('.git/git-upload-pack')
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('.git/git-upload-pack')
+
+ route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
end
def lfs_route
- request.path.end_with?('/info/lfs/objects/batch')
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('/info/lfs/objects/batch')
+
+ route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
end
end
end
diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb
index eb3c9002710..c22d0a84860 100644
--- a/lib/gitlab/multi_collection_paginator.rb
+++ b/lib/gitlab/multi_collection_paginator.rb
@@ -55,7 +55,9 @@ module Gitlab
def first_collection_last_page_size
return @first_collection_last_page_size if defined?(@first_collection_last_page_size)
- @first_collection_last_page_size = paginated_first_collection(first_collection_page_count).count
+ @first_collection_last_page_size = paginated_first_collection(first_collection_page_count)
+ .except(:select)
+ .size
end
end
end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 47c2a422387..552133234a3 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -157,7 +157,7 @@ module Gitlab
end
def find_by_uid_and_provider
- identity = Identity.find_by(provider: auth_hash.provider, extern_uid: auth_hash.uid)
+ identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
identity && identity.user
end
@@ -179,7 +179,7 @@ module Gitlab
valid_username = ::Namespace.clean_path(username)
uniquify = Uniquify.new
- valid_username = uniquify.string(valid_username) { |s| !DynamicPathValidator.valid_user_path?(s) }
+ valid_username = uniquify.string(valid_username) { |s| !UserPathValidator.valid_path?(s) }
name = auth_hash.name
name = valid_username if name.strip.empty?
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 962ff4d3985..1d9a5d1a20a 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -11,6 +11,7 @@ module Gitlab
rescue ActiveRecord::StaleObjectError
retries -= 1
raise unless retries >= 0
+
subject.reload
end
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 22f8dd669d0..9a91f8bf96a 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -112,22 +112,6 @@ module Gitlab
# this would map to the activity-page of its parent.
GROUP_ROUTES = %w[
-
- activity
- analytics
- audit_events
- avatar
- edit
- group_members
- hooks
- issues
- labels
- ldap
- ldap_group_links
- merge_requests
- milestones
- notification_setting
- pipeline_quota
- projects
].freeze
ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES
diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb
index 69e117f1da9..f2825db59ae 100644
--- a/lib/gitlab/performance_bar/peek_query_tracker.rb
+++ b/lib/gitlab/performance_bar/peek_query_tracker.rb
@@ -36,7 +36,7 @@ module Gitlab
end
def track_query(raw_query, bindings, start, finish)
- duration = finish - start
+ duration = (finish - start) * 1000.0
query_info = { duration: duration.round(3), sql: raw_query }
PEEK_DB_CLIENT.query_details << query_info
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index bd677ec4bf3..2c7b8af83f2 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -25,7 +25,7 @@ module Gitlab
# See https://github.com/docker/distribution/blob/master/reference/regexp.go.
#
def container_repository_name_regex
- @container_repository_regex ||= %r{\A[a-z0-9]+(?:[-._/][a-z0-9]+)*\Z}
+ @container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-])[a-z0-9]+)*\Z}
end
##
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
index e57890f1143..2c994536060 100644
--- a/lib/gitlab/routing.rb
+++ b/lib/gitlab/routing.rb
@@ -40,5 +40,24 @@ module Gitlab
def self.url_helpers
@url_helpers ||= Gitlab::Application.routes.url_helpers
end
+
+ def self.redirect_legacy_paths(router, *paths)
+ build_redirect_path = lambda do |request, _params, path|
+ # Only replace the last occurence of `path`.
+ #
+ # `request.fullpath` includes the querystring
+ new_path = request.path.sub(%r{/#{path}(/*)(?!.*#{path})}, "/-/#{path}\\1")
+ new_path << "?#{request.query_string}" if request.query_string.present?
+
+ new_path
+ end
+
+ paths.each do |path|
+ router.match "/#{path}(/*rest)",
+ via: [:get, :post, :patch, :delete],
+ to: router.redirect { |params, request| build_redirect_path.call(request, params, path) },
+ as: "legacy_#{path}_redirect"
+ end
+ end
end
end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index e0a9d1dee77..d8faf7aad8c 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -28,6 +28,7 @@ module Gitlab
def changed?
return true unless gl_user
+
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index a37112ae5c4..996d213fdb4 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -101,8 +101,7 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def import_repository(storage, name, url)
- # Timeout should be less than 900 ideally, to prevent the memory killer
- # to silently kill the process without knowing we are timing out here.
+ # The timeout ensures the subprocess won't hang forever
cmd = [gitlab_shell_projects_path, 'import-project',
storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"]
gitlab_shell_fast_execute_raise_error(cmd)
@@ -368,6 +367,7 @@ module Gitlab
output, status = gitlab_shell_fast_execute_helper(cmd, vars)
raise Error, output unless status.zero?
+
true
end
diff --git a/lib/gitlab/shell_adapter.rb b/lib/gitlab/shell_adapter.rb
index fbe2a7a0d72..053dd4ab9e0 100644
--- a/lib/gitlab/shell_adapter.rb
+++ b/lib/gitlab/shell_adapter.rb
@@ -5,7 +5,7 @@
module Gitlab
module ShellAdapter
def gitlab_shell
- Gitlab::Shell.new
+ @gitlab_shell ||= Gitlab::Shell.new
end
end
end
diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb
index 3489fb251b6..400a552bf99 100644
--- a/lib/gitlab/sherlock/transaction.rb
+++ b/lib/gitlab/sherlock/transaction.rb
@@ -89,7 +89,9 @@ module Gitlab
ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
next unless same_thread?
- track_query(data[:sql].strip, data[:binds], start, finish)
+ unless data.fetch(:cached, data[:name] == 'CACHE')
+ track_query(data[:sql].strip, data[:binds], start, finish)
+ end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index d7d24eeb37b..2bfb7caefd9 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -7,7 +7,6 @@ module Gitlab
GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
# Wait 30 seconds for running jobs to finish during graceful shutdown
SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
- SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGKILL').to_s
# Create a mutex used to ensure there will be only one thread waiting to
# shut Sidekiq down
@@ -15,6 +14,7 @@ module Gitlab
def call(worker, job, queue)
yield
+
current_rss = get_rss
return unless MAX_RSS > 0 && current_rss > MAX_RSS
@@ -23,32 +23,45 @@ module Gitlab
# Return if another thread is already waiting to shut Sidekiq down
return unless MUTEX.try_lock
- Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
- "#{MAX_RSS}"
- Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\
- "in #{GRACE_TIME} seconds"
- sleep(GRACE_TIME)
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} current RSS #{current_rss}"\
+ " exceeds maximum RSS #{MAX_RSS} after finishing job #{worker.class} JID-#{job['jid']}"
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later"
- Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- Process.kill('SIGTERM', Process.pid)
+ # Wait `GRACE_TIME` to give the memory intensive job time to finish.
+ # Then, tell Sidekiq to stop fetching new jobs.
+ wait_and_signal(GRACE_TIME, 'SIGSTP', 'stop fetching new jobs')
- Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\
- "#{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- sleep(SHUTDOWN_WAIT)
+ # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish.
+ # Then, tell Sidekiq to gracefully shut down by giving jobs a few more
+ # moments to finish, killing and requeuing them if they didn't, and
+ # then terminating itself.
+ wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down')
- Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- Process.kill(SHUTDOWN_SIGNAL, Process.pid)
+ # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't.
+ wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die')
end
end
private
def get_rss
- output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{Process.pid}))
+ output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}))
return 0 unless status.zero?
output.to_i
end
+
+ def wait_and_signal(time, signal, explanation)
+ Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ sleep(time)
+
+ Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ Process.kill(signal, pid)
+ end
+
+ def pid
+ Process.pid
+ end
end
end
end
diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb
index 11aeec1ebfa..f9faa134206 100644
--- a/lib/gitlab/string_range_marker.rb
+++ b/lib/gitlab/string_range_marker.rb
@@ -90,6 +90,7 @@ module Gitlab
# Takes an array of integers, and returns an array of ranges covering the same integers
def collapse_ranges(positions)
return [] if positions.empty?
+
ranges = []
start = prev = positions[0]
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index cb7957e2af9..33f07fa0120 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -18,6 +18,7 @@ module Gitlab
def read(path)
blob = @repository.blob_at(@commit.id, path) if @commit
raise FileNotFoundError if blob.nil?
+
blob.data
end
diff --git a/lib/gitlab/testing/request_blocker_middleware.rb b/lib/gitlab/testing/request_blocker_middleware.rb
index aa67fa08577..4a8e3c2eee0 100644
--- a/lib/gitlab/testing/request_blocker_middleware.rb
+++ b/lib/gitlab/testing/request_blocker_middleware.rb
@@ -7,6 +7,7 @@ module Gitlab
class RequestBlockerMiddleware
@@num_active_requests = Concurrent::AtomicFixnum.new(0)
@@block_requests = Concurrent::AtomicBoolean.new(false)
+ @@slow_requests = Concurrent::AtomicBoolean.new(false)
# Returns the number of requests the server is currently processing.
def self.num_active_requests
@@ -19,9 +20,15 @@ module Gitlab
@@block_requests.value = true
end
+ # Slows down incoming requests (useful for race conditions).
+ def self.slow_requests!
+ @@slow_requests.value = true
+ end
+
# Allows the server to accept requests again.
def self.allow_requests!
@@block_requests.value = false
+ @@slow_requests.value = false
end
def initialize(app)
@@ -33,6 +40,7 @@ module Gitlab
if block_requests?
block_request(env)
else
+ sleep 0.2 if slow_requests?
@app.call(env)
end
ensure
@@ -45,6 +53,10 @@ module Gitlab
@@block_requests.true?
end
+ def slow_requests?
+ @@slow_requests.true?
+ end
+
def block_request(env)
[503, {}, []]
end
diff --git a/lib/gitlab/testing/request_inspector_middleware.rb b/lib/gitlab/testing/request_inspector_middleware.rb
new file mode 100644
index 00000000000..e387667480d
--- /dev/null
+++ b/lib/gitlab/testing/request_inspector_middleware.rb
@@ -0,0 +1,71 @@
+# rubocop:disable Style/ClassVars
+
+module Gitlab
+ module Testing
+ class RequestInspectorMiddleware
+ @@log_requests = Concurrent::AtomicBoolean.new(false)
+ @@logged_requests = Concurrent::Array.new
+ @@inject_headers = Concurrent::Hash.new
+
+ # Resets the current request log and starts logging requests
+ def self.log_requests!(headers = {})
+ @@inject_headers.replace(headers)
+ @@logged_requests.replace([])
+ @@log_requests.value = true
+ end
+
+ # Stops logging requests
+ def self.stop_logging!
+ @@log_requests.value = false
+ end
+
+ def self.requests
+ @@logged_requests
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ return @app.call(env) unless @@log_requests.true?
+
+ url = env['REQUEST_URI']
+ env.merge! http_headers_env(@@inject_headers) if @@inject_headers.any?
+ request_headers = env_http_headers(env)
+ status, headers, body = @app.call(env)
+
+ request = OpenStruct.new(
+ url: url,
+ status_code: status,
+ request_headers: request_headers,
+ response_headers: headers
+ )
+ log_request request
+
+ [status, headers, body]
+ end
+
+ private
+
+ def env_http_headers(env)
+ Hash[*env.select { |k, v| k.start_with? 'HTTP_' }
+ .collect { |k, v| [k.sub(/^HTTP_/, ''), v] }
+ .collect { |k, v| [k.split('_').collect(&:capitalize).join('-'), v] }
+ .sort
+ .flatten]
+ end
+
+ def http_headers_env(headers)
+ Hash[*headers
+ .collect { |k, v| [k.split('-').collect(&:upcase).join('_'), v] }
+ .collect { |k, v| [k.prepend('HTTP_'), v] }
+ .flatten]
+ end
+
+ def log_request(response)
+ @@logged_requests.push(response)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index fee1a127fd7..13150ddab67 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -22,10 +22,12 @@ module Gitlab
return true if blocked_user_or_hostname?(uri.user)
return true if blocked_user_or_hostname?(uri.hostname)
- server_ips = Resolv.getaddresses(uri.hostname)
+ server_ips = Addrinfo.getaddrinfo(uri.hostname, 80, nil, :STREAM).map(&:ip_address)
return true if (blocked_ips & server_ips).any?
rescue Addressable::URI::InvalidURIError
return true
+ rescue SocketError
+ return false
end
false
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 1caa791c1be..59331c827af 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -70,6 +70,7 @@ module Gitlab
def generate_full_url
return @url unless valid_credentials?
+
@full_url = @url.dup
@full_url.password = credentials[:password] if credentials[:password].present?
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 70a403652e7..112d4939582 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -48,9 +48,9 @@ module Gitlab
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
- gcp_clusters: ::Gcp::Cluster.count,
- gcp_clusters_enabled: ::Gcp::Cluster.enabled.count,
- gcp_clusters_disabled: ::Gcp::Cluster.disabled.count,
+ clusters: ::Clusters::Cluster.count,
+ clusters_enabled: ::Clusters::Cluster.enabled.count,
+ clusters_disabled: ::Clusters::Cluster.disabled.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb
new file mode 100644
index 00000000000..a2ac9285b56
--- /dev/null
+++ b/lib/gitlab/utils/strong_memoize.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module Utils
+ module StrongMemoize
+ # Instead of writing patterns like this:
+ #
+ # def trigger_from_token
+ # return @trigger if defined?(@trigger)
+ #
+ # @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ #
+ # We could write it like:
+ #
+ # def trigger_from_token
+ # strong_memoize(:trigger) do
+ # Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ # end
+ #
+ def strong_memoize(name)
+ ivar_name = "@#{name}"
+
+ if instance_variable_defined?(ivar_name)
+ instance_variable_get(ivar_name)
+ else
+ instance_variable_set(ivar_name, yield)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index c60bd91ea6e..11472ce6cce 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -99,6 +99,7 @@ module Gitlab
def level_value(level)
return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i)
+
string_options[level] || PRIVATE
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 58d5b0da1c4..864a9e04888 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -16,14 +16,15 @@ module Gitlab
SECRET_LENGTH = 32
class << self
- def git_http_ok(repository, is_wiki, user, action)
+ def git_http_ok(repository, is_wiki, user, action, show_all_refs: false)
project = repository.project
repo_path = repository.path_to_repo
params = {
GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
GL_USERNAME: user&.username,
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: show_all_refs
}
server = {
address: Gitlab::GitalyClient.address(project.repository_storage),
@@ -173,6 +174,7 @@ module Gitlab
@secret ||= begin
bytes = Base64.strict_decode64(File.read(secret_path).chomp)
raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
+
bytes
end
end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index a440a3e3562..9242cbe840c 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -3,7 +3,6 @@ require 'google/apis/container_v1'
module GoogleApi
module CloudPlatform
class Client < GoogleApi::Auth
- DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
LEAST_TOKEN_LIFE_TIME = 10.minutes
diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb
index 05668c69006..f5485eb89fa 100644
--- a/lib/haml_lint/inline_javascript.rb
+++ b/lib/haml_lint/inline_javascript.rb
@@ -9,6 +9,7 @@ unless Rails.env.production?
def visit_filter(node)
return unless node.filter_type == 'javascript'
+
record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
end
end
diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb
deleted file mode 100644
index 939b23a3421..00000000000
--- a/lib/rouge/lexers/math.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Rouge
- module Lexers
- class Math < PlainText
- title "A passthrough lexer used for LaTeX input"
- desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter"
- tag 'math'
- end
- end
-end
diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb
deleted file mode 100644
index 63c461764fc..00000000000
--- a/lib/rouge/lexers/plantuml.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Rouge
- module Lexers
- class Plantuml < PlainText
- title "A passthrough lexer used for PlantUML input"
- desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter"
- tag 'plantuml'
- end
- end
-end
diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb
index 9af21078403..ad41760dff2 100644
--- a/lib/system_check/app/git_user_default_ssh_config_check.rb
+++ b/lib/system_check/app/git_user_default_ssh_config_check.rb
@@ -11,10 +11,10 @@ module SystemCheck
].freeze
set_name 'Git user has default SSH configuration?'
- set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)'
+ set_skip_reason 'skipped (git user is not present / configured)'
def skip?
- Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
+ !home_dir || !File.directory?(home_dir)
end
def check?
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
index 08a2c495bd4..57bbabece1f 100644
--- a/lib/system_check/app/ruby_version_check.rb
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -5,7 +5,7 @@ module SystemCheck
set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version
- @required_version ||= Gitlab::VersionInfo.new(2, 3, 3)
+ @required_version ||= Gitlab::VersionInfo.new(2, 3, 5)
end
def self.current_version
diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb
index 00221f77cf4..8b145fb4511 100644
--- a/lib/system_check/simple_executor.rb
+++ b/lib/system_check/simple_executor.rb
@@ -24,6 +24,7 @@ module SystemCheck
# @param [BaseCheck] check class
def <<(check)
raise ArgumentError unless check.is_a?(Class) && check < BaseCheck
+
@checks << check
end
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 87ca39b079b..c2d3a6b6950 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,5 +1,28 @@
namespace :gemojione do
desc 'Generates Emoji SHA256 digests'
+
+ task aliases: ['yarn:check', 'environment'] do
+ require 'json'
+
+ aliases = {}
+
+ index_file = File.join(Rails.root, 'fixtures', 'emojis', 'index.json')
+ index = JSON.parse(File.read(index_file))
+
+ index.each_pair do |key, data|
+ data['aliases'].each do |a|
+ a.tr!(':', '')
+
+ aliases[a] = key
+ end
+ end
+
+ out = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
+ File.open(out, 'w') do |handle|
+ handle.write(JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: ''))
+ end
+ end
+
task digests: ['yarn:check', 'environment'] do
require 'digest/sha2'
require 'json'
@@ -16,8 +39,13 @@ namespace :gemojione do
fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
hash_digest = Digest::SHA256.file(fpath).hexdigest
+ category = emoji_hash['category']
+ if name == 'gay_pride_flag'
+ category = 'flags'
+ end
+
entry = {
- category: emoji_hash['category'],
+ category: category,
moji: emoji_hash['moji'],
description: emoji_hash['description'],
unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
@@ -29,7 +57,6 @@ namespace :gemojione do
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
-
File.open(out, 'w') do |handle|
handle.write(JSON.pretty_generate(resultant_emoji_map))
end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 1650263b98d..9dcf44fdc3e 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -33,24 +33,29 @@ namespace :gitlab do
backup.unpack
unless backup.skipped?('db')
- unless ENV['force'] == 'yes'
- warning = <<-MSG.strip_heredoc
- Before restoring the database we recommend removing all existing
- tables to avoid future upgrade problems. Be aware that if you have
- custom tables in the GitLab database these tables and all data will be
- removed.
- MSG
- puts warning.color(:red)
- ask_to_continue
- puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow)
- sleep(5)
+ begin
+ unless ENV['force'] == 'yes'
+ warning = <<-MSG.strip_heredoc
+ Before restoring the database, we will remove all existing
+ tables to avoid future upgrade problems. Be aware that if you have
+ custom tables in the GitLab database these tables and all data will be
+ removed.
+ MSG
+ puts warning.color(:red)
+ ask_to_continue
+ puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow)
+ sleep(5)
+ end
+ # Drop all tables Load the schema to ensure we don't have any newer tables
+ # hanging out from a failed upgrade
+ $progress.puts 'Cleaning the database ... '.color(:blue)
+ Rake::Task['gitlab:db:drop_tables'].invoke
+ $progress.puts 'done'.color(:green)
+ Rake::Task['gitlab:backup:db:restore'].invoke
+ rescue Gitlab::TaskAbortedByUserError
+ puts "Quitting...".color(:red)
+ exit 1
end
- # Drop all tables Load the schema to ensure we don't have any newer tables
- # hanging out from a failed upgrade
- $progress.puts 'Cleaning the database ... '.color(:blue)
- Rake::Task['gitlab:db:drop_tables'].invoke
- $progress.puts 'done'.color(:green)
- Rake::Task['gitlab:backup:db:restore'].invoke
end
Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 8ae1b6a626a..301affc9522 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -59,7 +59,11 @@ namespace :gitlab do
.sub(%r{^/*}, '')
.chomp('.git')
.chomp('.wiki')
- next if Project.find_by_full_path(repo_with_namespace)
+
+ # TODO ignoring hashed repositories for now. But revisit to fully support
+ # possible orphaned hashed repos
+ next if repo_with_namespace.start_with?('@hashed/') || Project.find_by_full_path(repo_with_namespace)
+
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
File.rename(path, new_path)
@@ -75,6 +79,7 @@ namespace :gitlab do
User.find_each do |user|
next unless user.ldap_user?
+
print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
if Gitlab::LDAP::Access.allowed?(user)
puts " [OK]".color(:green)
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 930b4bc13e2..ba221e44e5d 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -5,10 +5,9 @@ namespace :gitlab do
opts =
if ENV['CI']
{
- # We don't use CI_REPOSITORY_URL since it includes `gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@`
- # which is confusing in the steps suggested in the job's output.
- ce_repo: "#{ENV['CI_PROJECT_URL']}.git",
- branch: ENV['CI_COMMIT_REF_NAME']
+ ce_project_url: ENV['CI_PROJECT_URL'],
+ branch: ENV['CI_COMMIT_REF_NAME'],
+ job_id: ENV['CI_JOB_ID']
}
else
unless args[:branch]
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 8377fe3269d..4d880c05f99 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -14,18 +14,18 @@ namespace :gitlab do
checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir)
+ command = %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE]
+
_, status = Gitlab::Popen.popen(%w[which gmake])
- command = status.zero? ? ['gmake'] : ['make']
+ command << (status.zero? ? 'gmake' : 'make')
- if Rails.env.test?
- command += %W[BUNDLE_PATH=#{Bundler.bundle_path}]
- end
+ command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test?
Dir.chdir(args.dir) do
create_gitaly_configuration
# In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present?
- Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + command) }
+ Bundler.with_original_env { run_command!(command) }
end
end
end
@@ -78,13 +78,18 @@ namespace :gitlab do
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
+ config[:bin_dir] = Gitlab.config.gitaly.client_path
+
TOML.dump(config)
end
def create_gitaly_configuration
- File.open("config.toml", "w") do |f|
+ File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f|
f.puts gitaly_configuration_toml
end
+ rescue Errno::EEXIST
+ puts "Skipping config.toml generation:"
+ puts "A configuration file already exists."
rescue ArgumentError => e
puts "Skipping config.toml generation:"
puts e.message
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index d227a0c8bdb..adfcc3cda22 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -2,23 +2,21 @@ namespace :gitlab do
namespace :import do
# How to use:
#
- # 1. copy the bare repos under the repository storage paths (commonly the default path is /home/git/repositories)
- # 2. run: bundle exec rake gitlab:import:repos RAILS_ENV=production
+ # 1. copy the bare repos to a specific path that contain the group or subgroups structure as folders
+ # 2. run: bundle exec rake gitlab:import:repos[/path/to/repos] RAILS_ENV=production
#
# Notes:
# * The project owner will set to the first administator of the system
# * Existing projects will be skipped
- #
- #
desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
- task repos: :environment do
- if Project.current_application_settings.hashed_storage_enabled
- puts 'Cannot import repositories when Hashed Storage is enabled'.color(:red)
+ task :repos, [:import_path] => :environment do |_t, args|
+ unless args.import_path
+ puts 'Please specify an import path that contains the repositories'.color(:red)
exit 1
end
- Gitlab::BareRepositoryImporter.execute
+ Gitlab::BareRepositoryImport::Importer.execute(args.import_path)
end
end
end
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
deleted file mode 100644
index 6cbc83b8973..00000000000
--- a/lib/tasks/gitlab/sidekiq.rake
+++ /dev/null
@@ -1,47 +0,0 @@
-namespace :gitlab do
- namespace :sidekiq do
- QUEUE = 'queue:post_receive'.freeze
-
- desc 'Drop all Sidekiq PostReceive jobs for a given project'
- task :drop_post_receive, [:project] => :environment do |t, args|
- unless args.project.present?
- abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]"
- end
- project_path = Project.find_by_full_path(args.project).repository.path_to_repo
-
- Sidekiq.redis do |redis|
- unless redis.exists(QUEUE)
- abort "Queue #{QUEUE} is empty"
- end
-
- temp_queue = "#{QUEUE}_#{Time.now.to_i}"
- redis.rename(QUEUE, temp_queue)
-
- # At this point, then post_receive queue is empty. It may be receiving
- # new jobs already. We will repopulate it with the old jobs, skipping the
- # ones we want to drop.
- dropped = 0
- while (job = redis.lpop(temp_queue))
- if repo_path(job) == project_path
- dropped += 1
- else
- redis.rpush(QUEUE, job)
- end
- end
- # The temp_queue will delete itself after we have popped all elements
- # from it
-
- puts "Dropped #{dropped} jobs containing #{project_path} from #{QUEUE}"
- end
- end
-
- def repo_path(job)
- job_args = JSON.parse(job)['args']
- if job_args
- job_args.first
- else
- nil
- end
- end
- end
-end
diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake
deleted file mode 100644
index 3a16ace60bd..00000000000
--- a/lib/tasks/gitlab/users.rake
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace :gitlab do
- namespace :users do
- desc "GitLab | Clear the authentication token for all users"
- task clear_all_authentication_tokens: :environment do |t, args|
- # Do small batched updates because these updates will be slow and locking
- User.select(:id).find_in_batches(batch_size: 100) do |batch|
- User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
- end
- end
- end
-end
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 7f86fd7b45e..aafbe52e5f8 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -7,14 +7,16 @@ class GithubImport
end
def initialize(token, gitlab_username, project_path, extras)
- @options = { token: token, verbose: true }
+ @options = { token: token }
@project_path = project_path
@current_user = User.find_by_username(gitlab_username)
@github_repo = extras.empty? ? nil : extras.first
end
def run!
- @repo = GithubRepos.new(@options, @current_user, @github_repo).choose_one!
+ @repo = GithubRepos
+ .new(@options[:token], @current_user, @github_repo)
+ .choose_one!
raise 'No repo found!' unless @repo
@@ -28,7 +30,7 @@ class GithubImport
private
def show_warning!
- puts "This will import GitHub #{@repo['full_name'].bright} into GitLab #{@project_path.bright} as #{@current_user.name}"
+ puts "This will import GitHub #{@repo.full_name.bright} into GitLab #{@project_path.bright} as #{@current_user.name}"
puts "Permission checks are ignored. Press any key to continue.".color(:red)
STDIN.getch
@@ -42,7 +44,9 @@ class GithubImport
import_success = false
timings = Benchmark.measure do
- import_success = Github::Import.new(@project, @options).execute
+ import_success = Gitlab::GithubImport::SequentialImporter
+ .new(@project, token: @options[:token])
+ .execute
end
if import_success
@@ -63,16 +67,16 @@ class GithubImport
@current_user,
name: name,
path: name,
- description: @repo['description'],
+ description: @repo.description,
namespace_id: namespace.id,
visibility_level: visibility_level,
- skip_wiki: @repo['has_wiki']
+ skip_wiki: @repo.has_wiki
).execute
project.update!(
import_type: 'github',
- import_source: @repo['full_name'],
- import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@")
+ import_source: @repo.full_name,
+ import_url: @repo.clone_url.sub('://', "://#{@options[:token]}@")
)
project
@@ -91,13 +95,15 @@ class GithubImport
end
def visibility_level
- @repo['private'] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.current_application_settings.default_project_visibility
+ @repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.current_application_settings.default_project_visibility
end
end
class GithubRepos
- def initialize(options, current_user, github_repo)
- @options = options
+ def initialize(token, current_user, github_repo)
+ @client = Gitlab::GithubImport::Client.new(token)
+ @client.octokit.auto_paginate = true
+
@current_user = current_user
@github_repo = github_repo
end
@@ -106,17 +112,17 @@ class GithubRepos
return found_github_repo if @github_repo
repos.each do |repo|
- print "ID: #{repo['id'].to_s.bright}".color(:green)
- print "\tName: #{repo['full_name']}\n".color(:green)
+ print "ID: #{repo.id.to_s.bright}".color(:green)
+ print "\tName: #{repo.full_name}\n".color(:green)
end
print 'ID? '.bright
- repos.find { |repo| repo['id'] == repo_id }
+ repos.find { |repo| repo.id == repo_id }
end
def found_github_repo
- repos.find { |repo| repo['full_name'] == @github_repo }
+ repos.find { |repo| repo.full_name == @github_repo }
end
def repo_id
@@ -124,7 +130,7 @@ class GithubRepos
end
def repos
- Github::Repositories.new(@options).fetch
+ @client.octokit.list_repositories
end
end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index ad1818ff1fa..693597afdf8 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -1,12 +1,7 @@
require_relative '../../app/models/concerns/token_authenticatable.rb'
namespace :tokens do
- desc "Reset all GitLab user auth tokens"
- task reset_all_auth: :environment do
- reset_all_users_token(:reset_authentication_token!)
- end
-
- desc "Reset all GitLab email tokens"
+ desc "Reset all GitLab incoming email tokens"
task reset_all_email: :environment do
reset_all_users_token(:reset_incoming_email_token!)
end
@@ -31,11 +26,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users'
- def reset_authentication_token!
- write_new_token(:authentication_token)
- save!(validate: false)
- end
-
def reset_incoming_email_token!
write_new_token(:incoming_email_token)
save!(validate: false)
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 77b843f84ae..8e688dede89 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:34-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:29-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -34,6 +34,11 @@ msgstr[1] "%s Ð¿Ð¾Ð´Ð°Ð²Ð°Ð½Ð¸Ñ Ð±Ñха пропуÑнати, за да не Ñ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} подаде %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -54,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 Ñхема"
@@ -110,9 +121,18 @@ msgstr "Добавете SSH ключ в профила Ñи, за да може
msgid "Add new directory"
msgstr "ДобавÑне на нова папка"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -128,6 +148,9 @@ msgstr "ÐаиÑтина ли иÑкате да изтриете този пла
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -161,18 +184,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -235,6 +261,9 @@ msgstr[1] "Клонове"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "Клонът <strong>%{branch_name}</strong> беше Ñъздаден. За да наÑтроите автоматичното внедрÑване, изберете Yaml шаблон за GitLab CI и подайте промените Ñи. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ТърÑете в клоновете"
@@ -445,15 +474,24 @@ msgstr "пропуÑнато"
msgid "CiStatus|running"
msgstr "протича в момента"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -496,27 +534,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -526,7 +570,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -538,18 +585,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -582,6 +623,11 @@ msgid_plural "Commits"
msgstr[0] "Подаване"
msgstr[1] "ПодаваниÑ"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Commit Message"
msgstr ""
@@ -663,6 +709,12 @@ msgstr "РъководÑтво за ÑътрудничеÑтво"
msgid "Contributors"
msgstr "Сътрудници"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -676,7 +728,7 @@ msgid "Create New Directory"
msgstr "Създаване на нова папка"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
-msgstr "Създайте Ñи личен жетон за доÑтъп в профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
+msgstr "Създайте Ñи личен жетон за доÑтъп в акаунта Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
msgid "Create directory"
msgstr "Създаване на папка"
@@ -684,9 +736,21 @@ msgstr "Създаване на папка"
msgid "Create empty bare repository"
msgstr "Създаване на празно хранилище"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Създаване на заÑвка за Ñливане"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Създаване на нов…"
@@ -773,6 +837,9 @@ msgstr "Име на папката"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -845,12 +912,18 @@ msgstr "Ð’ÑÑка Ñедмица (в неделÑ, в 4 ч. Ñутринта)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "СобÑтвеникът не може да бъде променен"
msgid "Failed to remove the pipeline schedule"
msgstr "Планът за Ñхема не може да бъде премахнат"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Файлове"
@@ -895,9 +968,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -940,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -984,6 +1108,12 @@ msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Шаблон за интервала"
@@ -1052,6 +1182,9 @@ msgstr "Ðаучете повече в"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "документациÑта отноÑно планирането на Ñхеми"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "ÐапуÑкане на групата"
@@ -1075,6 +1208,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Медиана"
@@ -1105,6 +1244,9 @@ msgstr ""
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðов проблем"
@@ -1122,18 +1264,27 @@ msgstr "Ðова папка"
msgid "New file"
msgstr "Ðов файл"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Ðов проблем"
msgid "New merge request"
msgstr "Ðова заÑвка за Ñливане"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Ðов план"
msgid "New snippet"
msgstr "Ðов отрÑзък"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Ðов етикет"
@@ -1212,6 +1363,12 @@ msgstr "Ðаблюдение"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Филтър"
@@ -1353,9 +1510,54 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проектът „%{project_name}“ е добавен в опашката за изтриване."
@@ -1365,9 +1567,6 @@ msgstr "Проектът „%{project_name}“ беше Ñъздаден уÑпÐ
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проектът „%{project_name}“ беше обновен уÑпешно."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Проектът „%{project_name}“ ще бъде изтрит."
-
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтъпът до проекта Ñ‚Ñ€Ñбва да бъде даван поотделно на вÑеки потребител."
@@ -1425,6 +1624,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1446,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Прочетете повече"
@@ -1515,6 +1729,9 @@ msgstr "ОтмÑна на тази заÑвка за Ñливане"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1533,6 +1750,15 @@ msgstr "Планиране на Ñхемите"
msgid "Search branches and tags"
msgstr "ТърÑете в клоновете и етикетите"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "Изберете формата на архива"
@@ -1546,7 +1772,7 @@ msgid "Service Templates"
msgstr ""
msgid "Set a password on your account to pull or push via %{protocol}."
-msgstr "Задайте парола на профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
+msgstr "Задайте парола на акаунта Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
msgid "Set up CI"
msgstr "ÐаÑтройка на ÐИ"
@@ -1580,13 +1806,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1706,6 +1935,12 @@ msgstr "Създайте %{new_merge_request} Ñ Ñ‚ÐµÐ·Ð¸ промени"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Преминаване към клон/етикет"
@@ -1732,6 +1967,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "Етапът на програмиране показва времето от първото подаване до Ñъздаването на заÑвката за Ñливане. Данните ще бъдат добавени тук автоматично Ñлед като бъде Ñъздадена първата заÑвка за Ñливане."
@@ -1744,6 +1982,15 @@ msgstr "Връзката на разклонение беше премахнат
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "Етапът на проблемите показва колко е времето от Ñъздаването на проблем до определÑнето на целеви етап на проекта за него, или до добавÑнето му в ÑпиÑък на дъÑката за проблеми. Започнете да добавÑте проблеми, за да видите данните за този етап."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "Етапът от цикъла на разработка"
@@ -1774,6 +2021,12 @@ msgstr "Етапът на подготовка за издаване показÐ
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "Етапът на теÑтване показва времето, което е нужно на „Gitlab CI“ да изпълни вÑÑка Ñхема от задачи за Ñвързаната заÑвка за Ñливане. Данните ще бъдат добавени автоматично Ñлед като приключи изпълнението на първата Ви Ñхема."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "Времето, което отнема вÑеки Ð·Ð°Ð¿Ð¸Ñ Ð¾Ñ‚ данни за ÑÑŠÐ¾Ñ‚Ð²ÐµÑ‚Ð½Ð¸Ñ ÐµÑ‚Ð°Ð¿."
@@ -1783,6 +2036,9 @@ msgstr "СтойноÑтта, коÑто Ñе намира в Ñредата нÐ
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1967,6 +2223,9 @@ msgstr ""
msgid "Unstar"
msgstr "Без звезда"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2030,6 +2289,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2150,9 +2412,21 @@ msgstr "Ðа път Ñте да премахнете връзката на раÐ
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Ðа път Ñте да прехвърлите „%{project_name_with_namespace}“ към друг ÑобÑтвеник. ÐÐИСТИÐРли иÑкате това?"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "Можете да добавÑте файлове Ñамо когато Ñе намирате в клон"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Ðе можете да Ñъздавате повече проекти"
@@ -2178,7 +2452,7 @@ msgid "You will receive notifications only for comments in which you were @menti
msgstr "Ще получавате извеÑÑ‚Ð¸Ñ Ñамо за коментари, в които Ви @Ñпоменават"
msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
-msgstr "ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез %{protocol}, докато не %{set_password_link} за профила Ñи"
+msgstr "ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез %{protocol}, докато не %{set_password_link} за акаунта Ñи"
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез SSH, докато не %{add_ssh_key_link} в профила Ñи"
@@ -2186,6 +2460,9 @@ msgstr "ÐÑма да можете да изтеглÑте или изпраща
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Вашето име"
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] "родител"
msgstr[1] "родители"
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index f65012d1e1f..431e11818c1 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:36-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -34,6 +34,11 @@ msgstr[1] "%s zusätzliche Commits wurden ausgelassen um Leistungsprobleme zu ve
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} hat %{commit_timeago} committet"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -54,6 +59,12 @@ msgstr[1] "%{storage_name}: %{failed_attempts} fehlgeschlagene Speicherzugriffe:
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(beachte die Informationen zur Installation auf %{link})."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -110,9 +121,18 @@ msgstr "Füge einen SSH Schlüssel zu deinem Profil hinzu, um mittels SSH zu üb
msgid "Add new directory"
msgstr "Erstelle eine neues Verzeichnis"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "Alle"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -128,6 +148,9 @@ msgstr "Bist Du sicher, dass Du diesen Pipeline-Zeitplan löschen möchtest?"
msgid "Are you sure you want to discard your changes?"
msgstr "Bist Du sicher, dass Du alle Änderungen zurücksetzen willst?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "Bist Du sicher, dass Du den Registrierungstoken zurücksetzen willst?"
@@ -161,18 +184,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr "Abrechnung"
@@ -235,6 +261,9 @@ msgstr[1] "Zweige"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatische Bereitstellung einzurichten, wähle eine GitLab CI Yaml Vorlage und committe Deine Änderungen. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Branches durchsuchen"
@@ -445,15 +474,24 @@ msgstr "übersprungen"
msgid "CiStatus|running"
msgstr "laufend"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr "Schließen"
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -496,27 +534,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -526,7 +570,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -538,18 +585,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -582,6 +623,11 @@ 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 ""
@@ -663,6 +709,12 @@ msgstr "Mitarbeitsanleitung"
msgid "Contributors"
msgstr "Mitarbeiter"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr "Öffentlichen SSH-Schlüssel in die Zwischenablage kopieren"
@@ -684,9 +736,21 @@ msgstr "Erstelle Verzeichnis"
msgid "Create empty bare repository"
msgstr "Erstelle leeres Repository"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Erstelle Merge Request"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Erstelle neues..."
@@ -773,6 +837,9 @@ msgstr "Verzeichnisname"
msgid "Discard changes"
msgstr "Änderungen verwerfen"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -845,12 +912,18 @@ msgstr "Wöchentlich (Sonntags um 4:00 Uhr)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Wechsel des Besitzers fehlgeschlagen"
msgid "Failed to remove the pipeline schedule"
msgstr "Entfernung der Pipelineplanung fehlgeschlagen"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Dateien"
@@ -895,9 +968,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -940,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr "Systemzustand"
@@ -984,6 +1108,12 @@ msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervallmuster"
@@ -1052,6 +1182,9 @@ msgstr "Erfahre mehr in den"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "Pipelineplanungsdokumentation"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "Verlasse die Gruppe"
@@ -1075,6 +1208,12 @@ msgstr ""
msgid "Locked Files"
msgstr "Gesperrte Dateien"
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Median"
@@ -1105,6 +1244,9 @@ msgstr "hier"
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Neues Ticket"
@@ -1122,18 +1264,27 @@ msgstr "Neues Verzeichnis"
msgid "New file"
msgstr "Neue Datei"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Neues Ticket"
msgid "New merge request"
msgstr "Neuer Merge Request"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Neuer Zeitplan"
msgid "New snippet"
msgstr "Neuer Schnipsel"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Neuer Tag"
@@ -1212,6 +1363,12 @@ msgstr "Beobachten"
msgid "Notifications"
msgstr "Benachrichtigungen"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filter"
@@ -1353,9 +1510,54 @@ msgstr "mit Stages"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr "Profil"
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Das Projekt '%{project_name}' wurde zur Löschung eingeplant."
@@ -1365,9 +1567,6 @@ msgstr "Das Projekt '%{project_name}' wurde erfolgreich erstellt."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Das Projekt '%{project_name}' wurde erfolgreich aktualisiert."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Das Projekt '%{project_name}' wird gelöscht."
-
msgid "Project access must be granted explicitly to each user."
msgstr "Jedem Nutzer muss explizit der Zugriff auf das Projekt gewährt werden."
@@ -1425,6 +1624,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1446,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr "Ãœbertragungsereignisse"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Mehr lesen"
@@ -1515,6 +1729,9 @@ msgstr "Merge Request zurücksetzen"
msgid "SSH Keys"
msgstr "SSH-Schlüssel"
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1533,6 +1750,15 @@ msgstr "Pipelines planen"
msgid "Search branches and tags"
msgstr "Suche nach Branches und Tags"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "Archivierungsformat auswählen"
@@ -1580,13 +1806,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1706,6 +1935,12 @@ msgstr "Beginne einen %{new_merge_request} mit diesen Änderungen"
msgid "Start the Runner!"
msgstr "Starte den Runner!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Zu Branch/Tag wechseln"
@@ -1732,6 +1967,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "Die Entwicklungsphase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Du Deinen ersten Merge Request anlegst, werden dessen Daten automatisch ergänzt."
@@ -1744,6 +1982,15 @@ msgstr "Die Beziehung des Ablegers wurde entfernt."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "Die Ticketphase stellt die Zeit vom Anlegen eines Tickets bis zum Zuweisen eines Meilensteins oder Hinzufügen zur Aufgabentafel dar. Erstelle einen Ticket, damit dessen Daten hier erscheinen."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "Die Phase des Entwicklungslebenszyklus."
@@ -1774,6 +2021,12 @@ msgstr "Die Staging-Phase stellt die Zeit zwischen der Umsetzung eines Merge Re
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "Die Testphase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von zugehörigen Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "Zeit, die für das jeweilige Ereignis in der Phase ermittelt wurde."
@@ -1783,6 +2036,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 branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1967,6 +2223,9 @@ msgstr ""
msgid "Unstar"
msgstr "Entfavorisieren"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2030,6 +2289,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr "Wiki"
@@ -2150,9 +2412,21 @@ msgstr "Du bist dabei, die Beziehung des Ablegers zum Ursprungsprojekt %{forked_
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Du bist dabei %{project_name_with_namespace} einem andere Besitzer zu übergeben. Bist Du dir WIRKLICH sicher?"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "Du kannst Dateien nur hinzufügen, wenn Du dich auf einem Branch befindest."
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Du hast die Projektbegrenzung erreicht."
@@ -2186,6 +2460,9 @@ msgstr "Du kannst erst mittels SSH übertragen (push) oder abrufen (pull), nachd
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Dein Name"
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] "Vorgänger"
msgstr[1] "Vorgänger"
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 838c7b62810..0a1b379b3d3 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:36-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -34,6 +34,11 @@ msgstr[1] "%s enmetadoj estis transsaltitaj, por ne troÅarÄi la sistemon."
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} enmetis %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -54,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 ĉenstablo"
@@ -110,9 +121,18 @@ msgstr "Aldonu SSH-Ålosilon al via profilo por ebligi al vi eltiri kaj alpuÅi
msgid "Add new directory"
msgstr "Aldoni novan dosierujon"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -128,6 +148,9 @@ msgstr "Ĉu vi certe volas forigi ĉi tiun ĉenstablan planon?"
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -161,18 +184,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -235,6 +261,9 @@ msgstr[1] "Branĉoj"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "La branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aÅ­tomatan disponigadon, bonvolu elekti Yaml-Åablonon por GitLab CI kaj enmeti viajn ÅanÄojn. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Serĉu branĉon"
@@ -445,15 +474,24 @@ msgstr "transsaltita"
msgid "CiStatus|running"
msgstr "plenumiÄanta"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -496,27 +534,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -526,7 +570,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -538,18 +585,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -582,6 +623,11 @@ 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 ""
@@ -663,6 +709,12 @@ msgstr "Gvidlinioj por kontribuado"
msgid "Contributors"
msgstr "Kontribuantoj"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -684,9 +736,21 @@ msgstr "Krei dosierujon"
msgid "Create empty bare repository"
msgstr "Krei malplenan deponejon"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Krei peton pri kunfando"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Krei novan…"
@@ -773,6 +837,9 @@ msgstr "Nomo de dosierujo"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -845,12 +912,18 @@ msgstr "Ĉiusemajne (en dimanĉo, je 4:00)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Ne eblas ÅanÄi la posedanton"
msgid "Failed to remove the pipeline schedule"
msgstr "Ne eblas forigi la ĉenstablan planon"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Dosieroj"
@@ -895,9 +968,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -940,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -984,6 +1108,12 @@ msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervala Åablono"
@@ -1052,6 +1182,9 @@ msgstr "Lernu pli en la"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "dokumentado pri ĉenstablaj planoj"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "Forlasi la grupon"
@@ -1075,6 +1208,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
@@ -1105,6 +1244,9 @@ msgstr ""
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova problemo"
@@ -1122,18 +1264,27 @@ msgstr "Nova dosierujo"
msgid "New file"
msgstr "Nova dosiero"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Nova problemo"
msgid "New merge request"
msgstr "Nova peto pri kunfando"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Nova plano"
msgid "New snippet"
msgstr "Nova kodaĵo"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Nova etikedo"
@@ -1212,6 +1363,12 @@ msgstr "Rigardado"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrilo"
@@ -1353,9 +1510,54 @@ msgstr "kun etapoj"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "La projekto „%{project_name}“ estis alvicigita por forigado."
@@ -1365,9 +1567,6 @@ msgstr "La projekto „%{project_name}“ estis sukcese kreita."
msgid "Project '%{project_name}' was successfully updated."
msgstr "La projekto „%{project_name}“ estis sukcese Äisdatigita."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "La projekto „%{project_name}“ estos forigita."
-
msgid "Project access must be granted explicitly to each user."
msgstr "Ĉiu uzanto devas akiri propran atingon al la projekto."
@@ -1425,6 +1624,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1446,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Legu pli"
@@ -1515,6 +1729,9 @@ msgstr "Malfari ĉi tiun peton pri kunfando"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1533,6 +1750,15 @@ msgstr "Planado de la ĉenstabloj"
msgid "Search branches and tags"
msgstr "Serĉu branĉon aŭ etikedon"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "Elektu formaton de arkivo"
@@ -1580,13 +1806,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1706,6 +1935,12 @@ msgstr "Kreu %{new_merge_request} kun ĉi tiuj ÅanÄoj"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Iri al branĉo/etikedo"
@@ -1732,6 +1967,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "La etapo de programado montras la tempon de la unua enmetado Äis la kreado de la peto pri kunfando. La datenoj aldoniÄos aÅ­tomate ĉi tie post kiam vi kreas la unuan peton pri kunfando."
@@ -1744,6 +1982,15 @@ msgstr "La rilato de disbranĉigo estis forigita."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "La etapo de la problemo montras kiom la tempo pasas de la kreado de problemo Äis la atribuado de la problemo al cela etapo de la projekto, aÅ­ al listo sur la problemtabulo. Komencu krei problemojn por vidi la datenojn por ĉi tiu etapo."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "La etapo de la disvolva ciklo."
@@ -1774,6 +2021,12 @@ msgstr "La etapo de preparo por eldono montras la tempon inter la aplikado de la
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "La etapo de testado montras kiom da tempo necesas al „GitLab CI“ por plenumi ĉiujn ĉenstablojn por la rilata peto pri kunfando. La datenoj aldoniÄos aÅ­tomate post kiam via unua ĉenstablo finiÄos."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "La tempo, kiu estas necesa por ĉiu dateno kolektita de la etapo."
@@ -1783,6 +2036,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 branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1967,6 +2223,9 @@ msgstr ""
msgid "Unstar"
msgstr "Malsteligi"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2030,6 +2289,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2148,11 +2410,23 @@ msgid "You are going to remove the fork relationship to source project %{forked_
msgstr "Vi forigos la rilaton de la disbranĉigo al la originala projekto, „%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?"
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
-msgstr "Vi transigos „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas ABSOLUTE certa?"
+msgstr "Vi estas transigonta „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas ABSOLUTE certa?"
+
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
msgid "You can only add files when you are on a branch"
msgstr "Oni povas aldoni dosierojn nur kiam oni estas en branĉo"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Vi ne povas krei pliajn projektojn"
@@ -2186,6 +2460,9 @@ msgstr "Vi ne povos eltiri aÅ­ alpuÅi kodon per SSH antaÅ­ ol vi %{add_ssh_key_
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Via nomo"
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] "patro"
msgstr[1] "patroj"
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index c930e22a083..31ff4e08592 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:37-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -34,6 +34,11 @@ msgstr[1] "%s cambios adicionales han sido omitidos para evitar problemas de ren
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} cambió %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -54,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -110,9 +121,18 @@ msgstr "Agregar una clave SSH a tu perfil para actualizar o enviar a través de
msgid "Add new directory"
msgstr "Agregar nuevo directorio"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -128,6 +148,9 @@ msgstr "¿Estás seguro que deseas eliminar esta programación del pipeline?"
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -161,18 +184,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -235,6 +261,9 @@ msgstr[1] "Ramas"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Buscar ramas"
@@ -445,15 +474,24 @@ msgstr "omitido"
msgid "CiStatus|running"
msgstr "en ejecución"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -496,27 +534,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -526,7 +570,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -538,18 +585,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -582,6 +623,11 @@ 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 ""
@@ -663,6 +709,12 @@ msgstr "Guía de contribución"
msgid "Contributors"
msgstr "Contribuidores"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -684,9 +736,21 @@ msgstr "Crear directorio"
msgid "Create empty bare repository"
msgstr "Crear repositorio vacío"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Crear solicitud de fusión"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Crear nuevo..."
@@ -773,6 +837,9 @@ msgstr "Nombre del directorio"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -845,12 +912,18 @@ msgstr "Todas las semanas (domingos a las 4:00 am)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Error al cambiar el propietario"
msgid "Failed to remove the pipeline schedule"
msgstr "Error al eliminar la programación del pipeline"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Archivos"
@@ -895,9 +968,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -940,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -984,6 +1108,12 @@ msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Patrón de intervalo"
@@ -1052,6 +1182,9 @@ msgstr "Más información en la"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "documentación sobre la programación de pipelines"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "Abandonar grupo"
@@ -1075,6 +1208,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Mediana"
@@ -1105,6 +1244,9 @@ msgstr ""
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nueva incidencia"
@@ -1122,18 +1264,27 @@ msgstr "Nuevo directorio"
msgid "New file"
msgstr "Nuevo archivo"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Nueva incidencia"
msgid "New merge request"
msgstr "Nueva solicitud de fusión"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Nueva programación"
msgid "New snippet"
msgstr "Nuevo fragmento de código"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Nueva etiqueta"
@@ -1212,6 +1363,12 @@ msgstr "Vigilancia"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
@@ -1353,9 +1510,54 @@ msgstr "con etapas"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Proyecto ‘%{project_name}’ en cola para eliminación."
@@ -1365,9 +1567,6 @@ msgstr "Proyecto ‘%{project_name}’ fue creado satisfactoriamente."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Proyecto ‘%{project_name}’ será eliminado."
-
msgid "Project access must be granted explicitly to each user."
msgstr "El acceso al proyecto debe concederse explícitamente a cada usuario."
@@ -1425,6 +1624,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1446,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Leer más"
@@ -1515,6 +1729,9 @@ msgstr "Revertir esta solicitud de fusión"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1533,6 +1750,15 @@ msgstr "Programación de Pipelines"
msgid "Search branches and tags"
msgstr "Buscar ramas y etiquetas"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "Seleccionar formato de archivo"
@@ -1580,13 +1806,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1706,6 +1935,12 @@ msgstr "Iniciar una %{new_merge_request} con estos cambios"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Cambiar rama/etiqueta"
@@ -1732,6 +1967,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."
@@ -1744,6 +1982,15 @@ msgstr "La relación con la bifurcación se ha eliminado."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "La etapa del ciclo de vida de desarrollo."
@@ -1774,6 +2021,12 @@ msgstr "La etapa de puesta en escena muestra el tiempo entre la fusión y el des
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "El tiempo utilizado por cada entrada de datos obtenido por esa etapa."
@@ -1783,6 +2036,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 branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1967,6 +2223,9 @@ msgstr ""
msgid "Unstar"
msgstr "No Destacar"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2030,6 +2289,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2150,9 +2412,21 @@ msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{f
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "Solo puedes agregar archivos cuando estás en una rama"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Has alcanzado el límite de tu proyecto"
@@ -2186,6 +2460,9 @@ msgstr "No podrás actualizar o enviar código al proyecto a través de SSH hast
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Tu nombre"
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] "padre"
msgstr[1] "padres"
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 56cc02c55d5..a4f9a34b13b 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:36-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -23,8 +23,8 @@ msgstr[1] "%d validations"
msgid "%d layer"
msgid_plural "%d layers"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d couche"
+msgstr[1] "%d couches"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -34,6 +34,11 @@ msgstr[1] "%s validations supplémentaires ont été masquées afin d'éviter de
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} a validé %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} validations de retard sur %{default_branch}, %{number_commits_ahead} validations d'avance"
@@ -54,6 +59,12 @@ msgstr[1] "%{storage_name} : %{failed_attempts} tentatives d’accès au stocka
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(Lisez %{link} pour savoir comment l'installer)."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
@@ -63,13 +74,13 @@ msgid "1st contribution!"
msgstr "1ère contribution !"
msgid "2FA enabled"
-msgstr ""
+msgstr "2FA activé"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)"
msgid "About auto deploy"
-msgstr "A propos de l'auto-déploiement"
+msgstr "À propos de l'auto-déploiement"
msgid "Abuse Reports"
msgstr "Rapports d’abus"
@@ -99,7 +110,7 @@ msgid "Add Contribution guide"
msgstr "Ajouter un guide de contribution"
msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Ajouter des Webhooks de groupe et GitLab Enterprise Edition."
msgid "Add License"
msgstr "Ajouter une licence"
@@ -110,9 +121,18 @@ msgstr "Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser
msgid "Add new directory"
msgstr "Ajouter un nouveau dossier"
+msgid "AdminHealthPageLink|health page"
+msgstr "État des services"
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "Tous"
+msgid "An error occurred. Please try again."
+msgstr "Une erreur est survenue. Merci de réessayer."
+
msgid "Appearance"
msgstr "Apparence"
@@ -123,13 +143,16 @@ msgid "Archived project! Repository is read-only"
msgstr "Projet archivé ! Le dépôt est en lecture seule"
msgid "Are you sure you want to delete this pipeline schedule?"
-msgstr "Êtes-vous sûr de vouloir supprimer ce pipeline programmé"
+msgstr "Êtes-vous sûr·e de vouloir supprimer ce pipeline programmé ?"
msgid "Are you sure you want to discard your changes?"
-msgstr "Êtes-vous sûr de vouloir annuler vos modifications ?"
+msgstr "Êtes-vous sûr·e de vouloir annuler vos modifications ?"
+
+msgid "Are you sure you want to leave this group?"
+msgstr "Êtes-vous sûr·e de vouloir quitter ce groupe ?"
msgid "Are you sure you want to reset registration token?"
-msgstr "Êtes-vous sûr de vouloir réinitialiser le jeton d’inscription ?"
+msgstr "Êtes-vous sûr·e de vouloir réinitialiser le jeton d’inscription ?"
msgid "Are you sure you want to reset the health check token?"
msgstr "Êtes-vous sûr de vouloir réinitialiser le jeton de bilan de santé ?"
@@ -147,64 +170,67 @@ msgid "Authentication Log"
msgstr "Journal d'authentification"
msgid "Author"
-msgstr ""
+msgstr "Auteur"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Les applications de révision automatique et le déploiement automatique requièrent un nom de domaine et un %{kubernetes} pour fonctionner correctement."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "Les applications de révision automatique et de déploiement automatique requièrent un nom de domaine pour fonctionner correctement."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Les applications de révision automatique et de déploiement automatique requièrent un %{kubernetes} pour fonctionner correctement."
msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr ""
-
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
+msgstr "Auto DevOps (Béta)"
msgid "AutoDevOps|Auto DevOps documentation"
-msgstr ""
+msgstr "Documentation Auto DevOps"
msgid "AutoDevOps|Enable in settings"
-msgstr ""
+msgstr "Activer dans les paramètres"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "AutoDevOps vous permet de construire, tester et déployer automatiquement votre application à partir d'une configuration CI/CD prédéfinie."
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
-msgstr ""
+msgstr "En savoir plus dans %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Vous pouvez activer %{link_to_settings} pour ce projet."
msgid "Billing"
msgstr "Facturation"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} est actuellement abonné au forfait %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "La mise à niveau de certains abonnements n’est actuellement pas disponible."
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "Abonnement actuel"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "Support client"
msgid "BillingPlans|Downgrade"
-msgstr ""
+msgstr "Retour à un forfait inférieur"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "En savoir plus sur chaque abonnement en lisant nos %{faq_link}."
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "Gérer l'abonnement"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "Merci de contacter %{customer_support_link} à ce sujet."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "Voir toutes les fonctionnalités de l’abonnement %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr "Ce groupe utilise le plan associé à son groupe parent."
+msgstr "Ce groupe utilise l’abonnement associé à son groupe parent."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr "Pour gérer le plan de ce groupe, visitez la section facturation de %{parent_billing_page_link}."
@@ -213,19 +239,19 @@ msgid "BillingPlans|Upgrade"
msgstr "Mise à niveau"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Vous êtes actuellement abonné·e au forfait %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "Foire aux questions"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "Mensuellement"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "au prix annuel de %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr ""
+msgstr "par utilisateur"
msgid "Branch"
msgid_plural "Branches"
@@ -233,7 +259,10 @@ msgstr[0] "Branche"
msgstr[1] "Branches"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
-msgstr "La branche <strong>%{branch_name}</strong> a été crée. Pour mettre en place le déploiement automatisé, sélectionnez un modèle de fichier Yaml pour l'intégration continue (CI) de GitLab, et validez les modifications. %{link_to_autodeploy_doc}"
+msgstr "La branche <strong>%{branch_name}</strong> a été créée. Pour mettre en place le déploiement automatisé, sélectionnez un modèle de fichier Yaml pour l'intégration continue (CI) de GitLab, et validez les modifications. %{link_to_autodeploy_doc}"
+
+msgid "Branch has changed"
+msgstr "La branche a été modifiée"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Rechercher la branche"
@@ -314,7 +343,7 @@ msgid "Branches|To discard the local changes and overwrite the branch with the u
msgstr "Pour rejeter les changements locaux et écraser la branche avec la version du dépôt en amont, veuillez la supprimer ici puis cliquez ci-dessus sur 'Mettre à jour maintenant'."
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr "Vous êtes sur le point de supprimer définitivement la branche protégées %{branch_name}."
+msgstr "Vous êtes sur le point de supprimer définitivement la branche protégée %{branch_name}."
msgid "Branches|diverged from upstream"
msgstr "a dévié du dépôt en amont"
@@ -362,13 +391,13 @@ msgid "Change Weight"
msgstr "Changer le poids"
msgid "ChangeTypeActionLabel|Pick into branch"
-msgstr "Sélectionner dans la branche"
+msgstr "Picorer dans la branche"
msgid "ChangeTypeActionLabel|Revert in branch"
msgstr "Défaire dans la branche"
msgid "ChangeTypeAction|Cherry-pick"
-msgstr "Sélectionner"
+msgstr "Picorer"
msgid "ChangeTypeAction|Revert"
msgstr "Défaire"
@@ -377,19 +406,19 @@ msgid "Changelog"
msgstr "Journal des modifications"
msgid "Charts"
-msgstr "Graphiques"
+msgstr "Statistiques"
msgid "Chat"
msgstr "Chat"
msgid "Cherry-pick this commit"
-msgstr "Sélectionner cette validation"
+msgstr "Picorer cette validation"
msgid "Cherry-pick this merge request"
-msgstr "Sélectionner cette demande de fusion"
+msgstr "Picorer cette demande de fusion"
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
-msgstr ""
+msgstr "Choisissez quels groupes vous souhaitez répliquer sur le nœud secondaire. Laissez vide pour tous les répliquer."
msgid "CiStatusLabel|canceled"
msgstr "annulé"
@@ -445,134 +474,146 @@ msgstr "ignoré"
msgid "CiStatus|running"
msgstr "en cours"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "CircuitBreaker API"
+
msgid "Clone repository"
-msgstr ""
+msgstr "Cloner le dépôt"
msgid "Close"
msgstr "Fermer"
+msgid "Cluster"
+msgstr "Cluster"
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr ""
+msgstr "Un %{link_to_container_project} doit avoir été créé pour ce compte"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Détails du cluster"
msgid "ClusterIntegration|Cluster integration"
-msgstr ""
+msgstr "Intégration du cluster"
msgid "ClusterIntegration|Cluster integration is disabled for this project."
-msgstr ""
+msgstr "L'intégration du cluster est désactivée pour ce projet."
msgid "ClusterIntegration|Cluster integration is enabled for this project."
-msgstr ""
+msgstr "L'intégration du cluster est activée pour ce projet."
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr ""
+msgstr "L'intégration de cluster est activée pour ce projet. La désactivation de cette intégration n’affectera pas votre cluster, il coupera temporairement la connexion de GitLab à celui-ci."
msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr ""
+msgstr "Le cluster est en cours de création sur Google Container Engine…"
msgid "ClusterIntegration|Cluster name"
-msgstr ""
+msgstr "Nom du cluster"
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr ""
+msgstr "Le cluster a été correctement créé sur Google Container Engine"
msgid "ClusterIntegration|Copy cluster name"
-msgstr ""
+msgstr "Copier le nom du cluster"
msgid "ClusterIntegration|Create cluster"
-msgstr ""
+msgstr "Créer le cluster"
msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr ""
+msgstr "Créer un nouveau cluster sur Google Container Engine"
msgid "ClusterIntegration|Enable cluster integration"
-msgstr ""
+msgstr "Activer l’intégration du cluster"
msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr ""
+msgstr "ID de projet Google Cloud Platform"
msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Google Container Engine"
msgid "ClusterIntegration|Google Container Engine project"
-msgstr ""
-
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Google Container Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr ""
+msgstr "En savoir plus sur %{link_to_documentation}"
-msgid "ClusterIntegration|See machine types"
-msgstr ""
+msgid "ClusterIntegration|Machine type"
+msgstr "Type de machine"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
-msgstr ""
+msgstr "Assurez-vous que votre compte %{link_to_requirements} pour créer des clusters"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Gérer l’intégration du cluster sur votre projet GitLab"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "Gérer votre cluster en visitant le lien %{link_gke}"
msgid "ClusterIntegration|Number of nodes"
-msgstr ""
+msgstr "Nombre de nœuds"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "Veuillez vous assurer que votre compte Google répond aux exigences suivantes : "
msgid "ClusterIntegration|Project namespace (optional, unique)"
-msgstr ""
+msgstr "Espace de noms du projet (facultatif, unique)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Lire notre %{link_to_help_page} sur l’intégration d’un cluster."
msgid "ClusterIntegration|Remove cluster integration"
-msgstr ""
+msgstr "Retirer l’intégration du cluster"
msgid "ClusterIntegration|Remove integration"
-msgstr ""
+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 ""
+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|Save changes"
-msgstr ""
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "Voir et modifier les détails de votre cluster"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "Voir les types de machine"
msgid "ClusterIntegration|See your projects"
-msgstr ""
+msgstr "Voir vos projets"
msgid "ClusterIntegration|See zones"
-msgstr ""
+msgstr "Voir les zones"
msgid "ClusterIntegration|Something went wrong on our end."
-msgstr ""
-
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
+msgstr "Un problème est survenu de notre côté."
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
-msgstr ""
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "Un problème est survenu lors de la création de votre cluster sur Google Container Engine."
msgid "ClusterIntegration|Toggle Cluster"
-msgstr ""
-
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
+msgstr "Activer/désactiver le cluster"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
-msgstr ""
+msgstr "Avec un cluster associé à ce projet, vous pouvez utiliser des applications de revue, déployer vos applications, exécuter vos pipelines et bien plus encore, de manière très simple."
msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr ""
+msgstr "Votre compte doit disposer de %{link_to_container_engine}"
msgid "ClusterIntegration|Zone"
-msgstr ""
+msgstr "Zone"
msgid "ClusterIntegration|access to Google Container Engine"
-msgstr ""
+msgstr "Accéder à Google Container Engine"
msgid "ClusterIntegration|cluster"
-msgstr ""
+msgstr "cluster"
msgid "ClusterIntegration|help page"
-msgstr ""
+msgstr "page d’aide"
msgid "ClusterIntegration|meets the requirements"
-msgstr ""
+msgstr "répond aux exigences"
msgid "ClusterIntegration|properly configured"
-msgstr ""
+msgstr "correctement configuré"
msgid "Comments"
msgstr "Commentaires"
@@ -582,8 +623,13 @@ msgid_plural "Commits"
msgstr[0] "Validation"
msgstr[1] "Validations"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Commit Message"
-msgstr ""
+msgstr "Message de validation"
msgid "Commit duration in minutes for last 30 commits"
msgstr "Durée des 30 derniers pipelines en minutes"
@@ -613,49 +659,49 @@ msgid "Compare"
msgstr "Comparer"
msgid "Container Registry"
-msgstr ""
+msgstr "Registre de conteneur"
msgid "ContainerRegistry|Created"
-msgstr ""
+msgstr "Créé"
msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
-msgstr ""
+msgstr "D’abord inscrivez-vous au registre de conteneur GitLab à l’aide de votre nom d’utilisateur GitLab et de votre mot de passe. Si vous avez %{link_2fa} vous devrez utiliser un %{link_token} :"
msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
-msgstr ""
+msgstr "GitLab prend en charge jusqu'à 3 niveaux de noms d’image. Les exemples d’images suivants sont valides pour votre projet :"
msgid "ContainerRegistry|How to use the Container Registry"
-msgstr ""
+msgstr "Comment utiliser le registre de conteneur"
msgid "ContainerRegistry|Learn more about"
-msgstr ""
+msgstr "En savoir plus sur"
msgid "ContainerRegistry|No tags in Container Registry for this container image."
-msgstr ""
+msgstr "Aucune étiquettes dans le registre de conteneur pour l’image de ce conteneur."
msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
-msgstr ""
+msgstr "Une fois identifié·e, vous êtes libre de créer et télécharger une image de conteneur en utilisant les commandes courantes de %{build} et %{push}"
msgid "ContainerRegistry|Remove repository"
-msgstr ""
+msgstr "Supprimer le dépôt"
msgid "ContainerRegistry|Remove tag"
-msgstr ""
+msgstr "Supprimer l’étiquette"
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "Taille"
msgid "ContainerRegistry|Tag"
-msgstr ""
+msgstr "Étiquette"
msgid "ContainerRegistry|Tag ID"
-msgstr ""
+msgstr "ID d‘étiquette"
msgid "ContainerRegistry|Use different image names"
-msgstr ""
+msgstr "Utilisez des noms d’images différents"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
-msgstr ""
+msgstr "Avec le registre de conteneur Docker intégré à GitLab, chaque projet peut avoir son propre espace pour stocker ses images Docker."
msgid "Contribution guide"
msgstr "Guilde de contribution"
@@ -663,6 +709,12 @@ msgstr "Guilde de contribution"
msgid "Contributors"
msgstr "Contributeurs"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Contrôler la concurrence maximale des remplacements de fichier-joint LFS pour ce nœud secondaire"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Contrôler la concurrence maximale des remplacements de dépôt pour ce nœud secondaire"
+
msgid "Copy SSH public key to clipboard"
msgstr "Copier la clé publique SSH dans le presse-papier"
@@ -684,9 +736,21 @@ msgstr "Créer un dossier"
msgid "Create empty bare repository"
msgstr "Créer un dépôt vide"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Créer une demande de fusion"
+msgid "Create new branch"
+msgstr "Créer une nouvelle branche"
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Créer nouveau..."
@@ -721,7 +785,7 @@ msgid "CycleAnalyticsStage|Code"
msgstr "Code"
msgid "CycleAnalyticsStage|Issue"
-msgstr "Incident"
+msgstr "Ticket"
msgid "CycleAnalyticsStage|Plan"
msgstr "Planification"
@@ -762,7 +826,7 @@ msgid "Description"
msgstr "Description"
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
-msgstr ""
+msgstr "Les modèles de description permettent de définir des modèles spécifiques au contexte pour les champs de description de tickets et demandes de fusion pour votre projet."
msgid "Details"
msgstr "Détails"
@@ -773,8 +837,11 @@ msgstr "Nom du dossier"
msgid "Discard changes"
msgstr "Supprimer les modifications"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Passer l’introduction Cycle Analytics"
+
msgid "Dismiss Merge Request promotion"
-msgstr ""
+msgstr "Rejeter la promotion de la demande de fusion"
msgid "Don't show again"
msgstr "Ne plus montrer"
@@ -822,7 +889,7 @@ msgid "EventFilterBy|Filter by comments"
msgstr "Filtrer par commentaires"
msgid "EventFilterBy|Filter by issue events"
-msgstr "Filtrer par événements d'incident"
+msgstr "Filtrer par événements de ticket"
msgid "EventFilterBy|Filter by merge events"
msgstr "Filtrer par événements de fusion"
@@ -834,23 +901,29 @@ msgid "EventFilterBy|Filter by team"
msgstr "Filtrer par équipe"
msgid "Every day (at 4:00am)"
-msgstr "Chaque jour (à 4:00 du matin)"
+msgstr "Chaque jour (à 4h00 du matin)"
msgid "Every month (on the 1st at 4:00am)"
msgstr "Chaque mois (le 1er à 4:00 du matin)"
msgid "Every week (Sundays at 4:00am)"
-msgstr "Chaque semaine (dimanche à 4:00 du matin)"
+msgstr "Chaque semaine (dimanche à 4h00 du matin)"
msgid "Explore projects"
msgstr "Explorer les projets"
+msgid "Explore public groups"
+msgstr "Explorer les groupes publics"
+
msgid "Failed to change the owner"
msgstr "Échec du changement de propriétaire"
msgid "Failed to remove the pipeline schedule"
msgstr "Échec de la suppression du pipeline programmé"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Fichiers"
@@ -878,13 +951,13 @@ msgid "ForkedFromProjectPath|Forked from"
msgstr "Fourché depuis"
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
-msgstr ""
+msgstr "Fourché depuis %{project_name} (supprimé)"
msgid "Format"
msgstr "Format"
msgid "From issue creation until deploy to production"
-msgstr "Depuis la création de l'incident jusqu'au déploiement en production"
+msgstr "Depuis la création du ticket jusqu'au déploiement en production"
msgid "From merge request merge until deploy to production"
msgstr "Depuis la fusion de la demande de fusion jusqu'au déploiement en production"
@@ -893,19 +966,25 @@ msgid "GPG Keys"
msgstr "Clés GPG"
msgid "Geo Nodes"
-msgstr ""
+msgstr "NÅ“uds Geo"
+
+msgid "Geo|File sync capacity"
+msgstr "Capacité de synchronisation de fichier"
msgid "Geo|Groups to replicate"
-msgstr ""
+msgstr "Groupes à répliquer"
+
+msgid "Geo|Repository sync capacity"
+msgstr "Capacité de synchronisation du dépôt"
msgid "Geo|Select groups to replicate."
-msgstr ""
+msgstr "Sélectionner les groupes à répliquer."
msgid "Git storage health information has been reset"
msgstr "Les informations de santé du stockage Git ont été réinitialisées"
msgid "GitLab Runner section"
-msgstr "Section de Runner GitLab"
+msgstr "Section de l'Exécuteur GitLab"
msgid "Go to your fork"
msgstr "Aller à votre fourche"
@@ -914,37 +993,82 @@ msgid "GoToYourFork|Fork"
msgstr "Fourche"
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
-msgstr ""
+msgstr "L’authentification Google n’est pas %{link_to_documentation}. Demandez à votre administrateur GitLab si vous souhaitez utiliser ce service."
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Empêcher le partage d'un projet de %{group} avec d'autres groupes"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Verrou de partage avec un groupe"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Ce paramètre est appliqué sur %{ancestor_group} et a été modifié dans ce sous-groupe."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Ce paramètre est appliqué sur %{ancestor_group}. Pour partager des projets dans ce groupe avec un autre groupe, demander au propriétaire de modifier le paramètre ou %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Ce paramètre s’applique sur %{ancestor_group}. Vous pouvez modifier le paramètre ou le %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "Ce paramètre s’appliquera à tous les sous-groupes, sauf s’il est modifié par un propriétaire du groupe. Les groupes déjà liés au projet continueront d’y avoir accès à moins d’être retirés manuellement."
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "ne peut pas être désactivé lorsque le groupe parent « Verrou de partage avec un groupe » est activé, sauf pour le propriétaire du groupe parent"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "supprimer le verrou de partage avec un groupe pour %{ancestor_group_name}"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Un groupe est une collection de plusieurs projets."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "Si vous organisez vos projets au sein d’un groupe, il fonctionnera comme un dossier."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Aucun groupe trouvé"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Vous pouvez gérer les autorisations des membres de votre groupe et accéder à chacun de ses projets."
+
+msgid "GroupsTreeRole|as"
+msgstr "comme"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Êtes-vous sûr·e de vouloir quitter le groupe « ${this.group.fullName} » ?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Créez un projet dans ce groupe."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Créer un sous-groupe de ce groupe."
+
+msgid "GroupsTree|Edit group"
+msgstr "Modifier le groupe"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Impossible de quitter le groupe. Veuillez vous assurer que vous n’êtes pas seul·e propriétaire."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Filtrer par nom…"
+
+msgid "GroupsTree|Leave this group"
+msgstr "Quitter ce groupe"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Chargement des groupes"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "Désolé, aucun groupe ne correspond à vos critères de recherche"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "Désolé, aucun groupe ni projet ne correspond à vos critères de recherche"
msgid "Health Check"
-msgstr "Bilan de santé"
+msgstr "État des services"
msgid "Health information can be retrieved from the following endpoints. More information is available"
-msgstr "Des informations de santé peuvent être récupérées depuis les adresses suivantes. Plus d’informations"
+msgstr "L’état des service peut être récupéré depuis les adresses suivantes. Plus d’information disponible."
msgid "HealthCheck|Access token is"
msgstr "Le jeton d’accès est"
@@ -968,21 +1092,27 @@ msgid "Import repository"
msgstr "Importer un dépôt"
msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr ""
+msgstr "Améliorer le tableau de tickets avec GitLab Entreprise Edition."
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Améliorer la gestion des tickets avec les poids de ticket et GitLab Entreprise Edition."
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Améliorer la recherche avec la Recherche Globale Avancée et GitLab Enterprise Edition."
msgid "Install a Runner compatible with GitLab CI"
-msgstr "Installez un Runner compatible avec l'intégration continue de GitLab"
+msgstr "Installez un Exécuteur compatible avec l'intégration continue de GitLab"
msgid "Instance"
msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Instance"
+msgstr[1] "Instances"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Interne - Le groupe ainsi que tous les projets internes sont accessibles pour n’importe quel·le utilisa·teur·trice connecté·e."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Interne - Le projet est accessible pour n’importe quel·le utilisa·teur·trice connecté·e."
msgid "Interval Pattern"
msgstr "Schéma d’intervalle"
@@ -991,13 +1121,13 @@ msgid "Introducing Cycle Analytics"
msgstr "Introduction à l'analyseur de cycle"
msgid "Issue board focus mode"
-msgstr ""
+msgstr "Mode focus du tableau de tickets"
msgid "Issue boards with milestones"
-msgstr "Tableaux d'incidents avec leurs jalons"
+msgstr "Tableaux des tickets avec leurs jalons"
msgid "Issue events"
-msgstr "Événements de l'incident"
+msgstr "Événements du ticket"
msgid "IssueBoards|Board"
msgstr "Tableau"
@@ -1006,7 +1136,7 @@ msgid "IssueBoards|Boards"
msgstr "Tableaux"
msgid "Issues"
-msgstr "Incidents"
+msgstr "Tickets"
msgid "LFSStatus|Disabled"
msgstr "Désactivé"
@@ -1015,7 +1145,7 @@ msgid "LFSStatus|Enabled"
msgstr "Activé"
msgid "Labels"
-msgstr "Étiquettes"
+msgstr "Labels"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -1029,16 +1159,16 @@ msgid "Last commit"
msgstr "Dernière validation"
msgid "Last edited %{date}"
-msgstr ""
+msgstr "Dernière modification le %{date}"
msgid "Last edited by %{name}"
-msgstr ""
+msgstr "Dernière modification par %{name}"
msgid "Last update"
-msgstr ""
+msgstr "Dernière mise à jour"
msgid "Last updated"
-msgstr ""
+msgstr "Dernière mise à jour"
msgid "LastPushEvent|You pushed to"
msgstr "Vous avez poussé sur"
@@ -1052,6 +1182,9 @@ msgstr "En apprendre plus dans le"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "documentation concernant la programmation de pipeline"
+msgid "Leave"
+msgstr "Quitter"
+
msgid "Leave group"
msgstr "Quitter le groupe"
@@ -1075,6 +1208,12 @@ msgstr "Verrouillé"
msgid "Locked Files"
msgstr "Fichiers verrouillés"
+msgid "Login"
+msgstr "Se connecter"
+
+msgid "Maximum git storage failures"
+msgstr "Nombre maximum d’échecs du stockage git"
+
msgid "Median"
msgstr "Médian"
@@ -1094,7 +1233,7 @@ msgid "Messages"
msgstr "Messages"
msgid "MissingSSHKeyWarningLink|add an SSH key"
-msgstr "ajouter une clef SSH"
+msgstr "ajouter une clé SSH"
msgid "Monitoring"
msgstr "Surveillance"
@@ -1103,12 +1242,15 @@ msgid "More information is available|here"
msgstr "ici"
msgid "Multiple issue boards"
-msgstr ""
+msgstr "Multiple tableaux de tickets"
+
+msgid "New Cluster"
+msgstr "Nouveau cluster"
msgid "New Issue"
msgid_plural "New Issues"
-msgstr[0] "Nouvel incident"
-msgstr[1] "Nouveaux incidents"
+msgstr[0] "Nouveau ticket"
+msgstr[1] "Nouveaux tickets"
msgid "New Pipeline Schedule"
msgstr "Nouveau pipeline programmé"
@@ -1120,25 +1262,34 @@ msgid "New directory"
msgstr "Nouveau dossier"
msgid "New file"
-msgstr "Nouveau Fichier"
+msgstr "Nouveau fichier"
+
+msgid "New group"
+msgstr "Nouveau groupe"
msgid "New issue"
-msgstr "Nouvel incident"
+msgstr "Nouveau ticket"
msgid "New merge request"
msgstr "Nouvelle demande de fusion"
+msgid "New project"
+msgstr "Nouveau projet"
+
msgid "New schedule"
msgstr "Nouveau programme"
msgid "New snippet"
msgstr "Nouvel extrait de code"
+msgid "New subgroup"
+msgstr "Nouveau sous-groupe"
+
msgid "New tag"
msgstr "Nouveau tag"
msgid "No container images stored for this project. Add one by following the instructions above."
-msgstr ""
+msgstr "Aucune image de conteneur stockée pour ce projet. Ajoutez en une en suivant les instructions ci-dessous."
msgid "No repository"
msgstr "Pas de dépôt"
@@ -1147,7 +1298,7 @@ msgid "No schedules"
msgstr "Aucun programme"
msgid "None"
-msgstr "Aucun(e)"
+msgstr "Aucun·e"
msgid "Not available"
msgstr "Indisponible"
@@ -1159,7 +1310,7 @@ msgid "Notification events"
msgstr "Événement de notifications"
msgid "NotificationEvent|Close issue"
-msgstr "Clore l'incident"
+msgstr "Clore le ticket"
msgid "NotificationEvent|Close merge request"
msgstr "Clore la demande de fusion"
@@ -1171,7 +1322,7 @@ msgid "NotificationEvent|Merge merge request"
msgstr "Fusionner le demande de fusion"
msgid "NotificationEvent|New issue"
-msgstr "Nouvel incident"
+msgstr "Nouveau ticket"
msgid "NotificationEvent|New merge request"
msgstr "Nouvelle demande de fusion"
@@ -1180,13 +1331,13 @@ msgid "NotificationEvent|New note"
msgstr "Nouvelle note"
msgid "NotificationEvent|Reassign issue"
-msgstr "Réassigner l'incident"
+msgstr "Réassigner le ticket"
msgid "NotificationEvent|Reassign merge request"
msgstr "Réassigner la demande de fusion"
msgid "NotificationEvent|Reopen issue"
-msgstr "Ré-ouvrir l'incident"
+msgstr "Ré-ouvrir le ticket"
msgid "NotificationEvent|Successful pipeline"
msgstr "Pipeline réussi"
@@ -1212,17 +1363,23 @@ msgstr "Surveillé"
msgid "Notifications"
msgstr "Notifications"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtre"
msgid "Only project members can comment."
-msgstr ""
+msgstr "Seuls les membres du projet peuvent commenter."
msgid "OpenedNDaysAgo|Opened"
msgstr "Ouvert"
msgid "Opens in a new window"
-msgstr ""
+msgstr "Ouvrir dans une nouvelle fenêtre"
msgid "Options"
msgstr "Paramètres"
@@ -1249,7 +1406,7 @@ msgid "Password"
msgstr "Mot de Passe"
msgid "People without permission will never get a notification and won\\'t be able to comment."
-msgstr ""
+msgstr "Les personnes sans autorisation ne recevront jamais de notifications et ne pourront pas commenter."
msgid "Pipeline"
msgstr "Pipeline"
@@ -1330,13 +1487,13 @@ msgid "Pipelines charts"
msgstr "Graphique des pipelines"
msgid "Pipelines for last month"
-msgstr "Pipelines pour le dernier mois"
+msgstr "Pipelines du dernier mois"
msgid "Pipelines for last week"
-msgstr "Pipelines pour la dernière semaine"
+msgstr "Pipelines de la semaine dernière"
msgid "Pipelines for last year"
-msgstr "Pipelines pour la dernière année"
+msgstr "Pipelines de l’année dernière"
msgid "Pipeline|all"
msgstr "Tous"
@@ -1353,9 +1510,54 @@ msgstr "avec les étapes"
msgid "Preferences"
msgstr "Préférences"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Privé - L’accès au projet doit être autorisé explicitement pour chaque utilisa·teur·trice."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "Privé - Le groupe ainsi que ses projets ne sont accessibles qu’à ses membres."
+
msgid "Profile"
msgstr "Profil"
+msgid "Profiles|Account scheduled for removal."
+msgstr "Compte programmé pour sa suppression."
+
+msgid "Profiles|Delete Account"
+msgstr "Supprimer le compte"
+
+msgid "Profiles|Delete account"
+msgstr "Supprimer le compte"
+
+msgid "Profiles|Delete your account?"
+msgstr "Supprimer votre compte ?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Supprimer un compte aura les conséquences suivantes :"
+
+msgid "Profiles|Invalid password"
+msgstr "Mot de passe incorrect"
+
+msgid "Profiles|Invalid username"
+msgstr "Nom d’utilisateur incorrect"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Saisissez votre %{confirmationValue} pour confirmer :"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "Vous n’avez pas les autorisations suffisantes pour supprimer cet·te utilisa·teur·trice."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Vous devez transférer la propriété ou supprimer ces groupes avant de pouvoir supprimer votre compte."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Votre compte est actuellement propriétaire des groupes suivants :"
+
+msgid "Profiles|your account"
+msgstr "votre compte"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "Le projet “%{project_name}†est en train d’être supprimé."
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projet '%{project_name}' en attente de suppression."
@@ -1365,9 +1567,6 @@ msgstr "Projet '%{project_name}' créé avec succès."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Projet '%{project_name}' mis à jour avec succès."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Le projet '%{project_name}' sera supprimé."
-
msgid "Project access must be granted explicitly to each user."
msgstr "L’accès au projet doit être explicitement accordé à chaque utilisateur."
@@ -1408,23 +1607,29 @@ msgid "ProjectLifecycle|Stage"
msgstr "Étape"
msgid "ProjectNetworkGraph|Graph"
-msgstr "Graphique "
+msgstr "Graphes"
msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr ""
+msgstr "Contactez un administrateur pour modifier ce paramètre."
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
-msgstr ""
+msgstr "Seules les validations signées peuvent être poussées sur ce dépôt."
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr ""
+msgstr "Ce paramètre est appliqué au niveau du serveur et peut être modifié par un administrateur."
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr ""
+msgstr "Ce paramètre est appliqué au niveau du serveur mais il a été modifié pour ce projet."
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "Ce paramètre s’appliquera à tous les projets à moins qu’un administrateur ne le modifie."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
+msgid "Projects"
+msgstr "Projets"
+
msgid "ProjectsDropdown|Frequently visited"
msgstr "Fréquemment visité"
@@ -1446,12 +1651,21 @@ msgstr "Désolé, aucun projet ne correspond à votre recherche"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Cette fonctionnalité requiert le support du localStorage par votre navigateur"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Public - Le groupe ainsi que n’importe quel projet public est accessible sans authentification."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Public - Le projet est accessible sans aucune authentification."
+
msgid "Push Rules"
msgstr "Règles de poussée"
msgid "Push events"
msgstr "Évènements de poussée"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Lire plus"
@@ -1471,10 +1685,10 @@ msgid "Related Commits"
msgstr "Validations liés"
msgid "Related Deployed Jobs"
-msgstr "Tâches de déploiement liés"
+msgstr "Tâches de déploiement liées"
msgid "Related Issues"
-msgstr "Incidents liés"
+msgstr "Tickets liés"
msgid "Related Jobs"
msgstr "Tâches liées"
@@ -1504,7 +1718,7 @@ msgid "Reset health check access token"
msgstr "Réinitialiser le jeton d’accès au bilan de santé"
msgid "Reset runners registration token"
-msgstr "Réinitialiser le jeton d’inscription des Runners"
+msgstr "Réinitialiser le jeton d’inscription des exécuteurs"
msgid "Revert this commit"
msgstr "Défaire cette validation"
@@ -1515,6 +1729,9 @@ msgstr "Défaire cette demande de fusion"
msgid "SSH Keys"
msgstr "Clés SSH"
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr "Enregistrer les modifications"
@@ -1533,6 +1750,15 @@ msgstr "Programmer des pipelines"
msgid "Search branches and tags"
msgstr "Rechercher dans les branches et les étiquettes"
+msgid "Seconds before reseting failure information"
+msgstr "Nombre de secondes avant de réinitialiser les informations d’échec"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Nombre de secondes d'attente après un échec de stockage"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "Nombre de secondes d’attente pour un essai d'accès au stockage"
+
msgid "Select Archive Format"
msgstr "Sélectionnez le format de l'archive"
@@ -1578,16 +1804,19 @@ msgid "Snippets"
msgstr "Extraits de code"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "Une erreur est survenue de notre côté."
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "Quelque chose ne s'est pas bien passé en essayant de changer l’état de verrouillage de cet ${this.issuableDisplayName(this.issuableType)}"
msgid "Something went wrong while fetching the projects."
-msgstr ""
+msgstr "Une erreur s'est produite lors de la récupération des projets."
msgid "Something went wrong while fetching the registry list."
-msgstr ""
+msgstr "Une erreur s'est produite lors de la récupération de la liste du registre."
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr ""
+msgid "Sort by"
+msgstr "Trier par"
msgid "SortOptions|Access level, ascending"
msgstr "Niveau d’accès, croissant"
@@ -1608,7 +1837,7 @@ msgid "SortOptions|Due soon"
msgstr "Échéance proche"
msgid "SortOptions|Label priority"
-msgstr "Priorité des étiquettes"
+msgstr "Priorité du label"
msgid "SortOptions|Largest group"
msgstr "Taille de groupe"
@@ -1662,7 +1891,7 @@ msgid "SortOptions|Oldest joined"
msgstr "Rejoint depuis longtemps"
msgid "SortOptions|Oldest sign in"
-msgstr "Authentifié depuis longtemps"
+msgstr "Authentifié·e depuis longtemps"
msgid "SortOptions|Oldest updated"
msgstr "Mise à jour depuis longtemps"
@@ -1674,7 +1903,7 @@ msgid "SortOptions|Priority"
msgstr "Priorité"
msgid "SortOptions|Recent sign in"
-msgstr "Authentifié récemment"
+msgstr "Authentifié·e récemment"
msgid "SortOptions|Start later"
msgstr "Commence plus tard"
@@ -1692,10 +1921,10 @@ msgid "Spam Logs"
msgstr "Journaux des messages indésirables"
msgid "Specify the following URL during the Runner setup:"
-msgstr "Spécifiez l’URL suivante lors de la configuration du Runner :"
+msgstr "Spécifiez l’URL suivante lors de la configuration de l'Exécuteur :"
msgid "StarProject|Star"
-msgstr "S'abonner"
+msgstr "Mettre en favori"
msgid "Starred projects"
msgstr "Projets favoris"
@@ -1704,7 +1933,13 @@ msgid "Start a %{new_merge_request} with these changes"
msgstr "Créer une %{new_merge_request} avec ces changements"
msgid "Start the Runner!"
-msgstr "Démarrer le Runner !"
+msgstr "Démarrer l'Exécuteur !"
+
+msgid "Subgroups"
+msgstr "Sous-groupes"
+
+msgid "Subscribe"
+msgstr ""
msgid "Switch branch/tag"
msgstr "Changer de branche / d'étiquette"
@@ -1730,6 +1965,9 @@ msgid "Thanks! Don't show me this again"
msgstr "Merci de ne plus afficher ce message"
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "La Recherche Globale Avancée de GitLab est un outils qui vous fait gagner du temps. Au lieu de créer du code similaire et perdre du temps, vous pouvez maintenant chercher dans le code d'autres équipes pour vous aider sur votre projet."
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
@@ -1742,22 +1980,31 @@ msgid "The fork relationship has been removed."
msgstr "La relation de fourche a été supprimée."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
-msgstr "L'étape des incidents montre le temps nécessaire entre la création d'un incident et son assignation à un jalon, ou son ajout à une liste d'un tableau d'incidents. Débutez à créer des incidents pour voir des données pour cette étape."
+msgstr "L'étape des tickets montre le temps nécessaire entre la création d'un ticket et son assignation à un jalon, ou son ajout à une liste d'un tableau de tickets. Commencez par créer des tickets pour voir des données pour cette étape."
+
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "Nombre d’échecs avant que GitLab n’empêche tout accès au stockage. Ce nombre d’échecs peut être réinitialisé dans l’interface d’administration : %{link_to_health_page} ou en suivant le %{api_documentation_link}."
msgid "The phase of the development lifecycle."
msgstr "Les étapes du cycle de développement."
msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
-msgstr "Les pipelines programmés exécutent des pipelines dans le futur, de façon répétée, pour les branches et tags spécifiées. Ces pipelines programmés héritent d’un accès partiel au projet basé sur l’utilisateur qui leurs est associé."
+msgstr "Les pipelines programmés exécutent des pipelines dans le futur, de façon répétée, pour les branches et tags spécifiées. Ces pipelines programmés héritent d’un accès partiel au projet basé sur l’utilisa·teur·trice qui leurs est associé·e."
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "L’étape de planification montre le temps entre l’étape précédente et l’envoi de votre première validation. Ce temps sera automatiquement ajouté quand vous pousserez votre première validation."
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
-msgstr "L’étape de mise en production montre le temps nécessaire entre la création d’un incident et le déploiement du code en production. Les données seront automatiquement ajoutées une fois que vous aurez complété le cycle complet, depuis l’idée jusqu’à la mise en production."
+msgstr "L’étape de mise en production montre le temps nécessaire entre la création d’un ticket et le déploiement du code en production. Les données seront automatiquement ajoutées une fois que vous aurez complété le cycle complet, depuis l’idée jusqu’à la mise en production."
msgid "The project can be accessed by any logged in user."
-msgstr "Votre projet peut être accédé par n’importe quel utilisateur authentifié"
+msgstr "Votre projet peut être accédé par n’importe quel utilisa·teur·trice authentifié·e."
msgid "The project can be accessed without any authentication."
msgstr "Votre projet peut être accédé sans aucune authentification."
@@ -1774,6 +2021,12 @@ msgstr "L’étape de pré-production indique le temps entre la fusion de la DF
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "L’étape de test montre le temps que le CI de GitLab met pour exécuter chaque pipeline liés à la demande de fusion. Les données seront automatiquement ajoutées après que votre premier pipeline s’achèvera."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "Délai en secondes pendant lequel GitLab gardera les informations d’échec. Si aucun échec ne survient pendant ce délai, les informations de ce montage seront réinitialisées."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "Temps en secondes pendant lequel GitLab essaiera d’accéder au stockage. Après ce délai, une erreur d’expiration d’attente sera déclenchée."
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "Le temps pris par chaque entrée récoltée durant cette étape."
@@ -1783,29 +2036,32 @@ msgstr "La valeur située au point médian d’une série de valeur observée. C
msgid "There are problems accessing Git storage: "
msgstr "Il y a des difficultés à accéder aux données Git : "
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Cette branche a changé depuis le début de l’édition. Souhaitez-vous créer une nouvelle branche ?"
+
msgid "This is a confidential issue."
-msgstr ""
+msgstr "Ce ticket est confidentiel."
msgid "This is the author's first Merge Request to this project."
-msgstr ""
+msgstr "C’est la première demande de fusion de cet auteur pour ce projet."
msgid "This issue is confidential and locked."
-msgstr ""
+msgstr "Ce ticket est confidentiel et verrouillé."
msgid "This issue is locked."
-msgstr ""
+msgstr "Ce ticket est verrouillé."
msgid "This means you can not push code until you create an empty repository or import existing one."
-msgstr "Cela signifie que vous ne pouvez pas pousser du code tant que vous ne créez pas un dépôt vide, ou importez une dépôt existant."
+msgstr "Cela signifie que vous ne pouvez pas pousser du code tant que vous n’avez pas créé un dépôt vide, ou que vous n’avez pas importé un dépôt existant."
msgid "This merge request is locked."
-msgstr ""
+msgstr "Cette demande de fusion est verrouillée."
msgid "Time before an issue gets scheduled"
-msgstr "Temps avant qu’un incident ne soit planifié"
+msgstr "Temps avant qu’un ticket ne soit planifié"
msgid "Time before an issue starts implementation"
-msgstr "Temps avant que résolution ne débute"
+msgstr "Temps avant que la résolution du ticket ne débute"
msgid "Time between merge request creation and merge/close"
msgstr "Temps entre la création d'une demande de fusion et sa fusion/clôture"
@@ -1814,7 +2070,7 @@ msgid "Time until first merge request"
msgstr "Temps jusqu’à la première demande de fusion"
msgid "Timeago|%s days ago"
-msgstr "Il y a %s jours"
+msgstr "il y a %s jours"
msgid "Timeago|%s days remaining"
msgstr "Il reste %s jours"
@@ -1844,7 +2100,7 @@ msgid "Timeago|%s weeks remaining"
msgstr "Il reste %s semaines"
msgid "Timeago|%s years ago"
-msgstr "Il y a %s ans"
+msgstr "il y a %s ans"
msgid "Timeago|%s years remaining"
msgstr "Il reste %s ans"
@@ -1931,7 +2187,7 @@ msgid "Timeago|in 1 year"
msgstr "Dans 1 an"
msgid "Timeago|in a while"
-msgstr ""
+msgstr "il y a longtemps"
msgid "Timeago|less than a minute ago"
msgstr "il y a moins d'une minute"
@@ -1956,7 +2212,7 @@ 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 ""
+msgstr "Suivre l’activité avec Contribution Analytics."
msgid "Unlock"
msgstr "Déverrouiller"
@@ -1965,22 +2221,25 @@ msgid "Unlocked"
msgstr "Déverrouillé"
msgid "Unstar"
-msgstr "Se désabonner"
+msgstr "Supprimer des favoris"
-msgid "Upgrade your plan to activate Advanced Global Search."
+msgid "Unsubscribe"
msgstr ""
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr "Mettez à jour votre abonnement pour activer la Recherche Globale Avancée."
+
msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr ""
+msgstr "Mettez à jour votre abonnement pour activer Contribution Analytics."
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr ""
+msgstr "Mettez à jour votre abonnement pour activer les Webhooks de groupe."
msgid "Upgrade your plan to activate Issue weight."
-msgstr ""
+msgstr "Mettez à niveau votre abonnement pour activer les poids de ticket."
msgid "Upgrade your plan to improve Issue boards."
-msgstr ""
+msgstr "Mettez à niveau votre abonnement pour améliorer les tableaux de tickets."
msgid "Upload New File"
msgstr "Téléverser un nouveau fichier"
@@ -2025,139 +2284,154 @@ msgid "We don't have enough data to show this stage."
msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape."
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
-msgstr ""
+msgstr "Les webhooks vous permettent d’appeler une URL si, par exemple, du nouveau code est poussé ou un nouveau ticket est créé. Vous pouvez configurer les webhooks pour écouter les événements spécifiques comme des poussées de code, des tickets ou des demandes de fusion. Les webhooks de groupes s’appliqueront à tous les projets dans un groupe, ce qui vous permet de normaliser la fonctionnalité du webhook dans votre groupe entier."
msgid "Weight"
msgstr "Poids"
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "Si l’accès à un stockage échoue, GitLab empêchera l’accès au stockage pendant la durée spécifiée ici. Cela permettra au système de fichiers de récupérer. Les dépôts présents sur les secteurs en erreur seront temporairement indisponibles."
+
msgid "Wiki"
msgstr "Wiki"
msgid "WikiClone|Clone your wiki"
-msgstr ""
+msgstr "Clonez votre wiki"
msgid "WikiClone|Git Access"
-msgstr ""
+msgstr "Accès git"
msgid "WikiClone|Install Gollum"
-msgstr ""
+msgstr "Installer Gollum"
msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
-msgstr ""
+msgstr "Il est recommandé d’installer %{markdown} pour que les spécificités de GFM s’affichent en local :"
msgid "WikiClone|Start Gollum and edit locally"
-msgstr ""
+msgstr "Démarrer Gollum et modifier localement"
msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr ""
+msgstr "Vous n’êtes pas autorisé·e à créer des pages wiki"
msgid "WikiHistoricalPage|This is an old version of this page."
-msgstr ""
+msgstr "Il s’agit d’une ancienne version de la page."
msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
-msgstr ""
+msgstr "Vous pouvez consulter le %{most_recent_link} ou parcourir l’%{history_link}."
msgid "WikiHistoricalPage|history"
-msgstr ""
+msgstr "historique"
msgid "WikiHistoricalPage|most recent version"
-msgstr ""
+msgstr "version la plus récente"
msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
-msgstr ""
+msgstr "D’autres exemples se trouvent dans la %{docs_link}"
msgid "WikiMarkdownDocs|documentation"
-msgstr ""
+msgstr "documentation"
msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
-msgstr ""
+msgstr "Pour créer un lien vers une (nouvelle) page, il suffit de saisir %{link_example}"
msgid "WikiNewPagePlaceholder|how-to-setup"
-msgstr ""
+msgstr "consignes-installation"
msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
-msgstr ""
+msgstr "Astuce : vous pouvez saisir le chemin d’accès complet du nouveau fichier. Nous créerons automatiquement les répertoires manquants."
msgid "WikiNewPageTitle|New Wiki Page"
-msgstr ""
+msgstr "Nouvelle Page Wiki"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
-msgstr ""
+msgstr "Êtes-vous sûr·e de vouloir supprimer cette page ?"
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
-msgstr ""
+msgstr "Quelqu’un a modifié la page en même temps que vous. Veuillez consulter %{page_link} et vérifiez que vos modifications ne suppriment pas involontairement les siennes."
msgid "WikiPageConflictMessage|the page"
-msgstr ""
+msgstr "la page"
msgid "WikiPageCreate|Create %{page_title}"
-msgstr ""
+msgstr "Créer %{page_title}"
msgid "WikiPageEdit|Update %{page_title}"
-msgstr ""
+msgstr "Mettre à jour %{page_title}"
msgid "WikiPage|Page slug"
-msgstr ""
+msgstr "Slug de la page"
msgid "WikiPage|Write your content or drag files here..."
-msgstr ""
+msgstr "Écrivez du contenu ou faîtes glisser des fichiers ici..."
msgid "Wiki|Create Page"
-msgstr ""
+msgstr "Créer une Page"
msgid "Wiki|Create page"
-msgstr ""
+msgstr "Créer la page"
msgid "Wiki|Edit Page"
-msgstr ""
+msgstr "Modifier cette Page"
msgid "Wiki|Empty page"
-msgstr ""
+msgstr "Page vide"
msgid "Wiki|More Pages"
-msgstr ""
+msgstr "Plus de Pages"
msgid "Wiki|New page"
-msgstr ""
+msgstr "Nouvelle page"
msgid "Wiki|Page history"
-msgstr ""
+msgstr "Historique de la page"
msgid "Wiki|Page version"
-msgstr ""
+msgstr "Version de la page"
msgid "Wiki|Pages"
-msgstr ""
+msgstr "Pages"
msgid "Wiki|Wiki Pages"
-msgstr ""
+msgstr "Pages du Wiki"
msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
-msgstr ""
+msgstr "Avec Contribution Analytics vous pouvez avoir un aperçu de l’activité des tickets, demandes de fusion et des événements de poussée de votre organisation et de ses membres."
msgid "Withdraw Access Request"
msgstr "Retirer la demande d'accès"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?"
+msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr·e ?"
msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "Vous êtes sur le point de supprimer %{project_name_with_namespace}. Les projets supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?"
+msgstr "Vous êtes sur le point de supprimer %{project_name_with_namespace}. Les projets supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr·e ?"
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr "Vous allez supprimer la relation de fourche avec le projet source %{forked_from_project}. Êtes-vous VRAIMENT sûr."
+msgstr "Vous allez supprimer la relation de fourche avec le projet source %{forked_from_project}. Êtes-vous ABSOLUMENT sûr·e ?"
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
-msgstr "Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire. Êtes vous VRAIMENT sûr ?"
+msgstr "Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire. Êtes vous ABSOLUMENT sûr·e ?"
+
+msgid "You are on a read-only GitLab instance."
+msgstr "Vous êtes sur une instance GitLab en lecture seule."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Vous êtes sur une instance GitLab en lecture seule. Si vous souhaitez apporter des modifications, vous devez aller sur %{link_to_primary_node}."
msgid "You can only add files when you are on a branch"
msgstr "Vous ne pouvez ajouter de fichier que dans une branche"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Vous ne pouvez pas écrire sur une instance GitLab Geo secondaire en lecture-seule. Veuillez utiliser le %{link_to_primary_node} à la place."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Vous ne pouvez pas écrire sur cette instance GitLab en lecture-seule."
+
msgid "You have reached your project limit"
msgstr "Vous avez atteint votre limite de projet"
msgid "You must sign in to star a project"
-msgstr "Vous devez vous identifier pour vous abonner à un projet"
+msgstr "Vous devez vous connecter pour mettre un projet en favori"
msgid "You need permission."
msgstr "Vous avez besoin d’une autorisation."
@@ -2184,7 +2458,10 @@ msgid "You won't be able to pull or push project code via SSH until you %{add_ss
msgstr "Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous n’aurez pas %{add_ssh_key_link} dans votre profil"
msgid "Your comment will not be visible to the public."
-msgstr ""
+msgstr "Votre commentaire ne sera pas visible publiquement."
+
+msgid "Your groups"
+msgstr "Vos groupes"
msgid "Your name"
msgstr "Votre nom"
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] "parent"
msgstr[1] "parents"
+msgid "password"
+msgstr "mot de passe"
+
+msgid "personal access token"
+msgstr "jeton d’accès personnel"
+
msgid "to help your contributors communicate effectively!"
msgstr "pour aider vos contributeurs à communiquer efficacement !"
-msgid "personal access token"
-msgstr ""
+msgid "username"
+msgstr "nom d’utilisateur"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 08f6212d997..32afb7b06e4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -399,7 +399,7 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -480,7 +480,7 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine."
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 5697c4e415c..4ac7676ca18 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:36-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -34,6 +34,11 @@ msgstr[1] "%s commit aggiuntivi sono stati omessi per evitare degradi di prestaz
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} ha committato %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -54,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -110,9 +121,18 @@ msgstr "Aggiungi una chiave SSH al tuo profilo per eseguire pull o push tramite
msgid "Add new directory"
msgstr "Aggiungi una directory (cartella)"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -128,6 +148,9 @@ msgstr "Sei sicuro di voler cancellare questa pipeline programmata?"
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -161,18 +184,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -235,6 +261,9 @@ msgstr[1] ""
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "La branch <strong>%{branch_name}</strong> è stata creata. Per impostare un rilascio automatico scegli un template CI di Gitlab e committa le tue modifiche %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Cerca branches"
@@ -445,15 +474,24 @@ msgstr "saltata"
msgid "CiStatus|running"
msgstr "in corso"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -496,27 +534,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -526,7 +570,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -538,18 +585,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -582,6 +623,11 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Commit Message"
msgstr ""
@@ -663,6 +709,12 @@ msgstr "Guida per contribuire"
msgid "Contributors"
msgstr "Collaboratori"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -684,9 +736,21 @@ msgstr "Crea cartella"
msgid "Create empty bare repository"
msgstr "Crea una repository vuota"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Crea una richiesta di merge"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Crea nuovo..."
@@ -773,6 +837,9 @@ msgstr "Nome cartella"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -845,12 +912,18 @@ msgstr "Ogni settimana (Di domenica alle 4 del mattino)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Impossibile cambiare owner"
msgid "Failed to remove the pipeline schedule"
msgstr "Impossibile rimuovere la pipeline pianificata"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -895,9 +968,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -940,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -984,6 +1108,12 @@ msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervallo di Pattern"
@@ -1052,6 +1182,9 @@ msgstr "Leggi di più su"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "documentazione sulla pianificazione delle pipelines"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "Abbandona il gruppo"
@@ -1075,6 +1208,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
@@ -1105,6 +1244,9 @@ msgstr ""
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nuovo Issue"
@@ -1122,18 +1264,27 @@ msgstr "Nuova directory"
msgid "New file"
msgstr "Nuovo file"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Nuovo Issue"
msgid "New merge request"
msgstr "Nuova richiesta di merge"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Nuova pianficazione"
msgid "New snippet"
msgstr "Nuovo snippet"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Nuovo tag"
@@ -1212,6 +1363,12 @@ msgstr "Osserva"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtra"
@@ -1353,9 +1510,54 @@ msgstr "con più stadi"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Il Progetto '%{project_name}' in coda di eliminazione."
@@ -1365,9 +1567,6 @@ msgstr "Il Progetto '%{project_name}' è stato creato con successo."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Il Progetto '%{project_name}' è stato aggiornato con successo."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Il Progetto '%{project_name}' verrà eliminato"
-
msgid "Project access must be granted explicitly to each user."
msgstr "L'accesso al progetto dev'esser fornito esplicitamente ad ogni utente"
@@ -1425,6 +1624,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1446,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Vedi altro"
@@ -1515,6 +1729,9 @@ msgstr "Ripristina questa richiesta di merge"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1533,6 +1750,15 @@ msgstr "Pianificazione pipelines"
msgid "Search branches and tags"
msgstr "Ricerca branches e tags"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "Seleziona formato d'archivio"
@@ -1580,13 +1806,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1706,6 +1935,12 @@ msgstr "inizia una %{new_merge_request} con queste modifiche"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Cambia branch/tag"
@@ -1732,6 +1967,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "Lo stadio di programmazione mostra il tempo trascorso dal primo commit alla creazione di una richiesta di merge (MR). I dati saranno aggiunti una volta che avrai creato la prima richiesta di merge."
@@ -1744,6 +1982,15 @@ msgstr "La relazione del fork è stata rimossa"
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "Lo stadio di Issue mostra il tempo che impiega un issue ad esser correlato ad una Milestone, o ad esser aggiunto ad una tua Lavagna. Inizia la creazione di problemi per visualizzare i dati in questo stadio."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "Il ciclo vitale della fase di sviluppo."
@@ -1774,6 +2021,12 @@ msgstr "Lo stadio di pre-rilascio mostra il tempo che trascorre da una MR (Richi
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "Lo stadio di test mostra il tempo che ogni Pipeline impiega per essere eseguita in ogni Richiesta di Merge correlata. L'informazione sarà disponibile automaticamente quando la tua prima Pipeline avrà finito d'esser eseguita."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "Il tempo aggregato relativo eventi/data entry raccolto in quello stadio."
@@ -1783,6 +2036,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 branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1967,6 +2223,9 @@ msgstr ""
msgid "Unstar"
msgstr ""
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2030,6 +2289,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2150,9 +2412,21 @@ msgstr "Stai per rimuovere la relazione con il progetto sorgente %{forked_from_p
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Stai per trasferire %{project_name_with_namespace} ad un altro owner. Sei ASSOLUTAMENTE sicuro?"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "Puoi aggiungere files solo quando sei in una branch"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Hai raggiunto il tuo limite di progetto"
@@ -2186,6 +2460,9 @@ msgstr "Non sarai in grado di effettuare push o pull tramite SSH fino a che %{ad
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Il tuo nome"
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] ""
msgstr[1] ""
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 4b70f8dd9df..9045ae26c4a 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:37-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -31,6 +31,10 @@ msgstr[0] "パフォーマンス低下をé¿ã‘ã‚‹ãŸã‚ %s 個ã®ã‚³ãƒŸãƒƒãƒˆã‚
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_timeago}ã«%{commit_author_link}ãŒã‚³ãƒŸãƒƒãƒˆã—ã¾ã—ãŸã€‚"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -50,6 +54,12 @@ msgstr[0] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 個ã®ãƒ‘イプライン"
@@ -105,9 +115,18 @@ msgstr "SSHã§ãƒ—ルやプッシュã™ã‚‹å ´åˆã¯ã€ãƒ—ロフィールã«SSHéµ
msgid "Add new directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’追加"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -123,6 +142,9 @@ msgstr "ã“ã®ãƒ‘イプラインスケジュールを削除ã—ã¾ã™ã‹ï¼Ÿ"
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -156,18 +178,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -229,6 +254,9 @@ msgstr[0] "ブランãƒ"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "<strong>%{branch_name}</strong> ブランãƒãŒä½œæˆã•ã‚Œã¾ã—ãŸã€‚自動デプロイを設定ã™ã‚‹ã«ã¯ã€GitLab CI Yaml テンプレートをé¸æŠžã—ã¦ã€å¤‰æ›´ã‚’コミットã—ã¦ãã ã•ã„。 %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ブランãƒã‚’検索"
@@ -439,15 +467,24 @@ msgstr "スキップ済ã¿"
msgid "CiStatus|running"
msgstr "実行中"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -490,27 +527,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -520,7 +563,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -532,18 +578,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -575,6 +615,10 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "コミット"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
msgid "Commit Message"
msgstr ""
@@ -656,6 +700,12 @@ msgstr "貢献者å‘ã‘ガイド"
msgid "Contributors"
msgstr "貢献者"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -677,9 +727,21 @@ msgstr "ディレクトリを作æˆ"
msgid "Create empty bare repository"
msgstr "空ã®bareレãƒã‚¸ãƒˆãƒªãƒ¼ã‚’作æˆ"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "マージリクエストを作æˆ"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "æ–°è¦ä½œæˆ"
@@ -765,6 +827,9 @@ msgstr "ディレクトリå"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -837,12 +902,18 @@ msgstr "毎週 (日曜日ã®åˆå‰4:00)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "オーナーを変更ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Failed to remove the pipeline schedule"
msgstr "パイプラインスケジュールを削除ã§ãã¾ã›ã‚“ã§ã—ãŸ"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "ファイル"
@@ -886,9 +957,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -931,6 +1008,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -974,6 +1096,12 @@ msgid "Instance"
msgid_plural "Instances"
msgstr[0] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "é–“éš”ã®ãƒ‘ターン"
@@ -1041,6 +1169,9 @@ msgstr "詳ã—ã見る:"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "詳ã—ãã¯ãƒ‘イプラインスケジュールã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’å‚ç…§"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "グループを離脱"
@@ -1063,6 +1194,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "中央値"
@@ -1093,6 +1230,9 @@ msgstr ""
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "æ–°è¦èª²é¡Œ"
@@ -1109,18 +1249,27 @@ msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª"
msgid "New file"
msgstr "æ–°è¦ãƒ•ã‚¡ã‚¤ãƒ«"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "æ–°è¦èª²é¡Œ"
msgid "New merge request"
msgstr "æ–°è¦ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "æ–°è¦ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«"
msgid "New snippet"
msgstr "æ–°è¦ã‚¹ãƒ‹ãƒšãƒƒãƒˆ"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "æ–°è¦ã‚¿ã‚°"
@@ -1199,6 +1348,12 @@ msgstr "ã™ã¹ã¦é€šçŸ¥"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "フィルター"
@@ -1340,9 +1495,54 @@ msgstr "ステージã‚ã‚Š"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}' プロジェクトã¯å‰Šé™¤å‡¦ç†å¾…ã¡ã§ã™ã€‚"
@@ -1352,9 +1552,6 @@ msgstr "'%{project_name}' プロジェクトã¯æ­£å¸¸ã«ä½œæˆã•ã‚Œã¾ã—ãŸã€‚
msgid "Project '%{project_name}' was successfully updated."
msgstr "'%{project_name}' プロジェクトã¯æ­£å¸¸ã«æ›´æ–°ã•ã‚Œã¾ã—ãŸã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "'%{project_name}' プロジェクトã¯å‰Šé™¤ã•ã‚Œã¾ã™ã€‚"
-
msgid "Project access must be granted explicitly to each user."
msgstr "ユーザーã”ã¨ã«ãƒ—ロジェクトアクセスã®æ¨©é™ã‚’指定ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
@@ -1412,6 +1609,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1433,12 +1636,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "続ãを読む"
@@ -1502,6 +1714,9 @@ msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’リãƒãƒ¼ãƒˆ"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1520,6 +1735,15 @@ msgstr "パイプラインスケジューリング"
msgid "Search branches and tags"
msgstr "ブランãƒã¾ãŸã¯ã‚¿ã‚°ã‚’検索"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "アーカイブã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’é¸æŠž"
@@ -1566,13 +1790,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1692,6 +1919,12 @@ msgstr "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
@@ -1717,6 +1950,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "コーディングステージã§ã¯ã€æœ€åˆã®ã‚³ãƒŸãƒƒãƒˆã‹ã‚‰ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•ã‚Œã‚‹ã¾ã§ã®æ™‚é–“ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•ã‚ŒãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚"
@@ -1729,6 +1965,15 @@ msgstr "フォークã®ãƒªãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚"
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "課題ステージã§ã¯ã€èª²é¡ŒãŒç™»éŒ²ã•ã‚Œã¦ã‹ã‚‰ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã«å‰²ã‚Šå½“ã¦ã‚‰ã‚Œã‚‹ã‹ã€èª²é¡Œãƒœãƒ¼ãƒ‰ã®ãƒªã‚¹ãƒˆã«è¿½åŠ ã•ã‚Œã‚‹ã¾ã§ã®æ™‚é–“ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®ãƒªã‚¹ãƒˆã«è¡¨ç¤ºã™ã‚‹ã«ã¯èª²é¡Œã‚’最åˆã«ä½œæˆã—ã¦ãã ã•ã„。"
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "開発ライフサイクルã®æ®µéšŽ"
@@ -1759,6 +2004,12 @@ msgstr "ステージングステージã§ã¯ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒãƒžãƒ¼
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "テスティングステージã§ã¯ã€GitLab CI ãŒé–¢é€£ã™ã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å„パイプラインを実行ã™ã‚‹æ™‚é–“ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒ‘イプラインãŒå®Œäº†ã—ãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚"
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¸ã«åŽé›†ã•ã‚ŒãŸãƒ‡ãƒ¼ã‚¿æ¯Žã®æ™‚é–“"
@@ -1768,6 +2019,9 @@ msgstr "得られãŸä¸€é€£ã®ãƒ‡ãƒ¼ã‚¿ã‚’å°ã•ã„é †ã«ä¸¦ã¹ãŸã¨ãã«ä¸­å¤®
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1950,6 +2204,9 @@ msgstr ""
msgid "Unstar"
msgstr "スターを外ã™"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2013,6 +2270,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2133,9 +2393,21 @@ msgstr "å…ƒã®ãƒ—ロジェクト (%{forked_from_project}) ã¨ã®ãƒªãƒ¬ãƒ¼ã‚·ãƒ§ã
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "%{project_name_with_namespace} プロジェクトを別ã®ã‚ªãƒ¼ãƒŠãƒ¼ã«ç§»è­²ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "ファイルを追加ã™ã‚‹ã«ã¯ã€ã©ã“ã‹ã®ãƒ–ランãƒã«ã„ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "プロジェクト数ã®ä¸Šé™ã«é”ã—ã¦ã„ã¾ã™"
@@ -2169,6 +2441,9 @@ msgstr "%{add_ssh_key_link} をプロファイルã«è¿½åŠ ã—ã¦ã„ãªã„ã®ã§ã
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "åå‰"
@@ -2192,9 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "親"
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 5a6c8ef9c7a..ab74d4cbeae 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:37-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -31,6 +31,10 @@ msgstr[0] "%s 추가 ì»¤ë°‹ì€ ì„±ëŠ¥ ì´ìŠˆë¥¼ 방지하기 위해 ìƒëžµë˜ì—ˆ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_timeago} ì— %{commit_author_link} ë‹˜ì´ ì»¤ë°‹í•˜ì˜€ìŠµë‹ˆë‹¤. "
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -50,6 +54,12 @@ msgstr[0] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr "설치 ë°©ë²•ì— ëŒ€í•œ 정보를 얻기 위해 %{link} 를 ì²´í¬ì•„웃하세요."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 파ì´í”„ë¼ì¸"
@@ -105,9 +115,18 @@ msgstr "í”„ë¡œí•„ì— SSH 키를 추가하여 SSH를 통해 Pull 하거나 Pushí•
msgid "Add new directory"
msgstr "새 디렉토리 추가"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "ì „ì²´"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -123,6 +142,9 @@ msgstr "ì´ íŒŒì´í”„ë¼ì¸ ìŠ¤ì¼€ì¥´ì„ ì‚­ì œ 하시겠습니까?"
msgid "Are you sure you want to discard your changes?"
msgstr "변경 ë‚´ìš©ì„ ì·¨ì†Œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "ë“±ë¡ í† í°ì„ 초기화 하시겠습니까?"
@@ -156,18 +178,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -229,6 +254,9 @@ msgstr[0] "브랜치"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "<strong>%{branch_name}</strong> 브랜치가 ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. ìžë™ ë°°í¬ë¥¼ 설정하려면 GitLab CI Yaml í…œí”Œë¦¿ì„ ì„ íƒí•˜ê³  변경 ì‚¬í•­ì„ ì ìš©í•˜ì‹­ì‹œì˜¤. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "브랜치 검색"
@@ -439,15 +467,24 @@ msgstr "건너 뜀"
msgid "CiStatus|running"
msgstr "실행 중"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -490,27 +527,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -520,7 +563,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -532,18 +578,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -575,6 +615,10 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "커밋"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
msgid "Commit Message"
msgstr ""
@@ -656,6 +700,12 @@ msgstr "ê¸°ì—¬ì— ëŒ€í•œ 안내"
msgid "Contributors"
msgstr "기여해 주신 분들"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -677,9 +727,21 @@ msgstr "디렉토리 만들기"
msgid "Create empty bare repository"
msgstr "빈 bare 저장소 만들기"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "머지 리퀘스트 만들기"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "새로 만들기 ..."
@@ -765,6 +827,9 @@ msgstr "디렉토리 ì´ë¦„"
msgid "Discard changes"
msgstr "변경 내용 취소"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -837,12 +902,18 @@ msgstr "매주 (ì¼ìš”ì¼ ì˜¤ì „ 4ì‹œì—)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "소유ìžë¥¼ 변경하지 못했습니다"
msgid "Failed to remove the pipeline schedule"
msgstr "파ì´í”„ë¼ì¸ ìŠ¤ì¼€ì¤„ì„ ì œê±°í•˜ì§€ 못했습니다."
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "파ì¼"
@@ -886,9 +957,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -931,6 +1008,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr "헬스 ì²´í¬"
@@ -974,6 +1096,12 @@ msgid "Instance"
msgid_plural "Instances"
msgstr[0] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "주기 패턴"
@@ -1041,6 +1169,9 @@ msgstr "ë” ìžì„¸ížˆ 알아보기"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "파ì´í”„ë¼ì¸ 스케쥴 문서로부터 ë” ì•Œì•„ë³´ê¸°"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "그룹 떠나기"
@@ -1063,6 +1194,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "중앙값"
@@ -1093,6 +1230,9 @@ msgstr "여기"
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "새 ì´ìŠˆ"
@@ -1109,18 +1249,27 @@ msgstr "새 디렉토리"
msgid "New file"
msgstr "새 파ì¼"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "새 ì´ìŠˆ"
msgid "New merge request"
msgstr "새 머지 리퀘스트"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "새 ì¼ì •"
msgid "New snippet"
msgstr "새 스니펫"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "새 태그 "
@@ -1199,6 +1348,12 @@ msgstr "Watch"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "í•„í„°"
@@ -1340,9 +1495,54 @@ msgstr "스테ì´ì§•"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}'프로ì íŠ¸ê°€ ì‚­ì œ 처리 중입니다."
@@ -1352,9 +1552,6 @@ msgstr "'%{project_name}'프로ì íŠ¸ê°€ 성공ì ìœ¼ë¡œ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤."
msgid "Project '%{project_name}' was successfully updated."
msgstr "'%{project_name}'프로ì íŠ¸ê°€ 성공ì ìœ¼ë¡œ ì—…ë°ì´íŠ¸ë˜ì—ˆìŠµë‹ˆë‹¤."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "'%{project_name}'프로ì íŠ¸ê°€ ì‚­ì œë©ë‹ˆë‹¤."
-
msgid "Project access must be granted explicitly to each user."
msgstr "프로ì íŠ¸ 액세스는 ê° ì‚¬ìš©ìžì—게 명시ì ìœ¼ë¡œ 부여ë˜ì–´ì•¼í•©ë‹ˆë‹¤."
@@ -1412,6 +1609,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1433,12 +1636,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr "푸쉬 ì´ë²¤íŠ¸"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "ë” ì½ê¸°"
@@ -1502,6 +1714,9 @@ msgstr "ì´ ë¨¸ì§€ 리퀘스트 ë˜ëŒë¦¬ê¸°"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1520,6 +1735,15 @@ msgstr "파ì´í”„ë¼ì¸ 스케줄ë§"
msgid "Search branches and tags"
msgstr "브랜치 ë° íƒœê·¸ 검색"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "ì•„ì¹´ì´ë¸Œ í¬ë§· ì„ íƒ"
@@ -1566,13 +1790,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1692,6 +1919,12 @@ msgstr "ì´ ë³€ê²½ 사항으로 %{new_merge_request} ì„ ì‹œìž‘í•˜ì‹­ì‹œì˜¤."
msgid "Start the Runner!"
msgstr "Runner 시작!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "스위치 브랜치/태그"
@@ -1717,6 +1950,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "Coding Stage는 첫 번째 커밋ì—서부터 머지 리퀘스트 ìƒì„±ê¹Œì§€ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. 첫 번째 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ìƒì„±í•˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ ì—¬ê¸°ì— ì¶”ê°€ë©ë‹ˆë‹¤."
@@ -1729,6 +1965,15 @@ msgstr "í¬í¬ 관계가 제거ë˜ì—ˆìŠµë‹ˆë‹¤."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "ì´ìŠˆ 단계ì—는 ì´ìŠˆë¥¼ 작성하여 마ì¼ìŠ¤í†¤ìœ¼ë¡œ 지정하는 ë° ê±¸ë¦¬ëŠ” 시간 ë˜ëŠ” ì´ìŠˆ ë³´ë“œì˜ ëª©ë¡ì— ì´ìŠˆë¥¼ 추가하는 ì‹œê°„ì´ í‘œì‹œë©ë‹ˆë‹¤. ì´ ë‹¨ê³„ì˜ ë°ì´í„°ë¥¼ 보기 위해서는 ì´ìŠˆë¥¼ 먼저 작성해야 합니다."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "개발 ìˆ˜ëª…ì£¼ê¸°ì˜ ë‹¨ê³„."
@@ -1759,6 +2004,12 @@ msgstr "Staging 단계ì—서는 MR 머지과 프로ë•ì…˜ í™˜ê²½ì— ì½”ë“œ ë°°í
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "테스트 단계ì—서는 GitLab CIê°€ 관련 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ìœ„í•´ 모든 파ì´í”„ë¼ì¸ì„ 실행하는 ë° ê±¸ë¦¬ëŠ” ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. 첫 번째 파ì´í”„ë¼ì¸ ì‹¤í–‰ì´ ì™„ë£Œë˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "해당 단계ì—ì„œ 수집 í•œ ê° ë°ì´í„° ìž…ë ¥ì— ì†Œìš” ëœ ì‹œê°„"
@@ -1768,6 +2019,9 @@ msgstr "ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— 있습니다. 예를 들어, 3, 5,
msgid "There are problems accessing Git storage: "
msgstr "git storageì— ì ‘ê·¼í•˜ëŠ”ë° ë¬¸ì œê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. "
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1950,6 +2204,9 @@ msgstr ""
msgid "Unstar"
msgstr "별표 제거"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2013,6 +2270,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2133,9 +2393,21 @@ msgstr "í¬í¬ 관계를 소스 프로ì íŠ¸ %{forked_from_project}ì— ëŒ€í•´ ì 
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "%{project_name_with_namespace}ì„ ë‹¤ë¥¸ 소유ìžì—게 ì´ì „하려고합니다. \"ì •ë§ë¡œ\" 확실합니까?"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "ë¸Œëžœì¹˜ì— ìžˆì„ ë•Œì—만 파ì¼ì„ 추가 í•  수 있습니다."
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "프로ì íŠ¸ ìˆ«ìž í•œë„ì— ë„달했습니다."
@@ -2169,6 +2441,9 @@ msgstr "ë‹¹ì‹ ì˜ í”„ë¡œí•„ì— %{add_ssh_key_link} 를 하기 ì „ì—는 SSH를 í
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "ê·€í•˜ì˜ ì´ë¦„"
@@ -2192,9 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "부모"
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index fc7bbc79899..7e33af9f747 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:37-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -34,6 +34,11 @@ msgstr[1] "%s andere commits zijn weggelaten om prestatieproblemen te voorkomen.
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr ""
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -54,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(bekijk de %{link} voor meer info over hoe je het kan installeren)."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -110,9 +121,18 @@ msgstr ""
msgid "Add new directory"
msgstr "Nieuwe map toevoegen"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "Alles"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr "Uiterlijk"
@@ -128,6 +148,9 @@ msgstr ""
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -161,18 +184,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr "Facturatie"
@@ -235,6 +261,9 @@ msgstr[1] ""
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "BranchSwitcherPlaceholder|Zoek branches"
@@ -445,15 +474,24 @@ msgstr "overgeslagen"
msgid "CiStatus|running"
msgstr ""
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -496,27 +534,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -526,7 +570,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -538,18 +585,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -582,6 +623,11 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Commit Message"
msgstr ""
@@ -663,6 +709,12 @@ msgstr ""
msgid "Contributors"
msgstr ""
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -684,9 +736,21 @@ msgstr "Maak map aan"
msgid "Create empty bare repository"
msgstr ""
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr ""
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr ""
@@ -773,6 +837,9 @@ msgstr ""
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -845,12 +912,18 @@ msgstr ""
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -895,9 +968,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -940,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -984,6 +1108,12 @@ msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr ""
@@ -1052,6 +1182,9 @@ msgstr ""
msgid "Learn more in the|pipeline schedules documentation"
msgstr ""
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr ""
@@ -1075,6 +1208,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr ""
@@ -1105,6 +1244,9 @@ msgstr ""
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nieuwe issue"
@@ -1122,18 +1264,27 @@ msgstr ""
msgid "New file"
msgstr ""
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr ""
msgid "New snippet"
msgstr ""
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr ""
@@ -1212,6 +1363,12 @@ msgstr ""
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr ""
@@ -1353,9 +1510,54 @@ msgstr ""
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
@@ -1365,9 +1567,6 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
-msgid "Project '%{project_name}' will be deleted."
-msgstr ""
-
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -1425,6 +1624,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1446,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -1515,6 +1729,9 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1533,6 +1750,15 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr ""
@@ -1580,13 +1806,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1706,6 +1935,12 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -1732,6 +1967,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr ""
@@ -1744,6 +1982,15 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr ""
@@ -1774,6 +2021,12 @@ msgstr ""
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr ""
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr ""
@@ -1783,6 +2036,9 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1967,6 +2223,9 @@ msgstr ""
msgid "Unstar"
msgstr ""
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2030,6 +2289,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2150,9 +2412,21 @@ msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr ""
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
@@ -2186,6 +2460,9 @@ msgstr ""
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr ""
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] ""
msgstr[1] ""
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index e6ba0c8cf9a..473086886ac 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:37-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-04 13:27-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -18,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 camada"
+msgstr[1] "%d camadas"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -34,36 +34,47 @@ msgstr[1] "%s commits adicionais foram omitidos para prevenir problemas de perfo
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} fez commit %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] "%{count} participante"
+msgstr[1] "%{count} participantes"
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "%{number_commits_behind} commits atrás de %{default_branch}, %{number_commits_ahead} commits à frente"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
-msgstr ""
+msgstr "%{number_of_failures} de %{maximum_failures} falhas. O GitLab permitirá o acesso na próxima tentativa."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
-msgstr ""
+msgstr "%{number_of_failures} de %{maximum_failures} falhas. O GitLab irá bloquear o acesso por %{number_of_seconds} segundos."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "%{number_of_failures} de %{maximum_failures} falhas. O GitLab não tentará mais automaticamente. Redefina as informações de storage quando o problema for resolvido."
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%{storage_name}: falha na tentativa de acesso ao storage no host:"
+msgstr[1] "%{storage_name}: %{failed_attempts} falhas de acesso ao storage:"
msgid "(checkout the %{link} for information on how to install it)."
-msgstr ""
+msgstr "(veja o %{link} para informações de como instalar)."
+
+msgid "+ %{moreCount} more"
+msgstr "%{moreCount} mais"
+
+msgid "- show less"
+msgstr "- exibir menos"
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "1 pipeline"
+msgstr[1] "%d pipelines"
msgid "1st contribution!"
-msgstr ""
+msgstr "1ª contribuição!"
msgid "2FA enabled"
-msgstr ""
+msgstr "Autenticação de 2 passos ativada"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Uma coleção de gráficos sobre Integração Contínua"
@@ -72,16 +83,16 @@ msgid "About auto deploy"
msgstr "Sobre o deploy automático"
msgid "Abuse Reports"
-msgstr ""
+msgstr "Relatórios de abuso"
msgid "Access Tokens"
-msgstr ""
+msgstr "Tokens de acesso"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
-msgstr ""
+msgstr "Os acessos à storages com defeito foram temporariamente desabilitados para permitir a sua remontagem. Redefina as informações de armazenamento depois que o problema foi resolvido para permitir o acesso de novo."
msgid "Account"
-msgstr ""
+msgstr "Conta"
msgid "Active"
msgstr "Ativo"
@@ -90,7 +101,7 @@ msgid "Activity"
msgstr "Atividade"
msgid "Add"
-msgstr ""
+msgstr "Adicionar"
msgid "Add Changelog"
msgstr "Adicionar registro de mudanças"
@@ -99,7 +110,7 @@ msgid "Add Contribution guide"
msgstr "Adicionar Guia de contribuição"
msgid "Add Group Webhooks and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Adicione o grupo de Webhooks e GitLab Enterprise Edition."
msgid "Add License"
msgstr "Adicionar Licença"
@@ -110,14 +121,23 @@ msgstr "Adicionar chave SSH ao seu perfil para fazer pull ou push via SSH."
msgid "Add new directory"
msgstr "Adicionar novo diretório"
+msgid "AdminHealthPageLink|health page"
+msgstr "página de saúde"
+
+msgid "Advanced settings"
+msgstr "Configurações avançadas"
+
msgid "All"
-msgstr ""
+msgstr "Todos"
+
+msgid "An error occurred. Please try again."
+msgstr "Ocorreu um erro. Tente novamente."
msgid "Appearance"
-msgstr ""
+msgstr "Aparência"
msgid "Applications"
-msgstr ""
+msgstr "Aplicações"
msgid "Archived project! Repository is read-only"
msgstr "Projeto arquivado! O repositório é somente leitura"
@@ -126,115 +146,124 @@ msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Tem certeza que deseja excluir este agendamento de pipeline?"
msgid "Are you sure you want to discard your changes?"
-msgstr ""
+msgstr "Você tem certeza que deseja descartar suas alterações?"
+
+msgid "Are you sure you want to leave this group?"
+msgstr "Tem certeza que quer sair desse grupo?"
msgid "Are you sure you want to reset registration token?"
-msgstr ""
+msgstr "Você tem certeza que quer recriar o token de registro?"
msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgstr "Você tem certeza que quer reiniciar o token de status de saúde?"
msgid "Are you sure?"
-msgstr ""
+msgstr "Você tem certeza?"
msgid "Artifacts"
-msgstr ""
+msgstr "Artefatos"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Para anexar arquivo, arraste e solte ou %{upload_link}"
msgid "Authentication Log"
-msgstr ""
+msgstr "Log de autenticação"
msgid "Author"
-msgstr ""
+msgstr "Autor"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Apps de revisão automática e Auto Deploy precisam de um nome de domínio e o %{kubernetes} para que funcione corretamente."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "Apps de revisão automática e Auto Deploy precisam de um nome de domínio para que funcione corretamente."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Apps de revisão automática e Auto Deploy precisam do %{kubernetes} para que funcione corretamente."
msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr ""
-
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
+msgstr "Auto DevOps (Beta)"
msgid "AutoDevOps|Auto DevOps documentation"
-msgstr ""
+msgstr "Documentação de auto DevOps"
msgid "AutoDevOps|Enable in settings"
-msgstr ""
+msgstr "Habilitar nas configurações"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "Ele gerará a build, testará e fará deploy de sua aplicação automaticamente com base em uma configuração predefinida do CI/CD."
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
-msgstr ""
+msgstr "Saiba mais em %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Você pode ativar %{link_to_settings} para esse projeto."
msgid "Billing"
-msgstr ""
+msgstr "Cobrança"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Grupo %{group_name} está atualmente no plano %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "Downgrade e upgrade automático para alguns planos ainda não está disponível."
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "Plano atual"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "Suporte ao cliente"
msgid "BillingPlans|Downgrade"
-msgstr ""
+msgstr "Downgrade"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "Aprenda mais sobre cada plano lendo nossa %{faq_link}."
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "Gerenciar plano"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "Por favor contacte o %{customer_support_link} para resolver seu caso."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "Veja todas as funcionalidades do seu plano %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "Esse grupo utiliza o plano associado ao seu grupo pai."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "Para gerenciar o plano para esse grupo, visite a sessão de cobrança de %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "Atualizar"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Vocês está utilizando o plano %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "perguntas frequentes"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "mensalmente"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "pago %{price_per_year} anualmente"
msgid "BillingPlans|per user"
-msgstr ""
+msgstr "por usuário"
msgid "Branch"
msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Branch"
+msgstr[1] "Branches"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "O branch <strong>%{branch_name}</strong> foi criado. Para configurar o deploy automático, selecione um modelo de Yaml do GitLab CI e commit suas mudanças. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr "Branch foi alterado"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Procurar por branches"
@@ -242,91 +271,91 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Mudar de branch"
msgid "Branches"
-msgstr ""
+msgstr "Branches"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "Não foi possível encontrar o commit HEAD para essa branch"
msgid "Branches|Compare"
-msgstr ""
+msgstr "Comparar"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "Apagar todas as branches que foi feito merge em '%{default_branch}'"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "Apagar branch"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "Apagar branches que foram feito merge"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "Apagar branch protegida"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "Apagar branch protegida '%{branch_name}'?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Apagar a branch '%{branch_name}' não pode ser desfeito. Você tem certeza?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Apagar branches que foram feito merge não pode ser desfeito. Você tem certeza?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "Filtrar por nome de branch"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "Merge feito na branch '%{default_branch}'"
msgid "Branches|New branch"
-msgstr ""
+msgstr "Nova branch"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "Nenhuma branch para mostrar"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+msgstr "Uma vez que você confirmar e pressionar %{delete_protected_branch}, não pode ser desfeito ou recuperado."
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "Somente alguém master ou dono do projeto poderá apagar branches protegidas"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "Ramos protegidos podem ser gerenciados em %{project_settings_link}"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "Ordernar por"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "A branch não pode ser atualizada automaticamente porque diverge do seu upstream."
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+msgstr "A branch padrão não pode ser apagada"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "Essa branch não teve o merge realizado na '%{default_branch}'."
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "Para evitar perda de dados, considere fazer um merge dessa branch antes de apagá-la."
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+msgstr "Para confirmar, digite %{branch_name_confirmation}:"
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "Para descartar as mudanças locais e sobrescrever a branch com a versão de upstream, apague-o aqui e escolha 'Atualizar agora', acima."
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "Você irá apagar irreparavelmente a branch protegida '%{branch_name}'."
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "divergiu do upstream"
msgid "Branches|merged"
-msgstr ""
+msgstr "merge realizado"
msgid "Branches|project settings"
-msgstr ""
+msgstr "configurações do projeto"
msgid "Branches|protected"
-msgstr ""
+msgstr "protegido"
msgid "Browse Directory"
msgstr "Navegar no Diretório"
@@ -344,22 +373,22 @@ msgid "ByAuthor|by"
msgstr "por"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "Configuração da IC"
msgid "CICD|Jobs"
-msgstr ""
+msgstr "Jobs"
msgid "Cancel"
msgstr "Cancelar"
msgid "Cancel edit"
-msgstr ""
+msgstr "Cancelar edição"
msgid "Change Weight"
-msgstr ""
+msgstr "Mudar peso"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Pick para um branch"
@@ -380,7 +409,7 @@ msgid "Charts"
msgstr "Gráficos"
msgid "Chat"
-msgstr ""
+msgstr "Bate-papo"
msgid "Cherry-pick this commit"
msgstr "Cherry-pick esse commit"
@@ -389,7 +418,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 ""
+msgstr "Escolha quais os grupos que você deseja replicar para este nó secundário. Deixe em branco para replicar todos."
msgid "CiStatusLabel|canceled"
msgstr "cancelado"
@@ -445,145 +474,162 @@ msgstr "ignorado"
msgid "CiStatus|running"
msgstr "executando"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "interruptor da api"
+
msgid "Clone repository"
-msgstr ""
+msgstr "Clonar repositório"
msgid "Close"
-msgstr ""
+msgstr "Fechar"
+
+msgid "Cluster"
+msgstr "Cluster"
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr ""
+msgstr "Um %{link_to_container_project} deve ter sido criado com essa conta"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Detalhes do cluster"
msgid "ClusterIntegration|Cluster integration"
-msgstr ""
+msgstr "Integração do cluster"
msgid "ClusterIntegration|Cluster integration is disabled for this project."
-msgstr ""
+msgstr "Integração do cluster está desabilitada para esse projeto."
msgid "ClusterIntegration|Cluster integration is enabled for this project."
-msgstr ""
+msgstr "Integração do cluster está ativada nesse projeto."
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
-msgstr ""
+msgstr "Integração do cluster está ativada para esse projeto. Desabilitar a integração não afetará seu cluster, mas desligará temporariamente a conexão do Gitlab com ele."
msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr ""
+msgstr "O cluster está sendo criado no Google Container Engine..."
msgid "ClusterIntegration|Cluster name"
-msgstr ""
+msgstr "Nome do cluster"
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr ""
+msgstr "O cluster foi criado com sucesso no Google Container Engine"
msgid "ClusterIntegration|Copy cluster name"
-msgstr ""
+msgstr "Copiar nome do cluster"
msgid "ClusterIntegration|Create cluster"
-msgstr ""
+msgstr "Criar cluster"
msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr ""
+msgstr "Criar novo cluster no Google Container Engine"
msgid "ClusterIntegration|Enable cluster integration"
-msgstr ""
+msgstr "Ativar integração com o cluster"
msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr ""
+msgstr "ID do projeto no Google Cloud Platform"
msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Google Container Engine"
msgid "ClusterIntegration|Google Container Engine project"
-msgstr ""
-
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Projeto no Google Container Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr ""
+msgstr "Leia mais sobre %{link_to_documentation}"
-msgid "ClusterIntegration|See machine types"
-msgstr ""
+msgid "ClusterIntegration|Machine type"
+msgstr "Tipo de máquina"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
-msgstr ""
+msgstr "Confira se sua conta %{link_to_requirements} para criar clusters"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Gerenciar integração de cluster com o projeto no GitLab"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "Gerencie seu cluster visitando %{link_gke}"
msgid "ClusterIntegration|Number of nodes"
-msgstr ""
+msgstr "Número de nós"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "Por favor, tenha certeza que sua conta no Google cumpre com os requisitos:"
msgid "ClusterIntegration|Project namespace (optional, unique)"
-msgstr ""
+msgstr "Namespace do projeto (opcional, único)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Ler nossa %{link_to_help_page} na integração com cluster."
msgid "ClusterIntegration|Remove cluster integration"
-msgstr ""
+msgstr "Remover integração com cluster"
msgid "ClusterIntegration|Remove integration"
-msgstr ""
+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 ""
+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|Save changes"
-msgstr ""
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "Ver e editar os detalhes para seu cluster"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "Ver tipos de máquina"
msgid "ClusterIntegration|See your projects"
-msgstr ""
+msgstr "Ver seus projetos"
msgid "ClusterIntegration|See zones"
-msgstr ""
+msgstr "Ver zonas"
msgid "ClusterIntegration|Something went wrong on our end."
-msgstr ""
-
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
+msgstr "Alguma coisa deu errado do nosso lado."
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
-msgstr ""
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "Algo deu errado ao criar seu cluster no Google Container Engine"
msgid "ClusterIntegration|Toggle Cluster"
-msgstr ""
-
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
+msgstr "Alternar cluster"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
-msgstr ""
+msgstr "Com um cluster associado à esse projeto, você pode usar revisão de apps, fazer deploy de suas aplicações, rodar suas pipelines e muito mais de um jeito simples."
msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr ""
+msgstr "Sua conta precisa ter %{link_to_container_engine}"
msgid "ClusterIntegration|Zone"
-msgstr ""
+msgstr "Zona"
msgid "ClusterIntegration|access to Google Container Engine"
-msgstr ""
+msgstr "Acesso ao Google Container Engine"
msgid "ClusterIntegration|cluster"
-msgstr ""
+msgstr "cluster"
msgid "ClusterIntegration|help page"
-msgstr ""
+msgstr "ajuda"
msgid "ClusterIntegration|meets the requirements"
-msgstr ""
+msgstr "atende aos requisitos"
msgid "ClusterIntegration|properly configured"
-msgstr ""
+msgstr "configurado corretamente"
msgid "Comments"
-msgstr ""
+msgstr "Comentários"
msgid "Commit"
msgid_plural "Commits"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Commit"
+msgstr[1] "Commits"
+
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] "Commit %d arquivo"
+msgstr[1] "Commit %d arquivos"
msgid "Commit Message"
-msgstr ""
+msgstr "Mensagem de Commit"
msgid "Commit duration in minutes for last 30 commits"
msgstr "Duração do commit em minutos para os últimos 30 commits"
@@ -598,7 +644,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Adicionar %{file_name}"
msgid "Commits"
-msgstr ""
+msgstr "Commits"
msgid "Commits feed"
msgstr "Feed de commits"
@@ -613,49 +659,49 @@ msgid "Compare"
msgstr "Comparar"
msgid "Container Registry"
-msgstr ""
+msgstr "Container Registry"
msgid "ContainerRegistry|Created"
-msgstr ""
+msgstr "Criado"
msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
-msgstr ""
+msgstr "Primeiro faça login no Container Registry do Gitlab usando seu nome de usuário e senha. Se você tiver %{link_2fa}, será necessário usar um %{link_token}:"
msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
-msgstr ""
+msgstr "Gitlab suporta até três níveis de nomes de imagens. Os exemplos a seguir são de imagens válidas para seu projeto:"
msgid "ContainerRegistry|How to use the Container Registry"
-msgstr ""
+msgstr "Como usar o Container Registry"
msgid "ContainerRegistry|Learn more about"
-msgstr ""
+msgstr "Leia mais sobre"
msgid "ContainerRegistry|No tags in Container Registry for this container image."
-msgstr ""
+msgstr "Sem tags no Container Registry para essa imagem."
msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
-msgstr ""
+msgstr "Uma vez que você autenticar, será possível criar e fazer upload de uma imagem usando os comandos comuns de %{build} e %{push}"
msgid "ContainerRegistry|Remove repository"
-msgstr ""
+msgstr "Apagar repositório"
msgid "ContainerRegistry|Remove tag"
-msgstr ""
+msgstr "Apagar tag"
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "Tamanho"
msgid "ContainerRegistry|Tag"
-msgstr ""
+msgstr "Tag"
msgid "ContainerRegistry|Tag ID"
-msgstr ""
+msgstr "ID da Tag"
msgid "ContainerRegistry|Use different image names"
-msgstr ""
+msgstr "Use nomes de imagem diferentes"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
-msgstr ""
+msgstr "Com o Container Registry do Docker integrado ao Gitlab, todo projeto pode ter seu próprio espaço para guardar suas imagens."
msgid "Contribution guide"
msgstr "Guia de contribuição"
@@ -663,8 +709,14 @@ msgstr "Guia de contribuição"
msgid "Contributors"
msgstr "Contribuidores"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Controle a concorrência máxima de LFS/preenchimento de anexos para o nó secundário"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Controle a concorrência máxima de preenchimento de repositórios para o nó secundário"
+
msgid "Copy SSH public key to clipboard"
-msgstr ""
+msgstr "Copiar chave SSH pública para área de transferência"
msgid "Copy URL to clipboard"
msgstr "Copiar URL para área de transferência"
@@ -684,9 +736,21 @@ msgstr "Criar diretório"
msgid "Create empty bare repository"
msgstr "Criar repositório bruto vazio"
+msgid "Create file"
+msgstr "Criar arquivo"
+
msgid "Create merge request"
msgstr "Criar merge request"
+msgid "Create new branch"
+msgstr "Criar novo branch"
+
+msgid "Create new directory"
+msgstr "Criar nova pasta"
+
+msgid "Create new file"
+msgstr "Criar novo arquivo"
+
msgid "Create new..."
msgstr "Criar novo..."
@@ -739,10 +803,10 @@ msgid "CycleAnalyticsStage|Test"
msgstr "Teste"
msgid "DashboardProjects|All"
-msgstr ""
+msgstr "Todos"
msgid "DashboardProjects|Personal"
-msgstr ""
+msgstr "Pessoal"
msgid "Define a custom pattern with cron syntax"
msgstr "Defina um padrão personalizado utilizando a sintaxe do cron"
@@ -756,25 +820,28 @@ msgstr[0] "Implantação"
msgstr[1] "Implantações"
msgid "Deploy Keys"
-msgstr ""
+msgstr "Chaves para deploy"
msgid "Description"
msgstr "Descrição"
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
-msgstr ""
+msgstr "Modelos de descrição permitem que você defina modelos de contextos específicos para issue e descrição de merge requests para seu projeto."
msgid "Details"
-msgstr ""
+msgstr "Detalhes"
msgid "Directory name"
msgstr "Nome do diretório"
msgid "Discard changes"
-msgstr ""
+msgstr "Descartar alterações"
+
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Ignorar introdução do Cycle Analytics"
msgid "Dismiss Merge Request promotion"
-msgstr ""
+msgstr "Ignorar anúncio do merge request"
msgid "Don't show again"
msgstr "Não exibir novamente"
@@ -813,25 +880,25 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Alterar Agendamento do Pipeline %{id}"
msgid "Emails"
-msgstr ""
+msgstr "Emails"
msgid "EventFilterBy|Filter by all"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por tudo"
msgid "EventFilterBy|Filter by comments"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por comentários"
msgid "EventFilterBy|Filter by issue events"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por eventos de issue"
msgid "EventFilterBy|Filter by merge events"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por eventos de merge"
msgid "EventFilterBy|Filter by push events"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por eventos de push"
msgid "EventFilterBy|Filter by team"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por equipe"
msgid "Every day (at 4:00am)"
msgstr "Todos os dias (às 4:00)"
@@ -843,7 +910,10 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "Toda semana (domingos às 4:00)"
msgid "Explore projects"
-msgstr ""
+msgstr "Explorar projetos"
+
+msgid "Explore public groups"
+msgstr "Explorar grupos públicos"
msgid "Failed to change the owner"
msgstr "Erro ao alterar o proprietário"
@@ -851,6 +921,9 @@ msgstr "Erro ao alterar o proprietário"
msgid "Failed to remove the pipeline schedule"
msgstr "Erro ao excluir o agendamento do pipeline"
+msgid "File name"
+msgstr "Nome do arquivo"
+
msgid "Files"
msgstr "Arquivos"
@@ -871,17 +944,17 @@ msgstr "publicado por"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Fork"
+msgstr[1] "Forks"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Fork criado a partir de"
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
-msgstr ""
+msgstr "Fork a partir de %{project_name} (apagado)"
msgid "Format"
-msgstr ""
+msgstr "Formato"
msgid "From issue creation until deploy to production"
msgstr "Da abertura de tarefas até a implantação para a produção"
@@ -890,22 +963,28 @@ msgid "From merge request merge until deploy to production"
msgstr "Do merge request até a implantação em produção"
msgid "GPG Keys"
-msgstr ""
+msgstr "Chaves GPG"
msgid "Geo Nodes"
-msgstr ""
+msgstr "Nós de geo"
+
+msgid "Geo|File sync capacity"
+msgstr "Capacidade de sincronização de arquivo"
msgid "Geo|Groups to replicate"
-msgstr ""
+msgstr "Grupos para replicar"
+
+msgid "Geo|Repository sync capacity"
+msgstr "Capacidade de sincronização de repositório"
msgid "Geo|Select groups to replicate."
-msgstr ""
+msgstr "Selecione grupos para replicar."
msgid "Git storage health information has been reset"
-msgstr ""
+msgstr "Informações sobre o status de saúde do storage Git foram reiniciadas"
msgid "GitLab Runner section"
-msgstr ""
+msgstr "Seção GitLab Runner"
msgid "Go to your fork"
msgstr "Ir para seu fork"
@@ -914,52 +993,97 @@ 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 "Autenticação do Google não está %{link_to_documentation}. Peça ao administrador do Gitlab se você deseja usar esse serviço."
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Bloquear compartilhamento de projetos do grupo %{group} com outros grupos"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Travar compartilhamento de grupo"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Essa configuração é aplicada no grupo %{ancestor_group} e foi sobrescrita nesse subgrupo."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Essa configuração é aplicada no grupo %{ancestor_group}. Para compartilhar projetos desse grupo com outro grupo, peça ao dono para alterar a configuração ou %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Essa configuração foi aplicada no grupo %{ancestor_group}. Você pode sobrescrevê-la ou %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "Essa configuração será aplicada à todos os subgrupos a menos que sejam sobrescritas pelo dono do grupo. Grupos que já tem acesso ao projeto continuarão a acessá-los até que sejam removidos manualmente."
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "não pode ser desativado quando a configuração \"Travar compartilhamento de grupo\" está ativada, a não ser pelo dono do grupo pai"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "retirar a trava de compartilhamento de grupo de %{ancestor_group_name}"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Um grupo é uma coleção de vários projetos."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "Se você organizar seus projetos num grupo, será como uma pasta."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Nenhum grupo encontrado"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Você pode gerenciar permissões de membros e acesso do seu grupo para cada projeto no grupo."
+
+msgid "GroupsTreeRole|as"
+msgstr "como"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Você tem certeza que deseja sair do grupo \"${this.group.fullName}\"?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Criar um projeto nesse grupo."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Criar um subgrupo nesse grupo."
+
+msgid "GroupsTree|Edit group"
+msgstr "Editar grupo"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Falha ao deixar o grupo. Por favor, verifique se você é o único dono."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Filtrar por nome..."
+
+msgid "GroupsTree|Leave this group"
+msgstr "Deixar o grupo"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Carregando grupos"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "Desculpe, nenhum grupo corresponde à sua pesquisa"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "Desculpe, nenhum grupo ou projeto correspondem à sua pesquisa"
msgid "Health Check"
-msgstr ""
+msgstr "Status de Saúde"
msgid "Health information can be retrieved from the following endpoints. More information is available"
-msgstr ""
+msgstr "Informações do status de saúde podem ser obtidas nos seguintes locais. Mais informação está disponível"
msgid "HealthCheck|Access token is"
-msgstr ""
+msgstr "O token de acesso é"
msgid "HealthCheck|Healthy"
-msgstr ""
+msgstr "Saudável"
msgid "HealthCheck|No Health Problems Detected"
-msgstr ""
+msgstr "Nenhum problema de saúde detectado"
msgid "HealthCheck|Unhealthy"
-msgstr ""
+msgstr "Não saudável"
msgid "History"
-msgstr ""
+msgstr "Histórico"
msgid "Housekeeping successfully started"
msgstr "Manutenção iniciada com sucesso"
@@ -968,21 +1092,27 @@ msgid "Import repository"
msgstr "Importar repositório"
msgid "Improve Issue boards with GitLab Enterprise Edition."
-msgstr ""
+msgstr "Melhorar issue boards com o GitLab Enterprise Edition."
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Melhore gestão das issues com os pesos no GitLab Enterprise Edition."
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Encontre o que precisa mais facilmente com a pesquisa global avançada com GitLab Enterprise Edition."
msgid "Install a Runner compatible with GitLab CI"
-msgstr ""
+msgstr "Instalar um Runner compatível com o GitLab CI"
msgid "Instance"
msgid_plural "Instances"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Instância"
+msgstr[1] "Instâncias"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Interno - O grupo e projetos internos podem ser visualizados por qualquer usuário autenticado."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Interno - O projeto pode ser acessado por qualquer usuário autenticado."
msgid "Interval Pattern"
msgstr "Padrão de intervalo"
@@ -991,22 +1121,22 @@ msgid "Introducing Cycle Analytics"
msgstr "Apresentando a Análise de Ciclo"
msgid "Issue board focus mode"
-msgstr ""
+msgstr "Modo de foco no issue board"
msgid "Issue boards with milestones"
-msgstr ""
+msgstr "Issue board com milestone"
msgid "Issue events"
-msgstr ""
+msgstr "Eventos de issue"
msgid "IssueBoards|Board"
-msgstr ""
+msgstr "Board"
msgid "IssueBoards|Boards"
-msgstr ""
+msgstr "Boards"
msgid "Issues"
-msgstr ""
+msgstr "Issues"
msgid "LFSStatus|Disabled"
msgstr "Desabilitado"
@@ -1015,7 +1145,7 @@ msgid "LFSStatus|Enabled"
msgstr "Habilitado"
msgid "Labels"
-msgstr ""
+msgstr "Etiquetas"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -1029,22 +1159,22 @@ msgid "Last commit"
msgstr "Último commit"
msgid "Last edited %{date}"
-msgstr ""
+msgstr "Última edição em %{date}"
msgid "Last edited by %{name}"
-msgstr ""
+msgstr "Última edição por %{name}"
msgid "Last update"
-msgstr ""
+msgstr "Última atualização"
msgid "Last updated"
-msgstr ""
+msgstr "Último atualizado"
msgid "LastPushEvent|You pushed to"
-msgstr ""
+msgstr "Você fez o push para"
msgid "LastPushEvent|at"
-msgstr ""
+msgstr "em"
msgid "Learn more in the"
msgstr "Saiba mais em"
@@ -1052,6 +1182,9 @@ msgstr "Saiba mais em"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "documentação de agendamento de pipeline"
+msgid "Leave"
+msgstr "Sair"
+
msgid "Leave group"
msgstr "Sair do grupo"
@@ -1059,7 +1192,7 @@ msgid "Leave project"
msgstr "Sair do projeto"
msgid "License"
-msgstr ""
+msgstr "Licença"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
@@ -1067,43 +1200,52 @@ msgstr[0] "Limitado a mostrar %d evento, no máximo"
msgstr[1] "Limitado a mostrar %d eventos, no máximo"
msgid "Lock"
-msgstr ""
+msgstr "Bloquear"
msgid "Locked"
-msgstr ""
+msgstr "Bloqueado"
msgid "Locked Files"
-msgstr ""
+msgstr "Arquivos bloqueados"
+
+msgid "Login"
+msgstr "Entrar"
+
+msgid "Maximum git storage failures"
+msgstr "Máximo de falhas do git storage"
msgid "Median"
msgstr "Mediana"
msgid "Members"
-msgstr ""
+msgstr "Membros"
msgid "Merge Requests"
-msgstr ""
+msgstr "Merge Requests"
msgid "Merge events"
-msgstr ""
+msgstr "Eventos de merge"
msgid "Merge request"
-msgstr ""
+msgstr "Merge requests"
msgid "Messages"
-msgstr ""
+msgstr "Mensagens"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "adicione uma chave SSH"
msgid "Monitoring"
-msgstr ""
+msgstr "Monitoramento"
msgid "More information is available|here"
-msgstr ""
+msgstr "Mais informações estão disponíveis|aqui"
msgid "Multiple issue boards"
-msgstr ""
+msgstr "Múltiplos issue boards"
+
+msgid "New Cluster"
+msgstr "Novo cluster"
msgid "New Issue"
msgid_plural "New Issues"
@@ -1122,23 +1264,32 @@ msgstr "Novo diretório"
msgid "New file"
msgstr "Novo arquivo"
+msgid "New group"
+msgstr "Novo grupo"
+
msgid "New issue"
msgstr "Nova issue"
msgid "New merge request"
msgstr "Novo merge request"
+msgid "New project"
+msgstr "Novo projeto"
+
msgid "New schedule"
msgstr "Novo agendamento"
msgid "New snippet"
msgstr "Novo snippet"
+msgid "New subgroup"
+msgstr "Novo subgrupo"
+
msgid "New tag"
msgstr "Nova tag"
msgid "No container images stored for this project. Add one by following the instructions above."
-msgstr ""
+msgstr "Nenhuma imagem gravada para esse projeto. Adiciona uma com as instruções a seguir."
msgid "No repository"
msgstr "Nenhum repositório"
@@ -1147,7 +1298,7 @@ msgid "No schedules"
msgstr "Nenhum agendamento"
msgid "None"
-msgstr ""
+msgstr "Nenhum"
msgid "Not available"
msgstr "Não disponível"
@@ -1210,49 +1361,55 @@ msgid "NotificationLevel|Watch"
msgstr "Observar"
msgid "Notifications"
+msgstr "Notificações"
+
+msgid "Number of access attempts"
+msgstr "Número de tentativas de acesso"
+
+msgid "Number of failures before backing off"
msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
msgid "Only project members can comment."
-msgstr ""
+msgstr "Somente membros do projeto podem comentar."
msgid "OpenedNDaysAgo|Opened"
msgstr "Aberto"
msgid "Opens in a new window"
-msgstr ""
+msgstr "Abrir em nova janela"
msgid "Options"
msgstr "Opções"
msgid "Overview"
-msgstr ""
+msgstr "Visão geral"
msgid "Owner"
msgstr "Proprietário"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "Último >>"
msgid "Pagination|Next"
-msgstr ""
+msgstr "Próximo"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "Anterior"
msgid "Pagination|« First"
-msgstr ""
+msgstr "<< Primeiro"
msgid "Password"
-msgstr ""
+msgstr "Senha"
msgid "People without permission will never get a notification and won\\'t be able to comment."
-msgstr ""
+msgstr "Pessoas sem permissão nunca receberão uma notificação e não serão capazes de comentar."
msgid "Pipeline"
-msgstr ""
+msgstr "Pipeline"
msgid "Pipeline Health"
msgstr "Saúde da Pipeline"
@@ -1264,7 +1421,7 @@ msgid "Pipeline Schedules"
msgstr "Agendamentos da Pipeline"
msgid "Pipeline quota"
-msgstr ""
+msgstr "Cota de pipeline"
msgid "PipelineCharts|Failed:"
msgstr "Falhou:"
@@ -1324,19 +1481,19 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizado"
msgid "Pipelines"
-msgstr ""
+msgstr "Pipelines"
msgid "Pipelines charts"
msgstr "Gráficos de pipelines"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "Pipelines para o último mês"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "Pipelines para a última semana"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "Pipelines para o último ano"
msgid "Pipeline|all"
msgstr "todos"
@@ -1351,10 +1508,55 @@ msgid "Pipeline|with stages"
msgstr "com etapas"
msgid "Preferences"
-msgstr ""
+msgstr "Preferências"
+
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Privado - O acesso ao projeto deve ser concedido explicitamente para cada usuário."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "Privado - O grupo e seus projetos só podem ser vistos por seus membros."
msgid "Profile"
-msgstr ""
+msgstr "Perfil"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr "Conta agendada para remoção."
+
+msgid "Profiles|Delete Account"
+msgstr "Excluir conta"
+
+msgid "Profiles|Delete account"
+msgstr "Excluir conta"
+
+msgid "Profiles|Delete your account?"
+msgstr "Deseja apagar sua conta?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Apagando conta tem os seguintes efeitos:"
+
+msgid "Profiles|Invalid password"
+msgstr "Senha inválida"
+
+msgid "Profiles|Invalid username"
+msgstr "Nome de usuário inválido"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Escreva %{confirmationValue} para confirmar:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "Você não tem permissão para apagar esse usuário."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Você precisa delegar outro usuário para ser dono ou apagar esses grupos antes de excluir sua conta."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Sua conta é atualmente proprietária dos seguintes grupos:"
+
+msgid "Profiles|your account"
+msgstr "sua conta"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "O projeto '%{project_name}' está sendo excluído."
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projeto'%{project_name}' marcado para exclusão."
@@ -1365,14 +1567,11 @@ msgstr "Projeto '%{project_name}' criado com sucesso."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Projeto '%{project_name}' atualizado com sucesso."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Projeto '%{project_name}' será excluído."
-
msgid "Project access must be granted explicitly to each user."
msgstr "Acesso ao projeto deve ser concedido explicitamente para cada usuário."
msgid "Project details"
-msgstr ""
+msgstr "Detalhes do projeto"
msgid "Project export could not be deleted."
msgstr "A exportação do projeto não pôde ser excluída."
@@ -1387,7 +1586,7 @@ msgid "Project export started. A download link will be sent by email."
msgstr "Exportação do projeto iniciada. Um link para baixá-la será enviado por email."
msgid "ProjectActivityRSS|Subscribe"
-msgstr ""
+msgstr "Inscreva-se"
msgid "ProjectFeature|Disabled"
msgstr "Desabilitado"
@@ -1411,46 +1610,61 @@ msgid "ProjectNetworkGraph|Graph"
msgstr "Ãrvore"
msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr ""
+msgstr "Fale com um administrador para mudar essa configuração."
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
-msgstr ""
+msgstr "Esse repositório só aceita push de commits assinados."
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr ""
+msgstr "Essa configuração está aplicada à nivel de servidor e pode ser sobrescrita por um adminstrador."
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr ""
+msgstr "Essa configuração está aplicada à nivel de servidor mas pode ser sobrescrita para esse projeto."
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr ""
+msgstr "Essa configuração será aplicada à todos os projetos, a não ser que sejam sobrescritos pelo administrador."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr "Nesse repositório, usuários só podem fazer push de commits verificados pelos seus e-mails."
+
+msgid "Projects"
+msgstr "Projetos"
msgid "ProjectsDropdown|Frequently visited"
-msgstr ""
+msgstr "Visitados frequentemente"
msgid "ProjectsDropdown|Loading projects"
-msgstr ""
+msgstr "Carregando projetos"
msgid "ProjectsDropdown|Projects you visit often will appear here"
-msgstr ""
+msgstr "Projetos que você visita frequentemente aparecerão aqui"
msgid "ProjectsDropdown|Search your projects"
-msgstr ""
+msgstr "Procure seus projetos"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "Algo deu errado do nosso lado."
msgid "ProjectsDropdown|Sorry, no projects matched your search"
-msgstr ""
+msgstr "Desculpe, nenhum projeto corresponde a sua pesquisa"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
-msgstr ""
+msgstr "Esta funcionalidade necessita de suporte à localStorage do navegador"
+
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Público - O grupo e seus projetos podem ser visualizados por todos sem autenticação."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Público - O projeto pode ser acessado sem nenhuma autenticação."
msgid "Push Rules"
-msgstr ""
+msgstr "Regras de push"
msgid "Push events"
-msgstr ""
+msgstr "Eventos de push"
+
+msgid "PushRule|Committer restriction"
+msgstr "Restrição de commit"
msgid "Read more"
msgstr "Leia mais"
@@ -1465,7 +1679,7 @@ msgid "RefSwitcher|Tags"
msgstr "Tags"
msgid "Registry"
-msgstr ""
+msgstr "Registry"
msgid "Related Commits"
msgstr "Commits Relacionados"
@@ -1492,19 +1706,19 @@ msgid "Remove project"
msgstr "Remover projeto"
msgid "Repository"
-msgstr ""
+msgstr "Repositório"
msgid "Request Access"
msgstr "Solicitar acesso"
msgid "Reset git storage health information"
-msgstr ""
+msgstr "Reiniciar informações de status do storage Git"
msgid "Reset health check access token"
-msgstr ""
+msgstr "Recriar o token de status de saúde"
msgid "Reset runners registration token"
-msgstr ""
+msgstr "Recriar o token de registro de runners"
msgid "Revert this commit"
msgstr "Reverter este commit"
@@ -1513,10 +1727,13 @@ msgid "Revert this merge request"
msgstr "Reverter esse merge request"
msgid "SSH Keys"
-msgstr ""
+msgstr "Chaves SSH"
+
+msgid "Save"
+msgstr "Salvar"
msgid "Save changes"
-msgstr ""
+msgstr "Salvar alterações"
msgid "Save pipeline schedule"
msgstr "Salvar agendamento da pipeline"
@@ -1525,7 +1742,7 @@ msgid "Schedule a new pipeline"
msgstr "Agendar nova pipeline"
msgid "Schedules"
-msgstr ""
+msgstr "Agendamentos"
msgid "Scheduling Pipelines"
msgstr "Agendando pipelines"
@@ -1533,6 +1750,15 @@ msgstr "Agendando pipelines"
msgid "Search branches and tags"
msgstr "Procurar branch e tags"
+msgid "Seconds before reseting failure information"
+msgstr "Segundos antes de redefinir as informações de falha"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Segundos a esperar após uma falha de armazenamento"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "Segundo de espera para tentativa de acesso ao storage"
+
msgid "Select Archive Format"
msgstr "Selecionar Formato do Arquivo"
@@ -1543,7 +1769,7 @@ msgid "Select target branch"
msgstr "Selecionar branch de destino"
msgid "Service Templates"
-msgstr ""
+msgstr "Modelos de serviço"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "Defina uma senha para sua conta para aceitar ou entregar código via %{protocol}."
@@ -1561,13 +1787,13 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "defina uma senha"
msgid "Settings"
-msgstr ""
+msgstr "Configurações"
msgid "Show parent pages"
-msgstr ""
+msgstr "Mostrar páginas acima"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "Mostrar subgrupos acima"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -1575,161 +1801,173 @@ msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
msgid "Snippets"
-msgstr ""
+msgstr "Snippets"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "Algo deu errado do nosso lado."
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "Algo deu errado ao tentar mudar o estado de bloqueio de ${this.issuableDisplayName(this.issuableType)}"
msgid "Something went wrong while fetching the projects."
-msgstr ""
+msgstr "Algo deu errado ao recuperar os projetos."
msgid "Something went wrong while fetching the registry list."
-msgstr ""
+msgstr "Algo deu errado ao recuperar a lista de registro."
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr ""
+msgid "Sort by"
+msgstr "Ordenar por"
msgid "SortOptions|Access level, ascending"
-msgstr ""
+msgstr "Nível de acesso, ascendente"
msgid "SortOptions|Access level, descending"
-msgstr ""
+msgstr "Nível de acesso, decrescente"
msgid "SortOptions|Created date"
-msgstr ""
+msgstr "Data de criação"
msgid "SortOptions|Due date"
-msgstr ""
+msgstr "Data de vencimento"
msgid "SortOptions|Due later"
-msgstr ""
+msgstr "Data de vencimento mais longe"
msgid "SortOptions|Due soon"
-msgstr ""
+msgstr "Data de vencimento mais próxima"
msgid "SortOptions|Label priority"
-msgstr ""
+msgstr "Prioridade de label"
msgid "SortOptions|Largest group"
-msgstr ""
+msgstr "Maior grupo"
msgid "SortOptions|Largest repository"
-msgstr ""
+msgstr "Maior repositório"
msgid "SortOptions|Last created"
-msgstr ""
+msgstr "Últimos criados"
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "Últimos associados"
msgid "SortOptions|Last updated"
-msgstr ""
+msgstr "Últimos atualizados"
msgid "SortOptions|Least popular"
-msgstr ""
+msgstr "Menos populares"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "Menor peso"
msgid "SortOptions|Milestone"
-msgstr ""
+msgstr "Milestone"
msgid "SortOptions|Milestone due later"
-msgstr ""
+msgstr "Milestone de fim mais longo"
msgid "SortOptions|Milestone due soon"
-msgstr ""
+msgstr "Milestone de fim mais próximo"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "Mais peso"
msgid "SortOptions|Most popular"
-msgstr ""
+msgstr "Mais populares"
msgid "SortOptions|Name"
-msgstr ""
+msgstr "Nome"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "Nome, ascendente"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "Nome, decrescente"
msgid "SortOptions|Oldest created"
-msgstr ""
+msgstr "Criação mais antiga"
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "Primeiros associados"
msgid "SortOptions|Oldest sign in"
-msgstr ""
+msgstr "Assinados mais antigos"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "Atualização mais antiga"
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "Popularidade"
msgid "SortOptions|Priority"
-msgstr ""
+msgstr "Prioridade"
msgid "SortOptions|Recent sign in"
-msgstr ""
+msgstr "Assinados mais novos"
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "Iniciar mais tarde"
msgid "SortOptions|Start soon"
-msgstr ""
+msgstr "Iniciar mais próximo"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "Peso"
msgid "Source code"
msgstr "Código-fonte"
msgid "Spam Logs"
-msgstr ""
+msgstr "Logs de spam"
msgid "Specify the following URL during the Runner setup:"
-msgstr ""
+msgstr "Especifique a seguinte URL durante a configuração do Runner:"
msgid "StarProject|Star"
msgstr "Marcar"
msgid "Starred projects"
-msgstr ""
+msgstr "Projetos favoritos"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Iniciar um %{new_merge_request} a partir dessas alterações"
msgid "Start the Runner!"
-msgstr ""
+msgstr "Inicie o Runner!"
+
+msgid "Subgroups"
+msgstr "Subgrupos"
+
+msgid "Subscribe"
+msgstr "Assine"
msgid "Switch branch/tag"
msgstr "Trocar branch/tag"
msgid "System Hooks"
-msgstr ""
+msgstr "Hooks do sistema"
msgid "Tag"
msgid_plural "Tags"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Tag"
+msgstr[1] "Tags"
msgid "Tags"
-msgstr ""
+msgstr "Tags"
msgid "Target Branch"
msgstr "Branch de destino"
msgid "Team"
-msgstr ""
+msgstr "Equipe"
msgid "Thanks! Don't show me this again"
-msgstr ""
+msgstr "Obrigado! Não mostrar novamente"
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "A pesquisa global avançado no GitLab é um serviço poderoso de pesquisa que poupa seu tempo. Ao invés de criar código duplicado e perder seu tempo, você pode agora pesquisar por código de outros times que podem ajudar no seu próprio projeto."
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
@@ -1744,6 +1982,15 @@ msgstr "O relacionamento como fork foi removido."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "A etapa de planejamento mostra o tempo que se leva desde a criação de uma issue até sua atribuição à um milestone, ou sua adição a uma lista no seu Issue Board. Comece a criar issues para ver dados para esta etapa."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr "O número de tentativas que gitlab fará para acessar um storage."
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr "O número de falhas até o GitLab começar a desabilitar temporariamente o acesso a um nó de storage em um host"
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "O número de falhas para que o GitLab desabilite o acesso ao storage. O número de falhas pode ser redefinido na interface do administrador: %{link_to_health_page} ou %{api_documentation_link}."
+
msgid "The phase of the development lifecycle."
msgstr "A fase do ciclo de vida do desenvolvimento."
@@ -1774,6 +2021,12 @@ msgstr "A etapa de homologação mostra o tempo entre o aceite da solicitação
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "A etapa de testes mostra o tempo que o GitLab CI leva para executar cada pipeline para a solicitação de incorporação associada. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "Tempo em segundos para o GitLab manter as informações de falha. Se nenhuma falha ocorrer durante este tempo, a informação sobre o ponto de montagem será redefinida."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "Tempo em segundos que o GitLab tentará acessar o storage. Depois desse tempo, um erro de tempo excedido será disparado."
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "O tempo necessário por cada entrada de dados reunida por essa etapa."
@@ -1781,25 +2034,28 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5+7)/2 = 6."
msgid "There are problems accessing Git storage: "
-msgstr ""
+msgstr "Há problemas para acessar o storage Git: "
+
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Esse branch mudou desde quando você começou sua edição. Você quer criar um novo branch?"
msgid "This is a confidential issue."
-msgstr ""
+msgstr "Essa issue é confidencial."
msgid "This is the author's first Merge Request to this project."
-msgstr ""
+msgstr "Esse é o autor do primeiro merge request desse projeto."
msgid "This issue is confidential and locked."
-msgstr ""
+msgstr "Essa issue é confidencial e está bloqueada."
msgid "This issue is locked."
-msgstr ""
+msgstr "Essa issue está bloqueada."
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Isto significa que você não pode entregar código até que crie um repositório vazio ou importe um existente."
msgid "This merge request is locked."
-msgstr ""
+msgstr "Esse merge request está bloqueado."
msgid "Time before an issue gets scheduled"
msgstr "Tempo até que uma issue seja agendada"
@@ -1931,7 +2187,7 @@ msgid "Timeago|in 1 year"
msgstr "em 1 ano"
msgid "Timeago|in a while"
-msgstr ""
+msgstr "há algum tempo"
msgid "Timeago|less than a minute ago"
msgstr "há menos de um minuto"
@@ -1956,31 +2212,34 @@ 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 ""
+msgstr "Acompanhar atividade com o Contribution Analytics."
msgid "Unlock"
-msgstr ""
+msgstr "Desbloquear"
msgid "Unlocked"
-msgstr ""
+msgstr "Desbloqueado"
msgid "Unstar"
msgstr "Desmarcar"
+msgid "Unsubscribe"
+msgstr "Desassinar"
+
msgid "Upgrade your plan to activate Advanced Global Search."
-msgstr ""
+msgstr "Atualize seu plano para ativar a pesquisa global avançada."
msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr ""
+msgstr "Atualize seu plano para ativar o Contribution Analytics."
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr ""
+msgstr "Atualize seu plano para ativar Webhooks de grupo."
msgid "Upgrade your plan to activate Issue weight."
-msgstr ""
+msgstr "Atualize seu plano para ativar peso nas issues."
msgid "Upgrade your plan to improve Issue boards."
-msgstr ""
+msgstr "Atualize seu plano para melhorar os issue boards."
msgid "Upload New File"
msgstr "Enviar Novo Arquivo"
@@ -1992,19 +2251,19 @@ msgid "UploadLink|click to upload"
msgstr "clique para fazer upload"
msgid "Use the following registration token during setup:"
-msgstr ""
+msgstr "Use o seguinte token de registro durante a configuração:"
msgid "Use your global notification setting"
msgstr "Utilizar configuração de notificação global"
msgid "View file @ "
-msgstr ""
+msgstr "Ver arquivo @ "
msgid "View open merge request"
msgstr "Ver merge request aberto"
msgid "View replaced file @ "
-msgstr ""
+msgstr "Ver arquivo substituído @ "
msgid "VisibilityLevel|Internal"
msgstr "Interno"
@@ -2025,115 +2284,118 @@ msgid "We don't have enough data to show this stage."
msgstr "Esta etapa não possui dados suficientes para exibição."
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
-msgstr ""
+msgstr "Webhooks permitem que você acione uma URL se, por exemplo, um novo código for feito push ou uma nova issue criada. Você pode configurar os webhooks para escutar eventos específicos como push, issue ou merge request. Webhooks de grupo aplicarão para todos os projetos no grupo, permitindo você padronizar o funcionamento em todo o grupo."
msgid "Weight"
-msgstr ""
+msgstr "Peso"
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "Falha ao acessar o storage. Gitlab impedirá o acesso ao storage pelo tempo especificado aqui. Isso permite que o sistema de arquivos se recupere. Repositórios que estiverem em nós com falha ficarão temporariamente indisponíveis"
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "WikiClone|Clone your wiki"
-msgstr ""
+msgstr "Clonar sua wiki"
msgid "WikiClone|Git Access"
-msgstr ""
+msgstr "Acesso Git"
msgid "WikiClone|Install Gollum"
-msgstr ""
+msgstr "Instalar Gollum"
msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
-msgstr ""
+msgstr "É recomendado instalar %{markdown} para que as funções GFM sejam renderizadas localmente:"
msgid "WikiClone|Start Gollum and edit locally"
-msgstr ""
+msgstr "Inicie o Gollum e edite localmente"
msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
-msgstr ""
+msgstr "Você não tem permissão para criar páginas web"
msgid "WikiHistoricalPage|This is an old version of this page."
-msgstr ""
+msgstr "Essa é uma versão antiga dessa página."
msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
-msgstr ""
+msgstr "Você pode ver o %{most_recent_link} ou navegar o %{history_link}."
msgid "WikiHistoricalPage|history"
-msgstr ""
+msgstr "histórico"
msgid "WikiHistoricalPage|most recent version"
-msgstr ""
+msgstr "versão mais recente"
msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
-msgstr ""
+msgstr "Mais exemplos em %{docs_link}"
msgid "WikiMarkdownDocs|documentation"
-msgstr ""
+msgstr "documentação"
msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
-msgstr ""
+msgstr "Para criar um link para uma (nova) página, é so digitar %{link_example}"
msgid "WikiNewPagePlaceholder|how-to-setup"
-msgstr ""
+msgstr "como instalar"
msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
-msgstr ""
+msgstr "Dica: Você pode especificar o caminho completo para o novo arquivo. Nós vamos criar automaticamente quaisquer diretórios ainda não existentes."
msgid "WikiNewPageTitle|New Wiki Page"
-msgstr ""
+msgstr "Nova página Wiki"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
-msgstr ""
+msgstr "Quer mesmo apagar essa página?"
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
-msgstr ""
+msgstr "Alguém editou essa página ao mesmo tempo que você. Por favor olhe %{page_link} e tenha certeza de que suas mudanças não removerão as mudanças deles."
msgid "WikiPageConflictMessage|the page"
-msgstr ""
+msgstr "a página"
msgid "WikiPageCreate|Create %{page_title}"
-msgstr ""
+msgstr "Criar %{page_title}"
msgid "WikiPageEdit|Update %{page_title}"
-msgstr ""
+msgstr "Atualizar %{page_title}"
msgid "WikiPage|Page slug"
-msgstr ""
+msgstr "Nome amigável da página"
msgid "WikiPage|Write your content or drag files here..."
-msgstr ""
+msgstr "Escreve seu conteudo ou arraste arquivos aqui..."
msgid "Wiki|Create Page"
-msgstr ""
+msgstr "Criar página"
msgid "Wiki|Create page"
-msgstr ""
+msgstr "Criar página"
msgid "Wiki|Edit Page"
-msgstr ""
+msgstr "Ediar página"
msgid "Wiki|Empty page"
-msgstr ""
+msgstr "Página vazia"
msgid "Wiki|More Pages"
-msgstr ""
+msgstr "Mais páginas"
msgid "Wiki|New page"
-msgstr ""
+msgstr "Nova página"
msgid "Wiki|Page history"
-msgstr ""
+msgstr "Histórico da página"
msgid "Wiki|Page version"
-msgstr ""
+msgstr "Versão da página"
msgid "Wiki|Pages"
-msgstr ""
+msgstr "Páginas"
msgid "Wiki|Wiki Pages"
-msgstr ""
+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 ""
+msgstr "Com o contribution analytics você pode ter uma visão geral da atividade em issue, merge request e evento de push para sua organização e seus membros."
msgid "Withdraw Access Request"
msgstr "Remover Requisição de Acesso"
@@ -2145,14 +2407,26 @@ msgid "You are going to remove %{project_name_with_namespace}. Removed project C
msgstr "Você irá remover %{project_name_with_namespace}. O projeto removido NÃO PODE ser restaurado! Tem certeza ABSOLUTA?"
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
+msgstr "Você está prestes a remover a relação de fork do projeto original %{forked_from_project}. Você tem CERTEZA disso?"
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Você irá transferir %{project_name_with_namespace} para outro proprietário. Tem certeza ABSOLUTA?"
+msgid "You are on a read-only GitLab instance."
+msgstr "Você está em uma instância somente-leitura do GitLab."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Você está em uma instância somente-leitura do GitLab. Se você quiser fazer qualquer alteração visite %{link_to_primary_node}."
+
msgid "You can only add files when you are on a branch"
msgstr "Você somente pode adicionar arquivos quando estiver em um branch"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Você não pode escrever numa instância secundária de somente leitura do GitLab Geo. Por favor use %{link_to_primary_node}."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Você não pode escrever nesta instância somente-leitura do GitLab."
+
msgid "You have reached your project limit"
msgstr "Você atingiu o limite de seu projeto"
@@ -2184,16 +2458,19 @@ msgid "You won't be able to pull or push project code via SSH until you %{add_ss
msgstr "Você não conseguirá fazer pull ou push no projeto via SSH até que adicione %{add_ssh_key_link} ao seu perfil"
msgid "Your comment will not be visible to the public."
-msgstr ""
+msgstr "Seu comentário não estará visível ao público."
+
+msgid "Your groups"
+msgstr "Seus grupos"
msgid "Your name"
msgstr "Seu nome"
msgid "Your projects"
-msgstr ""
+msgstr "Seus projetos"
msgid "commit"
-msgstr ""
+msgstr "commit"
msgid "day"
msgid_plural "days"
@@ -2211,9 +2488,15 @@ msgid_plural "parents"
msgstr[0] "pai"
msgstr[1] "pais"
-msgid "to help your contributors communicate effectively!"
-msgstr ""
+msgid "password"
+msgstr "senha"
msgid "personal access token"
-msgstr ""
+msgstr "token de acesso pessoal"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "para ajudar seus contribuintes à se comunicar de maneira eficaz!"
+
+msgid "username"
+msgstr "nome do usuário"
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index 7e1f23178b9..2f612f46799 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:37-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-05 14:39-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -19,24 +19,30 @@ msgstr ""
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d коммит"
-msgstr[1] "%d коммитов"
+msgstr[1] "%d коммита"
msgstr[2] "%d коммитов"
msgid "%d layer"
msgid_plural "%d layers"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "%d Ñлой"
+msgstr[1] "%d ÑлоÑ"
+msgstr[2] "%d Ñлоёв"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s добавленный коммит был иÑключен Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
-msgstr[1] "%s добавленные коммиты были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
-msgstr[2] "%s добавленные коммиты были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
+msgstr[1] "%s добавленных коммита были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
+msgstr[2] "%s добавленных коммитов были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} добавил коммит %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] "%{count} учаÑтник"
+msgstr[1] "%{count} учаÑтника"
+msgstr[2] "%{count} учаÑтников"
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "на %{number_commits_behind} коммитов позади %{default_branch}, на %{number_commits_ahead} коммитов впереди"
@@ -58,20 +64,26 @@ msgstr[2] "%{storage_name}: %{failed_attempts} - неудачные попытк
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдите по ÑÑылке %{link} Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ð¸ об уÑтановке)."
+msgid "+ %{moreCount} more"
+msgstr "+ ещё %{moreCount}"
+
+msgid "- show less"
+msgstr "- Ñвернуть"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
-msgstr[1] "%d Ñборочных линий"
+msgstr[1] "%d Ñборочных линии"
msgstr[2] "%d Ñборочных линий"
msgid "1st contribution!"
msgstr "Первый вклад!"
msgid "2FA enabled"
-msgstr ""
+msgstr "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð²ÐºÐ»ÑŽÑ‡ÐµÐ½Ð°"
msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Графики отноÑительно непрерывной интеграции (Ci)"
+msgstr "Графики непрерывной интеграции (CI)"
msgid "About auto deploy"
msgstr "Об автоматичеÑком развёртывании"
@@ -115,9 +127,18 @@ msgstr "Добавьте ключ SSH в Ñвой профиль, чтобы оÑ
msgid "Add new directory"
msgstr "Добавить новый каталог"
+msgid "AdminHealthPageLink|health page"
+msgstr "Ñтраница работоÑпоÑобноÑти"
+
+msgid "Advanced settings"
+msgstr "РаÑширенные наÑтройки"
+
msgid "All"
msgstr "Ð’Ñе"
+msgid "An error occurred. Please try again."
+msgstr "Произошла ошибка. ПожалуйÑта, попробуйте Ñнова."
+
msgid "Appearance"
msgstr "Оформление"
@@ -131,13 +152,16 @@ msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Ð’Ñ‹ дейÑтвительно хотите удалить Ñто раÑпиÑание Ñборочной линии?"
msgid "Are you sure you want to discard your changes?"
-msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите отменить Ваши изменениÑ?"
+msgstr "Ð’Ñ‹ уверены, что хотите отменить ваши изменениÑ?"
+
+msgid "Are you sure you want to leave this group?"
+msgstr "Ð’Ñ‹ уверены, что хотите покинуть Ñту группу?"
msgid "Are you sure you want to reset registration token?"
-msgstr ""
+msgstr "Ð’Ñ‹ уверены, что хотите ÑброÑить Ñтот региÑтрационный токен?"
msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgstr "Ð’Ñ‹ уверены, что хотите ÑброÑить Ñтот токен проверки работоÑпоÑобноÑти?"
msgid "Are you sure?"
msgstr "Вы уверены?"
@@ -166,59 +190,62 @@ msgstr "ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкого ревью и
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr "Включить в наÑтройках"
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "Ð”Ð»Ñ Ñтого проекта может быть активирован автоматичеÑкий DevOps. Он будет автоматичеÑки Ñобирать, теÑтировать и разворачивать ваши Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½Ð° оÑнове предопределенной конфигурации CI/CD."
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
-msgstr ""
+msgstr "Подробнее по ÑÑылке %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Ð’Ñ‹ можете активировать %{link_to_settings} Ð´Ð»Ñ Ñтого проекта."
msgid "Billing"
-msgstr ""
+msgstr "Тариф"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} иÑпользует тарифный план %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "ÐвтоматичеÑкое повышение или понижение недоÑтупно Ð´Ð»Ñ Ð½ÐµÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… тарифных планов в наÑтоÑщее времÑ."
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "Текущий тарифный план"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "Поддержка Клиентов"
msgid "BillingPlans|Downgrade"
-msgstr ""
+msgstr "Понижение"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "Узнайте больше о каждом тарифном плане прочитав наш %{faq_link}."
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "Управление тарифным планом"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "ПожалуйÑта, в Ñтом Ñлучае обратитеÑÑŒ в %{customer_support_link}."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "ПоÑмотреть возможноÑти %{plan_name} полноÑтью"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "Эта группа иÑпользует тарифный план ÑвÑзанный Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑкой группой."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð°Ñ€Ð¸Ñ„Ð½Ñ‹Ð¼ планом Ñтой группы поÑетите раздел тарификации %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "Повышение"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð²Ñ‹ иÑпользуете тарифный план %{plan_link}."
msgid "BillingPlans|frequently asked questions"
msgstr "ЧаÑто задаваемые вопроÑÑ‹"
@@ -227,7 +254,7 @@ msgid "BillingPlans|monthly"
msgstr "ежемеÑÑчно"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "оплачиваетÑÑ ÐµÐ¶ÐµÐ³Ð¾Ð´Ð½Ð¾ в размере %{price_per_year}"
msgid "BillingPlans|per user"
msgstr "за пользователÑ"
@@ -239,7 +266,10 @@ msgstr[1] "Ветки"
msgstr[2] "Ветки"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
-msgstr "Ветка <strong>%{branch_name}</strong> Ñоздана. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичеÑкого Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð±ÐµÑ€Ð¸Ñ‚Ðµ Yaml-шаблон Ð´Ð»Ñ GitLab CI и зафикÑируйте Ñвои изменениÑ. %{link_to_autodeploy_doc}"
+msgstr "Ветка <strong>%{branch_name}</strong> Ñоздана. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичеÑкого Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð±ÐµÑ€Ð¸Ñ‚Ðµ YAML-шаблон Ð´Ð»Ñ GitLab CI и зафикÑируйте Ñвои изменениÑ. %{link_to_autodeploy_doc}"
+
+msgid "Branch has changed"
+msgstr "Ветка была изменена"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ПоиÑк веток"
@@ -365,7 +395,7 @@ msgid "Cancel edit"
msgstr "Отменить редактирование"
msgid "Change Weight"
-msgstr ""
+msgstr "Изменить ВеÑ"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Выбрать в ветке"
@@ -392,10 +422,10 @@ msgid "Cherry-pick this commit"
msgstr "Подобрать в Ñтом коммите"
msgid "Cherry-pick this merge request"
-msgstr "Побрать в Ñтом запроÑе на ÑлиÑние"
+msgstr "Подобрать Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
-msgstr ""
+msgstr "Выберите группы, которые хотите Ñкопировать на вторичный узел. ОÑтавьте пуÑтым Ð´Ð»Ñ ÐºÐ¾Ð¿Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ðµ вÑего."
msgid "CiStatusLabel|canceled"
msgstr "отменено"
@@ -451,134 +481,146 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "выполнÑетÑÑ"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "CircuitBreaker API"
+
msgid "Clone repository"
-msgstr ""
+msgstr "Клонировать репозиторий"
msgid "Close"
msgstr "Закрыть"
+msgid "Cluster"
+msgstr "КлаÑтер"
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr ""
+msgstr "%{link_to_container_project} должен быть Ñоздан под Ñтой учетной запиÑью"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Параметры клаÑтера"
msgid "ClusterIntegration|Cluster integration"
-msgstr ""
+msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров"
msgid "ClusterIntegration|Cluster integration is disabled for this project."
-msgstr ""
+msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров отключена Ð´Ð»Ñ Ñтого проекта."
msgid "ClusterIntegration|Cluster integration is enabled for this project."
-msgstr ""
+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 ""
+msgstr "Ð”Ð»Ñ Ñтого проекта включена Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров. Отключение интеграции не повлиÑет на клаÑтер, но Ñоединение Ñ GitLab будет временно отключено."
msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr ""
+msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..."
msgid "ClusterIntegration|Cluster name"
-msgstr ""
+msgstr "Ðазвание клаÑтера"
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr ""
+msgstr "КлаÑтер был уÑпешно Ñоздан в Google Container Engine"
msgid "ClusterIntegration|Copy cluster name"
-msgstr ""
+msgstr "Копировать название клаÑтера"
msgid "ClusterIntegration|Create cluster"
-msgstr ""
+msgstr "Создать клаÑтер"
msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr ""
+msgstr "Создать новый клаÑтер в Google Container Engine"
msgid "ClusterIntegration|Enable cluster integration"
-msgstr ""
+msgstr "Включить интеграцию Ñ ÐºÐ»Ð°Ñтерами"
msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr ""
+msgstr "Идентификатор проекта в Google Cloud Platform"
msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Google Container Engine"
msgid "ClusterIntegration|Google Container Engine project"
-msgstr ""
-
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Проект Google Container Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr ""
+msgstr "Узнайте больше на %{link_to_documentation}"
-msgid "ClusterIntegration|See machine types"
-msgstr ""
+msgid "ClusterIntegration|Machine type"
+msgstr "Тип машины"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
-msgstr ""
+msgstr "УбедитеÑÑŒ, что ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ %{link_to_requirements} Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтеров"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Управление интеграцией клаÑтера на вашем проекте Gitlab"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "УправлÑйте клаÑтером, Ð¿ÐµÑ€ÐµÐ¹Ð´Ñ Ð¿Ð¾ ÑÑылке %{link_gke}"
msgid "ClusterIntegration|Number of nodes"
-msgstr ""
+msgstr "КоличеÑтво узлов"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "ПожалуйÑта, убедитеÑÑŒ, что ваш аккаунт Google отвечает Ñледующим требованиÑм:"
msgid "ClusterIntegration|Project namespace (optional, unique)"
-msgstr ""
+msgstr "ПроÑтранÑтво имен проекта (необÑзательное, уникальное)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Прочтите нашу документацию %{link_to_help_page} по интеграции клаÑтера."
msgid "ClusterIntegration|Remove cluster integration"
-msgstr ""
+msgstr "Удалить интеграцию Ñ ÐºÐ»Ð°Ñтером"
msgid "ClusterIntegration|Remove integration"
-msgstr ""
+msgstr "Удалить интеграцию"
msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr ""
+msgstr "При удалении интеграции Ñ ÐºÐ»Ð°Ñтером будет удалена ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтера, которую вы добавили в Ñтот проект. Данное дейÑтвие не удалит Ñам проект."
-msgid "ClusterIntegration|Save changes"
-msgstr ""
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "ПроÑмотреть и отредактировать параметры Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ клаÑтера"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "См. типы машин"
msgid "ClusterIntegration|See your projects"
-msgstr ""
+msgstr "См. ваши проекты"
msgid "ClusterIntegration|See zones"
-msgstr ""
+msgstr "См. зоны"
msgid "ClusterIntegration|Something went wrong on our end."
-msgstr ""
+msgstr " У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
-msgstr ""
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтера в Google Container Engine"
msgid "ClusterIntegration|Toggle Cluster"
-msgstr ""
-
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
+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 ""
+msgstr "ЕÑли привÑзать клаÑтер к Ñтому проекту, вы Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью Ñможете иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ²ÑŒÑŽ, развертывать ваши приложениÑ, запуÑкать Ñборочные линии и многое другое."
msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr ""
+msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_container_engine}"
msgid "ClusterIntegration|Zone"
-msgstr ""
+msgstr "Зона"
msgid "ClusterIntegration|access to Google Container Engine"
-msgstr ""
+msgstr "доÑтуп к Google Container Engine"
msgid "ClusterIntegration|cluster"
-msgstr ""
+msgstr "клаÑтер"
msgid "ClusterIntegration|help page"
-msgstr ""
+msgstr "Ñтраница Ñправки"
msgid "ClusterIntegration|meets the requirements"
-msgstr ""
+msgstr "отвечает требованиÑм"
msgid "ClusterIntegration|properly configured"
-msgstr ""
+msgstr "правильно наÑтроен"
msgid "Comments"
msgstr "Комментарии"
@@ -589,8 +631,14 @@ 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 ""
+msgstr "ОпиÑание Коммита"
msgid "Commit duration in minutes for last 30 commits"
msgstr "ПродолжительноÑÑ‚ÑŒ поÑледних 30 коммитов в минутах"
@@ -602,7 +650,7 @@ msgid "CommitBoxTitle|Commit"
msgstr "Коммит"
msgid "CommitMessage|Add %{file_name}"
-msgstr "Добавлен %{file_name}"
+msgstr "Добавить %{file_name}"
msgid "Commits"
msgstr "Коммиты"
@@ -620,49 +668,49 @@ msgid "Compare"
msgstr "Сравнить"
msgid "Container Registry"
-msgstr ""
+msgstr "РееÑÑ‚Ñ€ Контейнеров"
msgid "ContainerRegistry|Created"
-msgstr ""
+msgstr "Создан"
msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
-msgstr ""
+msgstr "Сначала авторизуйтеÑÑŒ в рееÑтре контейнеров GitLab, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ñвои Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸ пароль. ЕÑли у Ð²Ð°Ñ ÐµÑÑ‚ÑŒ %{link_2fa}, вам необходимо иÑпользовать %{link_token}:"
msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
-msgstr ""
+msgstr "GitLab поддерживает до трех уровней имён образов. Следующие примеры имён образов подходÑÑ‚ Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ проекта:"
msgid "ContainerRegistry|How to use the Container Registry"
-msgstr ""
+msgstr "Как иÑпользовать РееÑÑ‚Ñ€ Контейнеров"
msgid "ContainerRegistry|Learn more about"
-msgstr ""
+msgstr "Узнайте больше"
msgid "ContainerRegistry|No tags in Container Registry for this container image."
-msgstr ""
+msgstr "Ð’ рееÑтре контейнеров нет тегов Ð´Ð»Ñ Ñтого образа."
msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
-msgstr ""
+msgstr "ПоÑле того, как вы вошли в ÑиÑтему, вы можете Ñоздать или загрузить образ контейнера, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð¾Ð±Ñ‰Ð¸Ðµ команды %{build} и %{push}"
msgid "ContainerRegistry|Remove repository"
-msgstr ""
+msgstr "Удалить репозиторий"
msgid "ContainerRegistry|Remove tag"
-msgstr ""
+msgstr "Удалить тег"
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "Размер"
msgid "ContainerRegistry|Tag"
-msgstr ""
+msgstr "Тег"
msgid "ContainerRegistry|Tag ID"
-msgstr ""
+msgstr "Идентификатор Тега"
msgid "ContainerRegistry|Use different image names"
-msgstr ""
+msgstr "ИÑпользовать различные имена образов"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
-msgstr ""
+msgstr "Когда рееÑÑ‚Ñ€ контейнеров Docker интегрирован Ñ GitLab, каждый проект может иметь Ñвое ÑобÑтвенное проÑтранÑтво Ð´Ð»Ñ Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ ÐµÐ³Ð¾ Docker образов."
msgid "Contribution guide"
msgstr "РуководÑтво учаÑтника"
@@ -670,6 +718,12 @@ msgstr "РуководÑтво учаÑтника"
msgid "Contributors"
msgstr "УчаÑтники"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Контролировать макÑимальное количеÑтво потоков фоновой загрузки LFS/вложений Ð´Ð»Ñ Ñтого вторичного узла"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Контролировать макÑимальное количеÑтво потоков фоновой загрузки хранилища Ð´Ð»Ñ Ñтого вторичного узла"
+
msgid "Copy SSH public key to clipboard"
msgstr "Скопировать публичный ключ SSH в буфер обмена"
@@ -691,9 +745,21 @@ msgstr "Создать каталог"
msgid "Create empty bare repository"
msgstr "Создать пуÑтой репозиторий"
+msgid "Create file"
+msgstr "Создать файл"
+
msgid "Create merge request"
msgstr "Создать Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgid "Create new branch"
+msgstr "Создать новую ветку"
+
+msgid "Create new directory"
+msgstr "Создать новый каталог"
+
+msgid "Create new file"
+msgstr "Создать новый файл"
+
msgid "Create new..."
msgstr "Ðовый"
@@ -713,7 +779,7 @@ msgid "Cron syntax"
msgstr "СинтакÑÐ¸Ñ Cron"
msgid "Custom notification events"
-msgstr " ÐаÑтраиваемые ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ ÑобытиÑÑ…"
+msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð½Ð°Ñтраиваемых уведомлений"
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr "ÐаÑтраиваемые уровни уведомлений аналогичны уровню уведомлений в ÑоответÑтвии Ñ ÑƒÑ‡Ð°Ñтием. С наÑтраиваемыми уровнÑми уведомлений вы также будете получать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ выбранных ÑобытиÑÑ…. Чтобы узнать больше, поÑмотрите %{notification_link}."
@@ -746,10 +812,10 @@ msgid "CycleAnalyticsStage|Test"
msgstr "ТеÑтирование"
msgid "DashboardProjects|All"
-msgstr ""
+msgstr "Ð’Ñе"
msgid "DashboardProjects|Personal"
-msgstr ""
+msgstr "Личные"
msgid "Define a custom pattern with cron syntax"
msgstr "Определить наÑтраиваемый шаблон Ñ ÑинтакÑиÑом cron"
@@ -781,8 +847,11 @@ msgstr "Ð˜Ð¼Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ð°"
msgid "Discard changes"
msgstr "Отменить изменениÑ"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Отключить блок Ð²Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð² Ðналитику Цикла"
+
msgid "Dismiss Merge Request promotion"
-msgstr ""
+msgstr "Отключить анонÑÑ‹ Ð´Ð»Ñ Ð—Ð°Ð¿Ñ€Ð¾Ñов на СлиÑние"
msgid "Don't show again"
msgstr "Ðе показывать Ñнова"
@@ -848,17 +917,23 @@ msgid "Every month (on the 1st at 4:00am)"
msgstr "ЕжемеÑÑчно (каждое 1-е чиÑло в 4:00)"
msgid "Every week (Sundays at 4:00am)"
-msgstr "Еженедельно (по воÑкреÑениÑми в 4:00)"
+msgstr "Еженедельно (по воÑкреÑениÑм в 4:00)"
msgid "Explore projects"
msgstr "Обзор проектов"
+msgid "Explore public groups"
+msgstr "ИÑÑледовать публичные группы"
+
msgid "Failed to change the owner"
msgstr "Ðе удалоÑÑŒ изменить владельца"
msgid "Failed to remove the pipeline schedule"
msgstr "Ðе удалоÑÑŒ удалить раÑпиÑание Ñборочной линии"
+msgid "File name"
+msgstr "Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°"
+
msgid "Files"
msgstr "Файлы"
@@ -875,7 +950,7 @@ msgid "FirstPushedBy|First"
msgstr "Первый"
msgid "FirstPushedBy|pushed by"
-msgstr ""
+msgstr "отправлено автором"
msgid "Fork"
msgid_plural "Forks"
@@ -890,7 +965,7 @@ msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr "Ответвление от %{project_name} (удалено)"
msgid "Format"
-msgstr ""
+msgstr "Формат"
msgid "From issue creation until deploy to production"
msgstr "От ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð¾ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ€ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ð¸ в рабочей Ñреде"
@@ -902,13 +977,19 @@ msgid "GPG Keys"
msgstr "GPG Ключи"
msgid "Geo Nodes"
-msgstr ""
+msgstr "ГеографичеÑкие Узлы"
+
+msgid "Geo|File sync capacity"
+msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации файлов"
msgid "Geo|Groups to replicate"
-msgstr ""
+msgstr "Группы Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸"
+
+msgid "Geo|Repository sync capacity"
+msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации репозиториÑ"
msgid "Geo|Select groups to replicate."
-msgstr ""
+msgstr "Выберите группы Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸."
msgid "Git storage health information has been reset"
msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑтабильноÑти Git хранилища была Ñброшена"
@@ -923,31 +1004,76 @@ msgid "GoToYourFork|Fork"
msgstr "Ответвление"
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
-msgstr ""
+msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Google не %{link_to_documentation}. ПопроÑите Ñвоего админиÑтратора GitLab, еÑли вы хотите воÑпользоватьÑÑ Ñтим ÑервиÑом."
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Запретить публикацию проектов из %{group} в других группах"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Опубликовать Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð¾Ð²Ð¾Ð¹ блокировкой"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Эта наÑтройка применена в %{ancestor_group} и была переопределена в Ñтой подгруппе."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Эта наÑтройка применена в %{ancestor_group}. Чтобы поделитьÑÑ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°Ð¼Ð¸ из Ñтой группы Ñ Ð´Ñ€ÑƒÐ³Ð¸Ð¼Ð¸ группами, запроÑите у владельца переопределить Ñту наÑтройку или %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Эта наÑтройка применена в %{ancestor_group}. Ð’Ñ‹ можете переопределить Ñту наÑтройку или %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "Эта наÑтройка будет применена Ð´Ð»Ñ Ð²Ñех подгрупп еÑли не будет переопределена владельцем группы. Группы которые уже имеют доÑтуп к проекту, будут иметь его и дальше пока не будут удалены вручную."
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "не может быть отменена до тех пор пока группа \"ПоделитьÑÑ Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð¾Ð²Ð¾Ð¹ блокировкой\" включена, за иÑключением владельца группы родителÑ"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "удалить возможноÑÑ‚ÑŒ поделитьÑÑ Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð¾Ð²Ð¾Ð¹ блокировкой из %{ancestor_group_name}"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Группа - Ñто набор из неÑкольких проектов."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "При размещении проектов в группе, группа выполнÑет функции папки."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Группы не найдены"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Ð’Ñ‹ можете управлÑÑ‚ÑŒ правами и доÑтупом учаÑтников вашей группы к каждому проекту в группе."
+
+msgid "GroupsTreeRole|as"
+msgstr "как"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Вы уверены, что вы хотите покинуть группу \"${this.group.fullName}\"?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Создать проект в Ñтой группе."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Создать подгруппу в Ñтой группе."
+
+msgid "GroupsTree|Edit group"
+msgstr "Редактировать группу"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Ðе удалоÑÑŒ покинуть группу. ПожалуйÑта, убедитеÑÑŒ, что вы не единÑтвенный владелец."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Фильтр по имени..."
+
+msgid "GroupsTree|Leave this group"
+msgstr "Покинуть Ñту группу"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Загрузка групп"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "К Ñожалению, по вашему запроÑу групп не найдено"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "К Ñожалению, по вашему запроÑу групп или проектов не найдено"
msgid "Health Check"
msgstr "Проверка работоÑпоÑобноÑти"
@@ -968,7 +1094,7 @@ msgid "HealthCheck|Unhealthy"
msgstr "ÐеÑтабильный"
msgid "History"
-msgstr ""
+msgstr "ИÑториÑ"
msgid "Housekeeping successfully started"
msgstr "ОчиÑтка уÑпешно запущена"
@@ -980,7 +1106,7 @@ msgid "Improve Issue boards with GitLab Enterprise Edition."
msgstr "Улучшить доÑки обÑуждений Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ верÑии GitLab Enterprise Edition."
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Улучшить управление обÑуждениÑми Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñтью Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ð²ÐµÑа обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸ помощи GitLab Enterprise Edition."
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr "Улучшить поиÑк при помощи РаÑширенного Глобального ПоиÑка в верÑии GitLab Enterprise Edition."
@@ -994,6 +1120,12 @@ msgstr[0] "ЭкземплÑÑ€"
msgstr[1] "ЭкземплÑры"
msgstr[2] "ЭкземплÑры"
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Внутренний - Группу и включённые в неё проекты может видеть любой зарегиÑтрированный пользователь."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Внутренний - Проект доÑтупен любому зарегиÑтрированному пользователю."
+
msgid "Interval Pattern"
msgstr "Шаблон интервала"
@@ -1040,16 +1172,16 @@ msgid "Last commit"
msgstr "ПоÑледний коммит"
msgid "Last edited %{date}"
-msgstr ""
+msgstr "Дата поÑледнего изменениÑ: %{date}"
msgid "Last edited by %{name}"
-msgstr ""
+msgstr "Ðвтор поÑледнего изменениÑ: %{name}"
msgid "Last update"
-msgstr ""
+msgstr "ПоÑледнее обновление"
msgid "Last updated"
-msgstr ""
+msgstr "ПоÑледний раз обновлено"
msgid "LastPushEvent|You pushed to"
msgstr "Вы отправили в"
@@ -1063,6 +1195,9 @@ msgstr "Узнайте больше в"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "Подробнее в|документации по раÑпиÑаниÑм Ñборочных линий"
+msgid "Leave"
+msgstr "Покинуть"
+
msgid "Leave group"
msgstr "Покинуть группу"
@@ -1074,21 +1209,27 @@ msgstr "ЛицензиÑ"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Показывать %d Ñобытие макÑимум"
+msgstr[1] "Показывать %d ÑÐ¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¼Ð°ÐºÑимум"
+msgstr[2] "Показывать %d Ñобытий макÑимум"
msgid "Lock"
-msgstr ""
+msgstr "Блокировка"
msgid "Locked"
-msgstr ""
+msgstr "Заблокировано"
msgid "Locked Files"
msgstr "Заблокированные Файлы"
+msgid "Login"
+msgstr "Войти"
+
+msgid "Maximum git storage failures"
+msgstr "МакÑимальное количеÑтво Ñбоев хранилища git"
+
msgid "Median"
-msgstr ""
+msgstr "Среднее"
msgid "Members"
msgstr "УчаÑтники"
@@ -1117,11 +1258,14 @@ msgstr "Больше информации доÑтупно|тут"
msgid "Multiple issue boards"
msgstr "Сводные доÑки задач"
+msgid "New Cluster"
+msgstr "Ðовый КлаÑтер"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðовое ОбÑуждение"
-msgstr[1] "Ðовые ОбращениÑ"
-msgstr[2] "Ðовые ОбращениÑ"
+msgstr[1] "Ðовых ОбÑуждениÑ"
+msgstr[2] "Ðовых ОбÑуждений"
msgid "New Pipeline Schedule"
msgstr "Ðовое РаÑпиÑание Сборочной Линии"
@@ -1135,23 +1279,32 @@ msgstr "Ðовый каталог"
msgid "New file"
msgstr "Ðовый файл"
+msgid "New group"
+msgstr "ÐÐ¾Ð²Ð°Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð°"
+
msgid "New issue"
msgstr "Ðовое обÑуждение"
msgid "New merge request"
msgstr "Ðовый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgid "New project"
+msgstr "Ðовый проект"
+
msgid "New schedule"
msgstr "Ðовое раÑпиÑание"
msgid "New snippet"
-msgstr ""
+msgstr "Ðовый фрагмент"
+
+msgid "New subgroup"
+msgstr "ÐÐ¾Ð²Ð°Ñ Ð¿Ð¾Ð´Ð³Ñ€ÑƒÐ¿Ð¿Ð°"
msgid "New tag"
msgstr "Ðовый тег"
msgid "No container images stored for this project. Add one by following the instructions above."
-msgstr ""
+msgstr "Ðет образов контейнеров Ð´Ð»Ñ Ñтого проекта. Добавьте образ, ÑÐ»ÐµÐ´ÑƒÑ Ð¸Ð½ÑтрукциÑм выше."
msgid "No repository"
msgstr "Ðет репозиториÑ"
@@ -1160,7 +1313,7 @@ msgid "No schedules"
msgstr "Ðет раÑпиÑаний"
msgid "None"
-msgstr ""
+msgstr "ПуÑто"
msgid "Not available"
msgstr "ÐедоÑтупно"
@@ -1181,7 +1334,7 @@ msgid "NotificationEvent|Failed pipeline"
msgstr "Ðеудача в Ñборочной линии"
msgid "NotificationEvent|Merge merge request"
-msgstr ""
+msgstr "Влит Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "NotificationEvent|New issue"
msgstr "Ðовое обÑуждение"
@@ -1196,7 +1349,7 @@ msgid "NotificationEvent|Reassign issue"
msgstr "Переназначить обÑуждение"
msgid "NotificationEvent|Reassign merge request"
-msgstr "Переназначить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgstr "Переназначен Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "NotificationEvent|Reopen issue"
msgstr "Переоткрыть обÑуждение"
@@ -1225,17 +1378,23 @@ msgstr "ОтÑлеживать"
msgid "Notifications"
msgstr "УведомлениÑ"
+msgid "Number of access attempts"
+msgstr "КоличеÑтво попыток доÑтупа"
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Фильтр"
msgid "Only project members can comment."
-msgstr ""
+msgstr "Только учаÑтники проекта могут оÑтавлÑÑ‚ÑŒ комментарии."
msgid "OpenedNDaysAgo|Opened"
msgstr "Открыто"
msgid "Opens in a new window"
-msgstr ""
+msgstr "ОткроетÑÑ Ð² новом окне"
msgid "Options"
msgstr "ÐаÑтройки"
@@ -1247,28 +1406,28 @@ msgid "Owner"
msgstr "Владелец"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "ПоÑледнÑÑ Â»"
msgid "Pagination|Next"
-msgstr ""
+msgstr "СледующаÑ"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "ПредыдущаÑ"
msgid "Pagination|« First"
-msgstr ""
+msgstr "« ПерваÑ"
msgid "Password"
msgstr "Пароль"
msgid "People without permission will never get a notification and won\\'t be able to comment."
-msgstr ""
+msgstr "Люди без разрешений не получат уведомление и не Ñмогут комментировать."
msgid "Pipeline"
msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
msgid "Pipeline Health"
-msgstr "Жизненный цикл конвейера"
+msgstr "РаботоÑпоÑобноÑÑ‚ÑŒ Сборочной Линии"
msgid "Pipeline Schedule"
msgstr "РаÑпиÑание Сборочной Линии"
@@ -1366,9 +1525,54 @@ msgstr "Ñо ÑтадиÑми"
msgid "Preferences"
msgstr "ПредпочтениÑ"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Приватный - ДоÑтуп к проекту должен предоÑтавлÑÑ‚ÑŒÑÑ Ñвно каждому пользователю."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "ÐŸÑ€Ð¸Ð²Ð°Ñ‚Ð½Ð°Ñ - Группу и включённые в неё проекты могут видеть только члены группы."
+
msgid "Profile"
msgstr "Профиль"
+msgid "Profiles|Account scheduled for removal."
+msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ запланирована к удалению."
+
+msgid "Profiles|Delete Account"
+msgstr "Удалить Учетную запиÑÑŒ"
+
+msgid "Profiles|Delete account"
+msgstr "Удалить учетную запиÑÑŒ"
+
+msgid "Profiles|Delete your account?"
+msgstr "Удалить Ñвою учетную запиÑÑŒ?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Удаление учетной запиÑи приведет к Ñледующим поÑледÑтвиÑм:"
+
+msgid "Profiles|Invalid password"
+msgstr "Ðеверный пароль"
+
+msgid "Profiles|Invalid username"
+msgstr "Ðеверное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Введите значение %{confirmationValue} Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ прав на удаление Ñтого пользователÑ."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Перед удалением учётной запиÑи, вам необходимо передать право Ð²Ð»Ð°Ð´ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ удалить Ñти группы."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ ÑвлÑетÑÑ Ð²Ð»Ð°Ð´ÐµÐ»ÑŒÑ†ÐµÐ¼ Ñледующих групп:"
+
+msgid "Profiles|your account"
+msgstr "ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "Проект '%{project_name}' находитÑÑ Ð² процеÑÑе удалениÑ."
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' добавлен в очередь на удаление."
@@ -1378,9 +1582,6 @@ msgstr "Проект '%{project_name}' уÑпешно Ñоздан."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проект '%{project_name}' уÑпешно обновлен."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Проект '%{project_name}' удален."
-
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп к проекту должен предоÑтавлÑÑ‚ÑŒÑÑ Ñвно каждому пользователю."
@@ -1424,22 +1625,28 @@ msgid "ProjectNetworkGraph|Graph"
msgstr "Граф"
msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr ""
+msgstr "ОбратитеÑÑŒ к админиÑтратору Ð´Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñтой наÑтройки."
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
-msgstr ""
+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 "Эта наÑтройка будет применена Ð´Ð»Ñ Ð²Ñех проектов, еÑли иное поведение не переопределено админиÑтратором."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
+msgid "Projects"
+msgstr "Проекты"
+
msgid "ProjectsDropdown|Frequently visited"
-msgstr ""
+msgstr "ЧаÑто поÑещаемые"
msgid "ProjectsDropdown|Loading projects"
msgstr "Загрузка проектов"
@@ -1451,7 +1658,7 @@ msgid "ProjectsDropdown|Search your projects"
msgstr "ПоиÑк по вашим проектам"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr "К Ñожалению, по вашему запроÑу проекты не найдены"
@@ -1459,12 +1666,21 @@ msgstr "К Ñожалению, по вашему запроÑу проекты Ð
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Эта функциональноÑÑ‚ÑŒ требует поддержки localStorage в вашем браузере"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Публичный - Группу и включённые в неё проекты могут видеть вÑе, без какой-либо проверки подлинноÑти."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Публичный - ДоÑтуп к проекту возможен без какой-либо проверки подлинноÑти."
+
msgid "Push Rules"
-msgstr ""
+msgstr "Правила Отправки"
msgid "Push events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Подробнее"
@@ -1478,7 +1694,7 @@ msgid "RefSwitcher|Tags"
msgstr "Теги"
msgid "Registry"
-msgstr ""
+msgstr "РееÑÑ‚Ñ€"
msgid "Related Commits"
msgstr "СвÑзанные коммиты"
@@ -1493,10 +1709,10 @@ msgid "Related Jobs"
msgstr "СвÑзанные задачи"
msgid "Related Merge Requests"
-msgstr "СвÑзанные запроÑÑ‹ на ÑлиÑние"
+msgstr "СвÑзанные ЗапроÑÑ‹ на СлиÑние"
msgid "Related Merged Requests"
-msgstr "СвÑзанные объединенные запроÑÑ‹"
+msgstr "СвÑзанные Влитые ЗапроÑÑ‹"
msgid "Remind later"
msgstr "Ðапомнить позже"
@@ -1520,7 +1736,7 @@ msgid "Reset runners registration token"
msgstr "СброÑить ключ региÑтрации Gitlab Runners"
msgid "Revert this commit"
-msgstr "Отменить Ñто изменение"
+msgstr "Отменить Ñто коммит"
msgid "Revert this merge request"
msgstr "Отменить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
@@ -1528,8 +1744,11 @@ msgstr "Отменить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "SSH Keys"
msgstr "SSH Ключи"
+msgid "Save"
+msgstr "Сохранить"
+
msgid "Save changes"
-msgstr ""
+msgstr "Сохранить изменениÑ"
msgid "Save pipeline schedule"
msgstr "Сохранить раÑпиÑание Ñборочной лини"
@@ -1538,7 +1757,7 @@ msgid "Schedule a new pipeline"
msgstr "РаÑпиÑание новой Ñборочной линии"
msgid "Schedules"
-msgstr ""
+msgstr "РаÑпиÑаниÑ"
msgid "Scheduling Pipelines"
msgstr "Планирование Сборочных Линий"
@@ -1546,6 +1765,15 @@ msgstr "Планирование Сборочных Линий"
msgid "Search branches and tags"
msgstr "Ðайти ветки и теги"
+msgid "Seconds before reseting failure information"
+msgstr "Секунд до очиÑтки информации о ÑбоÑÑ…"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Секунд задержки поÑле ÑÐ±Ð¾Ñ Ð² хранилище"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "Секунд задержки между попытками доÑтупа к хранилищу"
+
msgid "Select Archive Format"
msgstr "Выбрать формат архива"
@@ -1571,16 +1799,16 @@ msgid "Set up auto deploy"
msgstr "ÐаÑтройка автоматичеÑкого развертываниÑ"
msgid "SetPasswordToCloneLink|set a password"
-msgstr "уÑтановить пароль"
+msgstr "уÑтановите пароль"
msgid "Settings"
msgstr "ÐаÑтройки"
msgid "Show parent pages"
-msgstr ""
+msgstr "Показать родительÑкие Ñтраницы"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "Показать родительÑкие подгруппы"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -1592,16 +1820,19 @@ msgid "Snippets"
msgstr "Сниппеты"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "Что-то пошло не так при попытке Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²Ð°Ð½Ð½Ð¾Ð³Ð¾ ÑоÑтоÑÐ½Ð¸Ñ Ñтого ${this.issuableDisplayName(this.issuableType)}"
msgid "Something went wrong while fetching the projects."
-msgstr ""
+msgstr "Что-то пошло не так при получении проектов."
msgid "Something went wrong while fetching the registry list."
-msgstr ""
+msgstr "Что-то пошло не так при получении ÑпиÑка рееÑтров."
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr ""
+msgid "Sort by"
+msgstr "Сортировать по"
msgid "SortOptions|Access level, ascending"
msgstr "Уровень доÑтупа, по возраÑтанию"
@@ -1679,7 +1910,7 @@ msgid "SortOptions|Oldest sign in"
msgstr "Старейшие из заходивших"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "Старейших из обновленных"
msgid "SortOptions|Popularity"
msgstr "ПопулÑронÑÑ‚ÑŒ"
@@ -1712,7 +1943,7 @@ msgid "StarProject|Star"
msgstr "Отметить"
msgid "Starred projects"
-msgstr ""
+msgstr "Отмеченные проекты"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
@@ -1720,11 +1951,17 @@ msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
msgid "Start the Runner!"
msgstr "ЗапуÑтить GitLab Runner!"
+msgid "Subgroups"
+msgstr "Подгруппы"
+
+msgid "Subscribe"
+msgstr "ПодпиÑатьÑÑ"
+
msgid "Switch branch/tag"
msgstr "Переключить ветка/тег"
msgid "System Hooks"
-msgstr ""
+msgstr "СиÑтемные Обработчики"
msgid "Tag"
msgid_plural "Tags"
@@ -1742,13 +1979,16 @@ 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 ""
+msgstr "РаÑширенный глобальный поиÑк в GitLab - Ñто Ñерьезный инÑтрумент который Ñокращает ваше времÑ. ВмеÑто ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð´ÑƒÐ±Ð»Ð¸Ñ€ÑƒÑŽÑ‰ÐµÐ³Ð¾ кода и траты времени, вы можете иÑкать код внутри других команд, который поможет вам в вашем проекте."
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr "Порог ÑÑ€Ð°Ð±Ð°Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð¡ircuitBreaker должен быть меньше, чем порог ÑÑ€Ð°Ð±Ð°Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ ÑбоÑ"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
-msgstr "Ðа Ñтапе напиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð´Ð° показывает Ð²Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ коммита до ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние. Данные автоматичеÑки добавÑÑ‚ÑÑ Ð¿Ð¾Ñле того, как вы Ñоздать Ñвой первый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
+msgstr "Этап напиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð´Ð° показывает Ð²Ñ€ÐµÐ¼Ñ Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ коммита до ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние. Данные автоматичеÑки добавÑÑ‚ÑÑ Ñюда поÑле того, как вы Ñоздать Ñвой первый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
msgid "The collection of events added to the data gathered for that stage."
msgstr "ÐšÐ¾Ð»Ð»ÐµÐºÑ†Ð¸Ñ Ñобытий добавленных в данные Ñобранные Ð´Ð»Ñ Ñтого Ñтапа."
@@ -1759,6 +1999,15 @@ msgstr "СвÑзь Ñ Ð¾Ñ‚Ð²ÐµÑ‚Ð²Ð»ÐµÐ½Ð¸ÐµÐ¼ удалена."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "Ð¡Ñ‚Ð°Ð´Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ времÑ, которое потребуетÑÑ Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð¾ Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¾Ð±Ñуждению вехи, или Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð½Ð° вашу доÑку задач. Ðачните Ñоздавать обÑуждениÑ, чтобы увидеть ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñтой Ñтадии."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr "КоличеÑтво попыток, которые GitLab будет предпринимать Ð´Ð»Ñ Ð´Ð¾Ñтупа к хранилищу."
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr "КоличеÑтво ошибок, поÑле которого GitLab начнёт временно отключать доÑтуп к шарду хранилища на хоÑте"
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "КоличеÑтво Ñбоев, поÑле которого Gitlab полноÑтью прекратит доÑтуп к хранилищу. Изменение Ñчётчика \"количеÑтво Ñбоев\" может быть произведено через админиÑтративный интерфейÑ: %{link_to_health_page} или при помощи API %{api_documentation_link}."
+
msgid "The phase of the development lifecycle."
msgstr "Фаза жизненного цикла разработки."
@@ -1766,7 +2015,7 @@ 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 "ПроизводÑтвенный Ñтап показывает общее Ð²Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ Ñозданием обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸ развертыванием кода в продуктивной Ñреде. Данные будут автоматичеÑки добавлены поÑле полного Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð¸Ð´ÐµÐ¸."
@@ -1789,6 +2038,12 @@ msgstr "Этап поÑтановки показывает Ð²Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "Этап теÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ времÑ, которое GitLab CI занимает Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка каждой Ñборочной линии Ð´Ð»Ñ ÑоответÑтвующего запроÑа на ÑлиÑние. Данные будут автоматичеÑки добавлены поÑле Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ вашей первой Ñборочной линии."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð² Ñекундах, в течение которого GitLab будет хранить информацию о ÑбоÑÑ…. ЕÑли в течение Ñтого времени не было Ñбоев, Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ монтировании очищаетÑÑ."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð² Ñекундах в течении которого GitLab будет пытатьÑÑ Ð¿Ð¾Ð»ÑƒÑ‡Ð¸Ñ‚ÑŒ доÑтуп к хранилищу. ПоÑле Ñтого времени будет зафикÑирована ошибка Ð¿Ñ€ÐµÐ²Ñ‹ÑˆÐµÐ½Ð¸Ñ Ð²Ñ€ÐµÐ¼ÐµÐ½Ð¸ ожиданиÑ."
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "ВремÑ, затраченное каждым Ñлементом, Ñобранным на Ñтом Ñтапе."
@@ -1798,11 +2053,14 @@ msgstr "Среднее значение в Ñ€Ñду. Пример: между 3,
msgid "There are problems accessing Git storage: "
msgstr "Проблемы Ñ Ð´Ð¾Ñтупом к Git хранилищу: "
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Эта ветка была изменена, пока вы её редактировали. Ð’Ñ‹ хотите Ñоздать новую ветку?"
+
msgid "This is a confidential issue."
msgstr "Это конфиденциальное обÑуждение."
msgid "This is the author's first Merge Request to this project."
-msgstr ""
+msgstr "Это первый Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние от автора в Ñтот проект."
msgid "This issue is confidential and locked."
msgstr "Это обÑуждение конфиденциально и заблокировано."
@@ -1814,7 +2072,7 @@ msgid "This means you can not push code until you create an empty repository or
msgstr "Это означает, что вы не можете отправить код, пока не Ñоздадите пуÑтой репозиторий или не импортируете ÑущеÑтвующий."
msgid "This merge request is locked."
-msgstr ""
+msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние заблокирован."
msgid "Time before an issue gets scheduled"
msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала Ð¿Ð¾Ð¿Ð°Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð² планировщик"
@@ -1946,7 +2204,7 @@ msgid "Timeago|in 1 year"
msgstr "через год"
msgid "Timeago|in a while"
-msgstr ""
+msgstr "через некоторое времÑ"
msgid "Timeago|less than a minute ago"
msgstr "менее чем минуту назад"
@@ -1973,25 +2231,28 @@ msgid "Total test time for all commits/merges"
msgstr "Общее Ð²Ñ€ÐµÐ¼Ñ Ñ‚ÐµÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„Ð¸ÐºÑаций/ÑлиÑний"
msgid "Track activity with Contribution Analytics."
-msgstr ""
+msgstr "ОтÑлеживать активноÑÑ‚ÑŒ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Ðналитики УчаÑтников."
msgid "Unlock"
-msgstr ""
+msgstr "Разблокировать"
msgid "Unlocked"
-msgstr ""
+msgstr "Разблокировано"
msgid "Unstar"
msgstr "СнÑÑ‚ÑŒ отметку"
+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 "Обновите ваш тарифный план Ð´Ð»Ñ Ð¿Ð¾ÑÐ²Ð»ÐµÐ½Ð¸Ñ Ð²ÐµÑа у обÑуждений."
@@ -2015,13 +2276,13 @@ msgid "Use your global notification setting"
msgstr "ИÑпользуютÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ‹Ð¹ наÑтройки уведомлений"
msgid "View file @ "
-msgstr ""
+msgstr "ПроÑмотр файла @ "
msgid "View open merge request"
msgstr "ПроÑмотреть открытый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "View replaced file @ "
-msgstr ""
+msgstr "ПроÑмотр заменённого файла @ "
msgid "VisibilityLevel|Internal"
msgstr "Ограниченный"
@@ -2045,34 +2306,37 @@ msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed o
msgstr "Веб-обработчики позволÑÑŽÑ‚ вам вызывать Ð°Ð´Ñ€ÐµÑ URL еÑли, например, отправлен новый код или Ñоздано новое обÑуждение. Ð’Ñ‹ можете наÑтроить веб-обработчики так, чтобы они реагировали на определённые ÑобытиÑ, такие как отправки кода, обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ запроÑÑ‹ на ÑлиÑние. Групповые веб-обработчики применÑÑŽÑ‚ÑÑ ÐºÐ¾ вÑем проектам в группе и позволÑÑŽÑ‚ вам Ñтандартизовать функциональноÑÑ‚ÑŒ веб-обработчиков Ð´Ð»Ñ Ð²Ñей вашей группы."
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 приоÑтановит доÑтуп к хранилищу на времÑ, указанное здеÑÑŒ. Это позволит файловой ÑиÑтеме воÑÑтановитьÑÑ. Репозитории на Ñбойных \"шардах\" будут временно недоÑтупны"
msgid "Wiki"
msgstr "Wiki"
msgid "WikiClone|Clone your wiki"
-msgstr ""
+msgstr "Клонировать wiki"
msgid "WikiClone|Git Access"
-msgstr ""
+msgstr "ДоÑтуп через Git"
msgid "WikiClone|Install Gollum"
-msgstr ""
+msgstr "УÑтановка Gollum"
msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
-msgstr ""
+msgstr "РекомендуетÑÑ ÑƒÑтановить %{markdown}, чтобы возможноÑти GFM отображалиÑÑŒ локально:"
msgid "WikiClone|Start Gollum and edit locally"
-msgstr ""
+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 ""
+msgstr "Это уÑÑ‚Ð°Ñ€ÐµÐ²ÑˆÐ°Ñ Ð²ÐµÑ€ÑÐ¸Ñ Ñтой Ñтраницы."
msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
-msgstr ""
+msgstr "Ð’Ñ‹ можете увидеть %{most_recent_link} либо проÑмотреть %{history_link}."
msgid "WikiHistoricalPage|history"
msgstr "иÑториÑ"
@@ -2114,7 +2378,7 @@ msgid "WikiPageEdit|Update %{page_title}"
msgstr "Обновить %{page_title}"
msgid "WikiPage|Page slug"
-msgstr ""
+msgstr "СемантичеÑкий Url Ð´Ð»Ñ Ñтраницы"
msgid "WikiPage|Write your content or drag files here..."
msgstr "Ðапишите ваше Ñодержимое или перетащите Ñюда файлы..."
@@ -2150,7 +2414,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 "Отменить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа"
@@ -2167,9 +2431,21 @@ msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить ÑвÑзь ответвлен
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ передать проект %{project_name_with_namespace} другому владельцу. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
+msgid "You are on a read-only GitLab instance."
+msgstr "Ð’Ñ‹ находитеÑÑŒ на ÑкземплÑре \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Ð’Ñ‹ находитеÑÑŒ на ÑкземплÑре \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab. ЕÑли вы хотите произвеÑти любые изменениÑ, вы должны перейти на \"оÑновной\" ÑкземплÑÑ€ по ÑÑылке %{link_to_primary_node}."
+
msgid "You can only add files when you are on a branch"
msgstr "Ð’Ñ‹ можете добавлÑÑ‚ÑŒ только файлы, когда находитеÑÑŒ в ветке"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Ð’Ñ‹ не можете запиÑывать на подчиненные ÑкземплÑры \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab Geo. ИÑпользуйте вмеÑто Ñтого %{link_to_primary_node}."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Ð’Ñ‹ не можете запиÑывать на Ñтот ÑкземплÑÑ€ \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab."
+
msgid "You have reached your project limit"
msgstr "Ð’Ñ‹ доÑтигли Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð² вашем проекте"
@@ -2195,13 +2471,16 @@ msgid "You will receive notifications only for comments in which you were @menti
msgstr "Ð’Ñ‹ будете получать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ð¸ÐµÐ², в которых вы были @упомÑнуты"
msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
-msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код проекта через %{protocol} пока %{set_password_link} в ваш аккаунт"
+msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код в данный проект через %{protocol} пока не %{set_password_link} в вашей учетной запиÑи"
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код проекта через SSH пока %{add_ssh_key_link} в ваш профиль."
msgid "Your comment will not be visible to the public."
-msgstr ""
+msgstr "Ваш комментарий не будет виден вÑем."
+
+msgid "Your groups"
+msgstr "Ваши группы"
msgid "Your name"
msgstr "Ваше имÑ"
@@ -2210,7 +2489,7 @@ msgid "Your projects"
msgstr "Ваши проекты"
msgid "commit"
-msgstr ""
+msgstr "коммит"
msgid "day"
msgid_plural "days"
@@ -2230,9 +2509,15 @@ msgstr[0] "иÑточник"
msgstr[1] "иÑточники"
msgstr[2] "иÑточники"
-msgid "to help your contributors communicate effectively!"
-msgstr ""
+msgid "password"
+msgstr "пароль"
msgid "personal access token"
-msgstr ""
+msgstr "токен Ð´Ð»Ñ Ð¿ÐµÑ€Ñонального доÑтупа"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "чтобы помочь вашим учаÑтникам взаимодейÑтвовать Ñффективнее!"
+
+msgid "username"
+msgstr "Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 62f4d4cbf2e..cf51f8ec689 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:37-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-05 08:38-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -24,9 +24,9 @@ msgstr[2] "%d коммітів"
msgid "%d layer"
msgid_plural "%d layers"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "%d шар"
+msgstr[1] "%d шари"
+msgstr[2] "%d шарів"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -37,6 +37,12 @@ msgstr[2] "%s доданих коммітів були виключені длÑ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} комміт %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "на %{number_commits_behind} коммітів позаду %{default_branch}, на %{number_commits_ahead} коммітів попереду"
@@ -58,6 +64,12 @@ msgstr[2] "%{storage_name}: %{failed_attempts} невдалих Ñпроб доÑ
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдіть за поÑиланнÑм %{link} Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— ÑтоÑовно вÑтановленнÑ)."
+msgid "+ %{moreCount} more"
+msgstr "+ ще %{moreCount}"
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 конвеєр"
@@ -68,7 +80,7 @@ msgid "1st contribution!"
msgstr "Перший внеÑок!"
msgid "2FA enabled"
-msgstr ""
+msgstr "Двоетапна Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ ÑƒÐ²Ñ–Ð¼ÐºÐ½ÐµÐ½Ð°"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Це набір графічних елементів Ð´Ð»Ñ Ð±ÐµÐ·Ð¿ÐµÑ€ÐµÑ€Ð²Ð½Ð¾Ñ— інтеграції"
@@ -113,11 +125,20 @@ msgid "Add an SSH key to your profile to pull or push via SSH."
msgstr "Додати SSH ключа в Ñвій профіль, щоб мати можливіÑÑ‚ÑŒ завантажити чи надіÑлати зміни через SSH."
msgid "Add new directory"
-msgstr ""
+msgstr "Додати новий каталог"
+
+msgid "AdminHealthPageLink|health page"
+msgstr "Ñторінка ÑтатуÑу"
+
+msgid "Advanced settings"
+msgstr "Додаткові параметри"
msgid "All"
msgstr "Ð’ÑÑ–"
+msgid "An error occurred. Please try again."
+msgstr "СталаÑÑŒ помилка. Спробуйте ще раз."
+
msgid "Appearance"
msgstr "Зовнішній виглÑд"
@@ -133,6 +154,9 @@ msgstr "Ви впевнені, що хочете видалити цей розÐ
msgid "Are you sure you want to discard your changes?"
msgstr "Ви впевнені, що бажаєте ÑкаÑувати ваші зміни?"
+msgid "Are you sure you want to leave this group?"
+msgstr "Ви впевнені що хочете залишити цю групу?"
+
msgid "Are you sure you want to reset registration token?"
msgstr "Ви впевнені, що бажаєте Ñкинути реєÑтраційний токен?"
@@ -155,29 +179,32 @@ msgid "Author"
msgstr "Ðвтор"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхідно вказати доменне Ñ–Ð¼â€™Ñ Ñ‚Ð° %{kubernetes}."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхідно вказати доменне ім’Ñ."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхіден %{kubernetes}."
msgid "AutoDevOps|Auto DevOps (Beta)"
-msgstr ""
-
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr "Auto DevOps може бути активований Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту. Він буде автоматично Ñтворювати, теÑтувати Ñ– розгортати ваш додаток на оÑнові налаштованої конфігурації CI / CD."
+msgstr "Auto DevOps (бета)"
msgid "AutoDevOps|Auto DevOps documentation"
-msgstr ""
+msgstr "Auto DevOps документаціÑ"
msgid "AutoDevOps|Enable in settings"
msgstr "Включити в налаштуваннÑÑ…"
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "AutoDevOps буде автоматично збирати, теÑтувати та розгортати вашу програму на оÑнові визначеної CI/CD конфігурації."
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в %{link_to_documentation}"
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Ви можете активувати %{link_to_settings} Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
+
msgid "Billing"
msgstr "Білінг"
@@ -185,7 +212,7 @@ 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,7 +248,7 @@ msgid "BillingPlans|You are currently on the %{plan_link} plan."
msgstr "Зараз ви викориÑтовуєте план %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "ЧаÑÑ‚Ñ– питаннÑ"
msgid "BillingPlans|monthly"
msgstr "щоміÑÑцÑ"
@@ -241,6 +268,9 @@ msgstr[2] "Гілок"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "Гілка <strong>%{branch_name}</strong> Ñтворена. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичного Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð²Ð¸Ð±ÐµÑ€Ñ–Ñ‚ÑŒ GitLab CI Yaml-шаблон Ñ– закоммітьте зміни. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr "Гілка змінилаÑÑŒ"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Пошук гілок"
@@ -272,28 +302,28 @@ msgid "Branches|Delete protected branch '%{branch_name}'?"
msgstr "Видалити захищену гілку \"%{branch_name}\"?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð³Ñ–Ð»ÐºÐ¸ '%{branch_name}' неможливо буде ÑкаÑувати. Ви впевнені?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð»Ð¸Ñ‚Ð¸Ñ… гілок неможливо буде ÑкаÑувати. Ви впевнені?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "Фільтрувати за назвою гілки"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "Злито в %{default_branch}"
msgid "Branches|New branch"
-msgstr ""
+msgstr "Ðова гілка"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "Ðемає гілок Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr "Як тільки ви підтвердите Ñ– натиÑнете %{delete_protected_branch}, дані будуть втрачені, Ñ– Ñ—Ñ… не можливо буде відновити."
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "Тільки керівник або влаÑник проекту може видалити захищену гілку"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
msgstr "УправлÑти захищеними гілками можливо в %{project_settings_link}"
@@ -302,13 +332,13 @@ msgid "Branches|Sort by"
msgstr "Сортувати за"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "Гілка не може бути оновлена автоматично, тому що вона має розбіжноÑÑ‚Ñ– із upstream."
msgid "Branches|The default branch cannot be deleted"
msgstr "Гілка \"за замовчуваннÑм\" не може бути видалена"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° не була злита в %{default_branch}."
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
msgstr "Щоб уникнути втрати даних, розглÑньте можливіÑÑ‚ÑŒ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ†Ñ–Ñ”Ñ— гілки перед Ñ—Ñ— видаленнÑм."
@@ -317,16 +347,16 @@ 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 "Щоб відхилити локальні зміни Ñ– перезапиÑати гілку верÑією з upstream, видаліть Ñ—Ñ— тут Ñ– виберіть \"Оновити зараз\" вище."
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ захищену гілку %{branch_name}."
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "розходитьÑÑ Ð· upstream"
msgid "Branches|merged"
-msgstr ""
+msgstr "злита"
msgid "Branches|project settings"
msgstr "ÐаÑтройки проекту"
@@ -356,7 +386,7 @@ msgid "CI configuration"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
msgid "CICD|Jobs"
-msgstr ""
+msgstr "ЗавданнÑ"
msgid "Cancel"
msgstr "СкаÑувати"
@@ -365,7 +395,7 @@ msgid "Cancel edit"
msgstr "Відмінити правку"
msgid "Change Weight"
-msgstr ""
+msgstr "Вага зміни"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Вибрати в гілці"
@@ -395,7 +425,7 @@ msgid "Cherry-pick this merge request"
msgstr "Cherry-pick в цьому запиті на злиттÑ"
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
-msgstr ""
+msgstr "Виберіть, Ñкі групи ви хочете реплікувати на цю вторинну ноду. Залиште порожнім, щоб реплікувати вÑе."
msgid "CiStatusLabel|canceled"
msgstr "ÑкаÑовано"
@@ -451,134 +481,146 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "виконуєтьÑÑ"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "circuitbreaker api"
+
msgid "Clone repository"
-msgstr ""
+msgstr "Клонувати репозиторій"
msgid "Close"
msgstr "Закрити"
+msgid "Cluster"
+msgstr "КлаÑтер"
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr ""
+msgstr "%{link_to_container_project} напевно було Ñтворено під цим обліковим запиÑом"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Параметри клаÑтера"
msgid "ClusterIntegration|Cluster integration"
-msgstr ""
+msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером"
msgid "ClusterIntegration|Cluster integration is disabled for this project."
-msgstr ""
+msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером вимкнена Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
msgid "ClusterIntegration|Cluster integration is enabled for this project."
-msgstr ""
+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 ""
+msgstr "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту увімкнена Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером. Ð’Ð¸ÐºÐ½ÐµÐ½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— не вплине на клаÑтер, але з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ GitLab з ним буде тимчаÑово розірване."
msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr ""
+msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..."
msgid "ClusterIntegration|Cluster name"
-msgstr ""
+msgstr "Ім'Ñ ÐºÐ»Ð°Ñтера"
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr ""
+msgstr "КлаÑтер був уÑпішно Ñтворений в Google Container Engine"
msgid "ClusterIntegration|Copy cluster name"
-msgstr ""
+msgstr "Копіювати назву клаÑтера"
msgid "ClusterIntegration|Create cluster"
-msgstr ""
+msgstr "Створити клаÑтер"
msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr ""
+msgstr "Створити новий клаÑтер в Google Container Engine"
msgid "ClusterIntegration|Enable cluster integration"
-msgstr ""
+msgstr "Увімкнути інтеграцію із клаÑтерами"
msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr ""
+msgstr "Ідентифікатор проекту в Google Cloud Platform"
msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Google Container Engine"
msgid "ClusterIntegration|Google Container Engine project"
-msgstr ""
-
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Проект Google Container Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr ""
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{link_to_documentation}"
-msgid "ClusterIntegration|See machine types"
-msgstr ""
+msgid "ClusterIntegration|Machine type"
+msgstr "Тип машини"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
-msgstr ""
+msgstr "ПереконайтеÑÑ, що ваш обліковий Ð·Ð°Ð¿Ð¸Ñ %{link_to_requirements} Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтерів"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ”ÑŽ із клаÑтером у вашому Gitlab-проекті."
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "Ð”Ð»Ñ ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñвоїм клаÑтером перейдіть на %{link_gke}"
msgid "ClusterIntegration|Number of nodes"
-msgstr ""
+msgstr "КількіÑÑ‚ÑŒ вузлів"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "Будь-лаÑка впевнітьÑÑ, що ваш Google-аккаунт задовольнÑÑ” наÑтупним вимогам:"
msgid "ClusterIntegration|Project namespace (optional, unique)"
-msgstr ""
+msgstr "Namespace проекту (не обов’Ñзковий, унікальний)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Прочитайте нашу документацію %{link_to_help_page} по інтеграції із клаÑтером."
msgid "ClusterIntegration|Remove cluster integration"
-msgstr ""
+msgstr "Видалити інтеграцію з клаÑтером"
msgid "ClusterIntegration|Remove integration"
-msgstr ""
+msgstr "Видалити інтеграцію"
msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr ""
+msgstr "При видаленні інтеграції з клаÑтером буде видалена ÐºÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ ÐºÐ»Ð°Ñтера, Ñку ви додали в цей проект. Дана Ð´Ñ–Ñ Ð½Ðµ видалить Ñам проект."
-msgid "ClusterIntegration|Save changes"
-msgstr ""
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "ПереглÑнути та редагувати параметри вашого клаÑтера"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "ПереглÑнути типи машин"
msgid "ClusterIntegration|See your projects"
-msgstr ""
+msgstr "ПереглÑнути ваші проекти"
msgid "ClusterIntegration|See zones"
-msgstr ""
+msgstr "ПереглÑнути зони"
msgid "ClusterIntegration|Something went wrong on our end."
-msgstr ""
-
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
+msgstr "ЩоÑÑŒ пішло не так з нашого боку."
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
-msgstr ""
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в Google Container Engine"
msgid "ClusterIntegration|Toggle Cluster"
-msgstr ""
-
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
+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 ""
+msgstr "За допомогою підключеного до цього проекту клаÑтера, ви можете викориÑтовувати Review Apps, розгортати ваші проекти, запуÑкати конвеєри збірки та багато іншого."
msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr ""
+msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ мати %{link_to_container_engine}"
msgid "ClusterIntegration|Zone"
-msgstr ""
+msgstr "Зона"
msgid "ClusterIntegration|access to Google Container Engine"
-msgstr ""
+msgstr "доÑтуп до Google Container Engine"
msgid "ClusterIntegration|cluster"
-msgstr ""
+msgstr "клаÑтер"
msgid "ClusterIntegration|help page"
-msgstr ""
+msgstr "Ñторінка допомоги"
msgid "ClusterIntegration|meets the requirements"
-msgstr ""
+msgstr "задовольнÑÑ” вимогам"
msgid "ClusterIntegration|properly configured"
-msgstr ""
+msgstr "правильно налаштований"
msgid "Comments"
msgstr "Коментарі"
@@ -589,8 +631,14 @@ 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 ""
+msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼Ñ–Ñ‚Ñƒ"
msgid "Commit duration in minutes for last 30 commits"
msgstr "ТриваліÑÑ‚ÑŒ оÑтанніх 30 коммітів у хвилинах"
@@ -620,49 +668,49 @@ msgid "Compare"
msgstr "ПорівнÑти"
msgid "Container Registry"
-msgstr ""
+msgstr "РеєÑÑ‚Ñ€ Контейнерів"
msgid "ContainerRegistry|Created"
-msgstr ""
+msgstr "Створений"
msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
-msgstr ""
+msgstr "Спочатку увійдіть до реєÑтру контейнерів GitLab, викориÑтовуючи логін та пароль. Якщо у Ð²Ð°Ñ %{link_2fa}, треба викориÑтовувати %{link_token}:"
msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
-msgstr ""
+msgstr "GitLab підтримує до 3 рівнів імен образів. ÐаÑтупні приклади образів Ñ” правильними Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проекту:"
msgid "ContainerRegistry|How to use the Container Registry"
-msgstr ""
+msgstr "Як викориÑтовувати РеєÑÑ‚Ñ€ Контейнерів"
msgid "ContainerRegistry|Learn more about"
-msgstr ""
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про"
msgid "ContainerRegistry|No tags in Container Registry for this container image."
-msgstr ""
+msgstr "Ð’ РеєÑтрі Контейнерів немає тегів Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ образу."
msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
-msgstr ""
+msgstr "ПіÑÐ»Ñ Ð²Ñ…Ð¾Ð´Ñƒ ви можете Ñтворювати та завантажувати образи контейнерів, викориÑтовуючи звичайні %{build} та %{push} команди"
msgid "ContainerRegistry|Remove repository"
-msgstr ""
+msgstr "Видалити репозиторій"
msgid "ContainerRegistry|Remove tag"
-msgstr ""
+msgstr "Видалити тег"
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "Розмір"
msgid "ContainerRegistry|Tag"
-msgstr ""
+msgstr "Тег"
msgid "ContainerRegistry|Tag ID"
-msgstr ""
+msgstr "Тег ID"
msgid "ContainerRegistry|Use different image names"
-msgstr ""
+msgstr "ВикориÑтовуйте різні імена образів"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
-msgstr ""
+msgstr "За допомогою вбудованого в GitLab реєÑтру Docker контейнерів кожен проект може мати влаÑне міÑце Ð´Ð»Ñ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Docker образів."
msgid "Contribution guide"
msgstr "Керівництво контриб’юторів"
@@ -670,6 +718,12 @@ msgstr "Керівництво контриб’юторів"
msgid "Contributors"
msgstr "Контриб’ютори"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Задати макÑимальну кількіÑÑ‚ÑŒ потоків Ð´Ð»Ñ Ñ„Ð¾Ð½Ð¾Ð²Ð¾Ð³Ð¾ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ LFS/вкладень Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ вторинного вузла"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Задати макÑимальну кількіÑÑ‚ÑŒ потоків Ð´Ð»Ñ Ñ„Ð¾Ð½Ð¾Ð²Ð¾Ð³Ð¾ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ—Ð² Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ вторинного вузла"
+
msgid "Copy SSH public key to clipboard"
msgstr "Скопіюйте відкритий SSH-ключ в буфер обміну"
@@ -691,9 +745,21 @@ msgstr "Створити каталог"
msgid "Create empty bare repository"
msgstr "Створити порожній репозиторій"
+msgid "Create file"
+msgstr "Створити файл"
+
msgid "Create merge request"
msgstr "Створити запит на злиттÑ"
+msgid "Create new branch"
+msgstr "Створити нову гілку"
+
+msgid "Create new directory"
+msgstr "Створити новий каталог"
+
+msgid "Create new file"
+msgstr "Створити новий файл"
+
msgid "Create new..."
msgstr "Створити..."
@@ -781,6 +847,9 @@ msgstr "Ім'Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ñƒ"
msgid "Discard changes"
msgstr "СкаÑувати зміни"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Відмінити блок вÑтупу до Ðналитики Циклу"
+
msgid "Dismiss Merge Request promotion"
msgstr "Ðе показувати промоушн запитів на злиттÑ"
@@ -824,22 +893,22 @@ msgid "Emails"
msgstr "ÐдреÑи електронної пошти"
msgid "EventFilterBy|Filter by all"
-msgstr "Ð’ÑÑ–"
+msgstr "Фільтрувати по вÑім"
msgid "EventFilterBy|Filter by comments"
-msgstr "Коментарю"
+msgstr "Фільтрувати по коментарÑм"
msgid "EventFilterBy|Filter by issue events"
-msgstr "Проблеми"
+msgstr "Фільтрувати по проблемах"
msgid "EventFilterBy|Filter by merge events"
-msgstr "Запити на злиттÑ"
+msgstr "Фільтрувати по запитам на злиттÑ"
msgid "EventFilterBy|Filter by push events"
-msgstr "По відправленні комміту"
+msgstr "Фільтрувати по push-подіÑÑ…"
msgid "EventFilterBy|Filter by team"
-msgstr "За командою"
+msgstr "Фільтрувати по команді"
msgid "Every day (at 4:00am)"
msgstr "Кожен день (в 4:00 ранку)"
@@ -853,12 +922,18 @@ msgstr "Ð©Ð¾Ñ‚Ð¸Ð¶Ð½Ñ (в неділю о 4:00 ранку)"
msgid "Explore projects"
msgstr "ОглÑд проектів"
+msgid "Explore public groups"
+msgstr "ПереглÑнути публічні групи"
+
msgid "Failed to change the owner"
msgstr "Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ влаÑника"
msgid "Failed to remove the pipeline schedule"
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ розклад Конвеєра"
+msgid "File name"
+msgstr "Ім'Ñ Ñ„Ð°Ð¹Ð»Ñƒ"
+
msgid "Files"
msgstr "Файли"
@@ -904,9 +979,15 @@ msgstr "GPG ключі"
msgid "Geo Nodes"
msgstr "Гео-Вузли"
+msgid "Geo|File sync capacity"
+msgstr "ПропуÑкна здатніÑÑ‚ÑŒ Ñинхронізації файлів"
+
msgid "Geo|Groups to replicate"
msgstr "Групи Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ—"
+msgid "Geo|Repository sync capacity"
+msgstr "ПропуÑкна здатніÑÑ‚ÑŒ Ñинхронізації репозиторіїв"
+
msgid "Geo|Select groups to replicate."
msgstr "Виберіть групи Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ—."
@@ -923,7 +1004,7 @@ msgid "GoToYourFork|Fork"
msgstr "Форк"
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
-msgstr ""
+msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ Google не %{link_to_documentation}. ПопроÑÑ–Ñ‚ÑŒ Ñвого адмініÑтратора GitLab, Ñкщо ви хочете ÑкориÑтатиÑÑ Ñ†Ð¸Ð¼ ÑервіÑом."
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "Заборонити Ñпільний доÑтуп до проекту в рамках %{group} з іншими групами"
@@ -932,13 +1013,13 @@ msgid "GroupSettings|Share with group lock"
msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñпільного доÑтупу з іншими групами"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Цей параметр заÑтоÑовано до %{ancestor_group} Ñ– його було перевизначено в цій підгрупі."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Цей параметр заÑтоÑовано до %{ancestor_group}. Щоб поділитиÑÑ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°Ð¼Ð¸ з цієї групи з іншою групою, попроÑÑ–Ñ‚ÑŒ влаÑника перевизначити параметр або %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Цей параметр заÑтоÑовано до %{ancestor_group}. Ви можете перевизначити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
msgstr "Цей параметр буде заÑтоÑовано до вÑÑ–Ñ… підгруп, Ñкщо тільки не буде перевизначено влаÑником групи. Групи, Ñкі вже мають доÑтуп до проекту, будуть мати доÑтуп, Ñкщо вони не будуть вилучені вручну."
@@ -949,6 +1030,51 @@ msgstr "не може бути ÑкаÑовано поки \"БлокуваннÑ
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "Видалити Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñпільного доÑтупу з іншими групами з %{ancestor_group_name}"
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Група — набір із декількох проектів."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "Ð Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð² у групі Ñхоже на Ñ€Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð² папці."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Групи не знайдені"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Ви можете керувати правами доÑтупу членів групи мати доÑтуп до кожного проекту в ній."
+
+msgid "GroupsTreeRole|as"
+msgstr "Ñк"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Ви впевнені, що хочете залишити групу \"${this.group.fullName}\"?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Створити проект у групі."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Створити підгрупи у цій групі."
+
+msgid "GroupsTree|Edit group"
+msgstr "Редагувати групу"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Ðе вдалоÑÑ Ð·Ð°Ð»Ð¸ÑˆÐ¸Ñ‚Ð¸ групу. Будь-лаÑка впевнітьÑÑ, що ви не єдитий влаÑник."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Фільтрувати за іменем…"
+
+msgid "GroupsTree|Leave this group"
+msgstr "Залишити цю группу"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "Ðа жаль жодна группа не задовольнÑÑ” параметрам вашого запиту"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "Ðа жаль жодна группа чи проект не задовольнÑÑ” параметрам вашого запиту"
+
msgid "Health Check"
msgstr "Перевірки працездатноÑÑ‚Ñ–"
@@ -994,6 +1120,12 @@ msgstr[0] "ІнÑтанÑ"
msgstr[1] "ІнÑтанÑа"
msgstr[2] "ІнÑтанÑів"
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Ð’Ð½ÑƒÑ‚Ñ€Ñ–ÑˆÐ½Ñ â€” будь-Ñкий автентифікований кориÑтувач має доÑтуп до цієї групи та уÑÑ–Ñ… Ñ—Ñ— внутрішніх проектів."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Внутрішній — будь-Ñкий автентифікований кориÑтувач має доÑтуп до цього проекту."
+
msgid "Interval Pattern"
msgstr "Шаблон інтервалу"
@@ -1007,7 +1139,7 @@ msgid "Issue boards with milestones"
msgstr "Дошка обговорень із етапами"
msgid "Issue events"
-msgstr "Події проблем"
+msgstr "Проблеми"
msgid "IssueBoards|Board"
msgstr "Дошка"
@@ -1049,7 +1181,7 @@ msgid "Last update"
msgstr "ОÑтаннє оновленнÑ"
msgid "Last updated"
-msgstr ""
+msgstr "ВоÑтаннє оновленно"
msgid "LastPushEvent|You pushed to"
msgstr "Ви надіÑлали зміни до"
@@ -1063,6 +1195,9 @@ msgstr "ДізнайтеÑÑŒ більше"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "Детальніше в документації по розкладами конвеєрів"
+msgid "Leave"
+msgstr "Вийти"
+
msgid "Leave group"
msgstr "Залишити групу"
@@ -1087,6 +1222,12 @@ msgstr "Заблоковано"
msgid "Locked Files"
msgstr "Заблоковані файли"
+msgid "Login"
+msgstr "Вхід"
+
+msgid "Maximum git storage failures"
+msgstr "МакÑимальна кількіÑÑ‚ÑŒ невдач в Ñховищі даних git"
+
msgid "Median"
msgstr "Медіана"
@@ -1094,10 +1235,10 @@ msgid "Members"
msgstr "КориÑтувачі"
msgid "Merge Requests"
-msgstr "Запит на злиттÑ"
+msgstr "Запити на злиттÑ"
msgid "Merge events"
-msgstr "Події запит на злиттÑ"
+msgstr "Запити на злиттÑ"
msgid "Merge request"
msgstr "Запит на злиттÑ"
@@ -1117,6 +1258,9 @@ msgstr "тут"
msgid "Multiple issue boards"
msgstr "Зведені дошки обговореннÑ"
+msgid "New Cluster"
+msgstr "Ðовий клаÑтер"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðова проблема"
@@ -1135,23 +1279,32 @@ msgstr "Ðовий каталог"
msgid "New file"
msgstr "Ðовий файл"
+msgid "New group"
+msgstr "Ðова група"
+
msgid "New issue"
msgstr "Ðова проблема"
msgid "New merge request"
msgstr "Ðовий запит на злиттÑ"
+msgid "New project"
+msgstr "Ðовий проект"
+
msgid "New schedule"
msgstr "Ðовий Розклад"
msgid "New snippet"
msgstr "Ðовий Ñніппет"
+msgid "New subgroup"
+msgstr "Ðова підгрупа"
+
msgid "New tag"
msgstr "Ðовий тег"
msgid "No container images stored for this project. Add one by following the instructions above."
-msgstr ""
+msgstr "Ð’ цьому проекті немає жодного образа контейнера. Додайте його за інÑтрукціÑми вище."
msgid "No repository"
msgstr "Ðемає репозеторіÑ"
@@ -1225,6 +1378,12 @@ msgstr "ВідÑтежувати"
msgid "Notifications"
msgstr "СповіщеннÑ"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Фільтр"
@@ -1277,7 +1436,7 @@ msgid "Pipeline Schedules"
msgstr "Розклади Конвеєрів"
msgid "Pipeline quota"
-msgstr ""
+msgstr "Квота на конвеєри"
msgid "PipelineCharts|Failed:"
msgstr "Ðе вдалоÑÑ:"
@@ -1366,9 +1525,54 @@ msgstr "зі ÑтадіÑми"
msgid "Preferences"
msgstr "ÐалаштуваннÑ"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Приватний — доÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "Приватна — цю групу та Ñ—Ñ— проекти можуть бачити тільки Ñ—Ñ— кориÑтувачі."
+
msgid "Profile"
msgstr "Профіль"
+msgid "Profiles|Account scheduled for removal."
+msgstr "Обліковий Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð¿Ð»Ð°Ð½Ð¾Ð²Ð°Ð½Ð¸Ð¹ Ð´Ð»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ."
+
+msgid "Profiles|Delete Account"
+msgstr "Видалити обліковий запиÑ"
+
+msgid "Profiles|Delete account"
+msgstr "Видалити обліковий запиÑ"
+
+msgid "Profiles|Delete your account?"
+msgstr "Видалити ваш обліковий запиÑ?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу неÑе наÑтупні наÑлідки:"
+
+msgid "Profiles|Invalid password"
+msgstr "Ðеправильний пароль"
+
+msgid "Profiles|Invalid username"
+msgstr "Ðеправильне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Введіть ваш %{confirmationValue} Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу Ð´Ð»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ кориÑтувача."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Вам необхідно змінити влаÑника або видалити ці групи перед тим Ñк видалити ваш обліковий запиÑ."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ñ” влаÑником в цих групах:"
+
+msgid "Profiles|your account"
+msgstr "ваш обліковий запиÑ"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "Проект '%{project_name}' перебуває в процеÑÑ– видаленнÑ."
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' доданий в чергу на видаленнÑ."
@@ -1378,9 +1582,6 @@ msgstr "Проект '%{project_name}' уÑпішно Ñтворений."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проект '%{project_name}' уÑпішно оновлено."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Проект '%{project_name}' видалений."
-
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
@@ -1424,7 +1625,7 @@ msgid "ProjectNetworkGraph|Graph"
msgstr "ІÑторіÑ"
msgid "ProjectSettings|Contact an admin to change this setting."
-msgstr ""
+msgstr "ЗвернітьÑÑ Ð´Ð¾ адмініÑтратора, щоб змінити це налаштуваннÑ."
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr "Тільки підпиÑані комміти можуть бути надіÑлані в цей репозиторій."
@@ -1436,10 +1637,16 @@ msgid "ProjectSettings|This setting is applied on the server level but has been
msgstr "Цей параметр заÑтоÑовуєтьÑÑ Ð½Ð° рівні Ñервера, але його було перевизначено Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "Цей параметр буде заÑтоÑовано до вÑÑ–Ñ… проектів, Ñкщо адмініÑтратор не змінить його."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
+msgid "Projects"
+msgstr "Проекти"
+
msgid "ProjectsDropdown|Frequently visited"
-msgstr ""
+msgstr "ЧаÑто відвідувані"
msgid "ProjectsDropdown|Loading projects"
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
@@ -1459,11 +1666,20 @@ msgstr "Ðа жаль, по вашоу запиту проектів не зна
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Ð¦Ñ Ñ„ÑƒÐ½ÐºÑ†Ñ–Ñ Ð¿Ð¾Ñ‚Ñ€ÐµÐ±ÑƒÑ” підтримки localStorage вашим браузером"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Публічна — група та вÑÑ– публічні проекти можуть переглÑдатиÑÑ Ð±ÐµÐ· автентифікації."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Публічний — проект может переглÑдатиÑÑ Ð±ÐµÐ· автентифікації."
+
msgid "Push Rules"
msgstr "Push-правила"
msgid "Push events"
-msgstr "Push події"
+msgstr "Push-події"
+
+msgid "PushRule|Committer restriction"
+msgstr ""
msgid "Read more"
msgstr "Докладніше"
@@ -1528,6 +1744,9 @@ msgstr "СкаÑувати цей запит на злиттÑ"
msgid "SSH Keys"
msgstr "Ключі SSH"
+msgid "Save"
+msgstr "Зберегти"
+
msgid "Save changes"
msgstr "Зберегти зміни"
@@ -1546,6 +1765,15 @@ msgstr "ÐŸÐ»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
msgid "Search branches and tags"
msgstr "Пошук гілок та тегів"
+msgid "Seconds before reseting failure information"
+msgstr "КількіÑÑ‚ÑŒ Ñекунд до ÑÐºÐ¸Ð´Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— про збої"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Скільки Ñекунд очікувати піÑÐ»Ñ Ð·Ð±Ð¾ÑŽ в Ñховищі даних"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "КількіÑÑ‚ÑŒ Ñекунд Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ´ повторною Ñпробою доÑтупу до Ñховища даних"
+
msgid "Select Archive Format"
msgstr "Виберіть формат архіву"
@@ -1592,16 +1820,19 @@ msgid "Snippets"
msgstr "Фрагменти"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "ЩоÑÑŒ пішло не так з нашого боку"
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "ЩоÑÑŒ пішло не так, намагаючиÑÑŒ змінити ÑÑ‚Ð°Ñ‚ÑƒÑ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ ${this.issuableDisplayName(this.issuableType)}"
msgid "Something went wrong while fetching the projects."
-msgstr ""
+msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
msgid "Something went wrong while fetching the registry list."
-msgstr ""
+msgstr "ЩоÑÑŒ пішло не так при отриманні ÑпиÑку із реєÑтру."
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "ЩоÑÑŒ пішло не так, намагаючиÑÑŒ змінити ÑÑ‚Ð°Ñ‚ÑƒÑ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
+msgstr "Сортувати за"
msgid "SortOptions|Access level, ascending"
msgstr "Рівень доÑтупу, в порÑдку зроÑтаннÑ"
@@ -1720,6 +1951,12 @@ msgstr "Почати %{new_merge_request} з цих змін"
msgid "Start the Runner!"
msgstr "ЗапуÑÑ‚Ñ–Ñ‚ÑŒ Runner!"
+msgid "Subgroups"
+msgstr "Підгрупи"
+
+msgid "Subscribe"
+msgstr "ПідпиÑатиÑÑ"
+
msgid "Switch branch/tag"
msgstr "тег"
@@ -1747,6 +1984,9 @@ msgstr "ДÑкую! Більше не показувати це повідомл
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr "Розширений глобальний пошук в GitLab - це потужний інÑтрумент Ñкий заощаджує ваш чаÑ. ЗаміÑÑ‚ÑŒ Ð´ÑƒÐ±Ð»ÑŽÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ Ñ– витрати чаÑу, ви можете шукати код вÑередині інших команд, Ñкий може допомогти у вашому проекті."
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "Ðа Ñтадії напиÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ, показує Ñ‡Ð°Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ комміту до ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на об'єднаннÑ. Дані будуть автоматично додані піÑÐ»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ першого запиту на об'єднаннÑ."
@@ -1759,6 +1999,15 @@ msgstr "Зв'Ñзок форка видалена."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "Етап випуÑку показує, Ñкільки чаÑу потрібно від ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ до приÑÐ²Ð¾Ñ”Ð½Ð½Ñ Ð²Ð¸Ð¿ÑƒÑку, або Ð´Ð¾Ð´Ð°Ð²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ в вашу дошку проблем. Почніть Ñтворювати проблеми, щоб переглÑдати дані Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ етапу."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "КількіÑÑ‚ÑŒ збоїв піÑÐ»Ñ Ñкої Gitlab повніÑÑ‚ÑŽ заблокує доÑтуп до Ñховища данних. Лічильник кількоÑÑ‚Ñ– збоїв може бути Ñкинутий в інтерфейÑÑ– адмініÑтратора (%{link_to_health_page}), або через %{api_documentation_link}."
+
msgid "The phase of the development lifecycle."
msgstr "Фаза життєвого циклу розробки."
@@ -1789,6 +2038,12 @@ msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ð”Ð•Ð’ показує Ñ‡Ð°Ñ Ð¼Ñ–Ð¶ злиттÑм \"MR\" Ñ
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ñ‚ÐµÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾ÐºÐ°Ð·ÑƒÑ” чаÑ, Ñкий GitLab CI виконує Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку кожного конвеєра Ð´Ð»Ñ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ð½Ð¾Ð³Ð¾ запиту злиттÑ. Дані будуть автоматично додані піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ конвеєра."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "КількіÑÑ‚ÑŒ Ñекунд, протÑгом Ñкої GitLab зберігає інформацію про збої. Якщо протÑгом цього періоду жодних збоїв не відбуваєтьÑÑ, Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ точку Ð¼Ð¾Ð½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑкидаєтьÑÑ."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "КількіÑÑ‚ÑŒ Ñекунд, протÑгом Ñкої GitLab намагатиметьÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ доÑтуп до Ñховища даних. По завершенню цього періоду буде згенерована помилка про Ð¿ÐµÑ€ÐµÐ²Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð»Ñ–Ð¼Ñ–Ñ‚Ñƒ чаÑу."
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "ЧаÑ, витрачений на кожен елемент, зібраний на цьому етапі."
@@ -1798,6 +2053,9 @@ msgstr "Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3,
msgid "There are problems accessing Git storage: "
msgstr "Є проблеми з доÑтупом до Ñховища: "
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° була змінена піÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾ моменту, коли ви почали Ñ—Ñ— редагувати. Ви хотіли б Ñтворити нову?"
+
msgid "This is a confidential issue."
msgstr "Це конфіденційна проблема."
@@ -1984,6 +2242,9 @@ msgstr "Розблоковано"
msgid "Unstar"
msgstr "ВідпиÑатиÑÑŒ"
+msgid "Unsubscribe"
+msgstr "ВідпиÑатиÑÑ"
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "Перейдіть на вищий тарифний план щоб активувати Покращений Глобальний Пошук."
@@ -1991,13 +2252,13 @@ msgid "Upgrade your plan to activate Contribution Analytics."
msgstr "Перейдіть на вищий тарифний план Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— Ðналітики контриб’юторів."
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr ""
+msgstr "Перейдіть на вищий тарифний план щоб активувати Групові Webhook’и."
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 "Завантажити новий файл"
@@ -2047,6 +2308,9 @@ msgstr "Webhook дозволÑÑŽÑ‚ÑŒ вам викликати адреÑу URL
msgid "Weight"
msgstr "Вага"
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "Коли відбуваєтьÑÑ Ð·Ð±Ñ–Ð¹ при доÑтупі до Ñховища даних, GitLab блокує доÑуп до нього протÑгом періоду чаÑу, заданому тут. Це дає можливіÑÑ‚ÑŒ файловій ÑиÑтемі відновитиÑÑ. Репозиторії на шардах (shards) зі збоÑми тимчаÑово не доÑтупні"
+
msgid "Wiki"
msgstr "Wiki"
@@ -2054,103 +2318,103 @@ msgid "WikiClone|Clone your wiki"
msgstr "Клонувати ваш wiki"
msgid "WikiClone|Git Access"
-msgstr ""
+msgstr "Git доÑтуп"
msgid "WikiClone|Install Gollum"
-msgstr ""
+msgstr "Ð’Ñтановити Gollum"
msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
-msgstr ""
+msgstr "РекомендуєтьÑÑ Ð²Ñтановити %{markdown}, з тим щоб GFM функції візуалізувалиÑÑ Ð»Ð¾ÐºÐ°Ð»ÑŒÐ½Ð¾:"
msgid "WikiClone|Start Gollum and edit locally"
-msgstr ""
+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 ""
+msgstr "Це — Ñтара верÑÑ–Ñ Ñторінки."
msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
-msgstr ""
+msgstr "Ви можете переглÑнути %{most_recent_link} або перейти на %{history_link}."
msgid "WikiHistoricalPage|history"
-msgstr ""
+msgstr "Ñ–ÑторіÑ"
msgid "WikiHistoricalPage|most recent version"
-msgstr ""
+msgstr "оÑÑ‚Ð°Ð½Ð½Ñ Ð²ÐµÑ€ÑÑ–Ñ"
msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
-msgstr ""
+msgstr "Більше прикладів знаходитьÑÑ Ð² %{docs_link}"
msgid "WikiMarkdownDocs|documentation"
-msgstr ""
+msgstr "документаціÑ"
msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
-msgstr ""
+msgstr "Ð”Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° (нову) Ñторінку, проÑто введіть %{link_example}"
msgid "WikiNewPagePlaceholder|how-to-setup"
-msgstr ""
+msgstr "ІнÑтрукціÑ"
msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
-msgstr ""
+msgstr "Порада: можна вказати повний шлÑÑ… до нового файлу. Ми автоматично Ñтворимо вÑÑ– відÑутні каталоги."
msgid "WikiNewPageTitle|New Wiki Page"
-msgstr ""
+msgstr "Ðова Вікі-Ñторінка"
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
-msgstr ""
+msgstr "Ви дійÑно бажаєте видалити цю Ñторінку?"
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
-msgstr ""
+msgstr "ХтоÑÑŒ редагував Ñторінку в той же чаÑ, що Ñ– ви. Будь лаÑка, ознайомтеÑÑ Ð· %{page_link} Ñ– переконайтеÑÑ, ваші зміни не затруть зміни інших."
msgid "WikiPageConflictMessage|the page"
-msgstr ""
+msgstr "Ñторінка"
msgid "WikiPageCreate|Create %{page_title}"
-msgstr ""
+msgstr "Створити %{page_title}"
msgid "WikiPageEdit|Update %{page_title}"
-msgstr ""
+msgstr "Оновити %{page_title}"
msgid "WikiPage|Page slug"
-msgstr ""
+msgstr "ШлÑÑ… Ñторінки"
msgid "WikiPage|Write your content or drag files here..."
-msgstr ""
+msgstr "Ðапишіть текÑÑ‚ або перетÑгніть файли Ñюди..."
msgid "Wiki|Create Page"
-msgstr ""
+msgstr "Створити Ñторінку"
msgid "Wiki|Create page"
-msgstr ""
+msgstr "Створити Ñторінку"
msgid "Wiki|Edit Page"
msgstr "Редагувати Ñторінку"
msgid "Wiki|Empty page"
-msgstr ""
+msgstr "ÐŸÐ¾Ñ€Ð¾Ð¶Ð½Ñ Ñторінка"
msgid "Wiki|More Pages"
-msgstr ""
+msgstr "Більше Ñторінок"
msgid "Wiki|New page"
-msgstr ""
+msgstr "Ðова Ñторінка"
msgid "Wiki|Page history"
-msgstr ""
+msgstr "ІÑÑ‚Ð¾Ñ€Ñ–Ñ Ñторінки"
msgid "Wiki|Page version"
-msgstr ""
+msgstr "ВерÑÑ–Ñ Ñторінки"
msgid "Wiki|Pages"
-msgstr ""
+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 "СкаÑувати запит доÑтупу"
@@ -2167,9 +2431,21 @@ msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ зв'Ñзок з форка Ð
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Ви збираєтеÑÑ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‚Ð¸ проект %{project_name_with_namespace} іншому влаÑнику. Ви ÐБСОЛЮТÐО впевнені?"
+msgid "You are on a read-only GitLab instance."
+msgstr "Ви знаходитеÑÑ Ð½Ð° інÑтанÑÑ– Gitlab \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\"."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Ви знаходитеÑÑ Ð½Ð° інÑтанÑÑ– Gitlab \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\". Якщо ви хочете внеÑти зміни, вам необхідно перейти на %{link_to_primary_node}."
+
msgid "You can only add files when you are on a branch"
msgstr "Ви можете додавати тільки файли, коли перебуваєте в гілці"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Ви не можете запиÑувати на вторинні інÑтанÑи \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\" GitLab Geo. Будь лаÑка викориÑтовуйте %{link_to_primary_node}."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Ви не можете запиÑувати на цей \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\" інÑÑ‚Ð°Ð½Ñ GitLab."
+
msgid "You have reached your project limit"
msgstr "Ви доÑÑгли Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð² вашому проекті"
@@ -2201,13 +2477,16 @@ msgid "You won't be able to pull or push project code via SSH until you %{add_ss
msgstr "Ви не зможете отримувати Ñ– відправлÑти код проекту через SSH поки %{add_ssh_key_link} в ваш профіль."
msgid "Your comment will not be visible to the public."
-msgstr ""
+msgstr "Ваш коментар не буде видимим Ð´Ð»Ñ Ð²ÑÑ–Ñ…."
+
+msgid "Your groups"
+msgstr "Ваші групи"
msgid "Your name"
msgstr "Ваше ім'Ñ"
msgid "Your projects"
-msgstr ""
+msgstr "Ваші проекти"
msgid "commit"
msgstr "комміт"
@@ -2230,9 +2509,15 @@ msgstr[0] "джерело"
msgstr[1] "джерела"
msgstr[2] "джерел"
-msgid "to help your contributors communicate effectively!"
-msgstr ""
+msgid "password"
+msgstr "пароль"
msgid "personal access token"
-msgstr ""
+msgstr "оÑобиÑтий токен доÑтупу"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "щоб допомогти вашим контриб’юторам ефективно ÑпілкуватиÑÑ!"
+
+msgid "username"
+msgstr "ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index caccb246e0b..0d6f0d201c2 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:35-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-06 01:25-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -22,7 +22,7 @@ msgstr[0] "%d 次æ交"
msgid "%d layer"
msgid_plural "%d layers"
-msgstr[0] ""
+msgstr[0] "%d 层"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -31,6 +31,10 @@ msgstr[0] "为æ高页é¢åŠ è½½é€Ÿåº¦åŠæ€§èƒ½ï¼Œå·²çœç•¥äº† %s 次æ交。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "ç”± %{commit_author_link} æ交于 %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] "%{count} ä½å‚与者"
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} 个è½åŽ %{default_branch} 分支的æ交, %{number_commits_ahead} 早超å‰çš„æ交"
@@ -50,6 +54,12 @@ msgstr[0] "%{storage_name}:已 %{failed_attempts} 次å°è¯•è®¿é—®å­˜å‚¨å¤±è´¥ï
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如需了解更多的安装信æ¯ï¼Œè¯·æŸ¥çœ‹ %{link})"
+msgid "+ %{moreCount} more"
+msgstr "+ 还有 %{moreCount} æ¡"
+
+msgid "- show less"
+msgstr "- 显示较少"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¡æµæ°´çº¿"
@@ -58,7 +68,7 @@ msgid "1st contribution!"
msgstr "最高贡献"
msgid "2FA enabled"
-msgstr ""
+msgstr "å¯ç”¨ä¸¤æ­¥éªŒè¯"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
@@ -105,9 +115,18 @@ msgstr "新建一个用于推é€æˆ–拉å–çš„ SSH 秘钥到账å·ä¸­ã€‚"
msgid "Add new directory"
msgstr "添加目录"
+msgid "AdminHealthPageLink|health page"
+msgstr "å¥åº·é¡µé¢"
+
+msgid "Advanced settings"
+msgstr "高级设置"
+
msgid "All"
msgstr "全部"
+msgid "An error occurred. Please try again."
+msgstr "å‘生了错误,请å†è¯•ä¸€æ¬¡ã€‚"
+
msgid "Appearance"
msgstr "外观"
@@ -123,6 +142,9 @@ msgstr "确定è¦åˆ é™¤æ­¤æµæ°´çº¿è®¡åˆ’å—?"
msgid "Are you sure you want to discard your changes?"
msgstr "确定è¦æ”¾å¼ƒä¿®æ”¹å—?"
+msgid "Are you sure you want to leave this group?"
+msgstr "确定è¦ç¦»å¼€è¿™ä¸ªç¾¤ç»„å—?"
+
msgid "Are you sure you want to reset registration token?"
msgstr "确定è¦é‡ç½®æ³¨å†Œä»¤ç‰Œå—?"
@@ -156,18 +178,21 @@ msgstr "自动审查程åºå’Œè‡ªåŠ¨éƒ¨ç½²ç¨‹åºéœ€è¦ %{kubernetes} æ‰èƒ½æ­£å¸¸
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr "DevOps 自动化(测试版)"
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr "å¯ä»¥ä¸ºæ­¤é¡¹ç›®å¯ç”¨ DevOps 自动化。它将根æ®é¢„定义的 CI/CDé…置自动构建ã€æµ‹è¯•å’Œéƒ¨ç½²åº”用程åºã€‚"
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "DevOps 自动化文档"
msgid "AutoDevOps|Enable in settings"
msgstr "在设置中å¯ç”¨"
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "将根æ®é¢„定义的 CI/CD é…置自动构建ã€æµ‹è¯•å’Œéƒ¨ç½²åº”用程åºã€‚"
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr "想了解更多请访问 %{link_to_documentation}"
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "您å¯ä»¥ä¸ºæ­¤é¡¹ç›®æ¿€æ´» %{link_to_settings}。"
+
msgid "Billing"
msgstr "è´¦å•"
@@ -229,6 +254,9 @@ msgstr[0] "分支"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择åˆé€‚çš„ GitLab CI Yaml 模æ¿å¹¶æ交更改。%{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr "分支已有新å˜æ›´"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -439,134 +467,146 @@ msgstr "已跳过"
msgid "CiStatus|running"
msgstr "è¿è¡Œä¸­"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "断路器 API"
+
msgid "Clone repository"
msgstr "克隆存储库"
msgid "Close"
msgstr "关闭"
+msgid "Cluster"
+msgstr "集群"
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
-msgstr ""
+msgstr "必须在此å¸æˆ·ä¸‹åˆ›å»º %{link_to_container_project}"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "集群详情"
msgid "ClusterIntegration|Cluster integration"
-msgstr ""
+msgstr "集群集æˆ"
msgid "ClusterIntegration|Cluster integration is disabled for this project."
-msgstr ""
+msgstr "此项目已ç¦ç”¨é›†ç¾¤é›†æˆã€‚"
msgid "ClusterIntegration|Cluster integration is enabled for this project."
-msgstr ""
+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 ""
+msgstr "此项目已å¯ç”¨é›†ç¾¤é›†æˆã€‚ç¦ç”¨æ­¤é›†æˆä¸ä¼šå½±å“您的集群,它åªä¼šæš‚时关闭 GitLab 的连接。"
msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr ""
+msgstr "集群正在 Google Container Engine 上创建..."
msgid "ClusterIntegration|Cluster name"
-msgstr ""
+msgstr "集群å称"
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr ""
+msgstr "集群已在 Google Container Engine 上æˆåŠŸåˆ›å»º"
msgid "ClusterIntegration|Copy cluster name"
-msgstr ""
+msgstr "å¤åˆ¶é›†ç¾¤å称"
msgid "ClusterIntegration|Create cluster"
-msgstr ""
+msgstr "创建集群"
msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr ""
+msgstr "在 Google Container Engine 上创建新集群"
msgid "ClusterIntegration|Enable cluster integration"
-msgstr ""
+msgstr "å¯ç”¨é›†ç¾¤é›†æˆ"
msgid "ClusterIntegration|Google Cloud Platform project ID"
-msgstr ""
+msgstr "Google 云平å°é¡¹ç›®ID"
msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Google Container Engine"
msgid "ClusterIntegration|Google Container Engine project"
-msgstr ""
-
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
+msgstr "Google Container Engine 项目"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
-msgstr ""
+msgstr "了解详细%{link_to_documentation}"
-msgid "ClusterIntegration|See machine types"
-msgstr ""
+msgid "ClusterIntegration|Machine type"
+msgstr "机器类型"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
-msgstr ""
+msgstr "ç¡®ä¿æ‚¨çš„å¸æˆ·ç¬¦åˆåˆ›å»ºé›†ç¾¤çš„%{link_to_requirements}"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "管ç†æ‚¨çš„ GitLab 项目集群集æˆ"
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
-msgstr ""
+msgstr "访问%{link_gke}æ¥ç®¡ç†æ‚¨çš„集群"
msgid "ClusterIntegration|Number of nodes"
-msgstr ""
+msgstr "节点数é‡"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "请确ä¿æ‚¨çš„ Google å¸æˆ·ç¬¦åˆä»¥ä¸‹è¦æ±‚:"
msgid "ClusterIntegration|Project namespace (optional, unique)"
-msgstr ""
+msgstr "项目命å空间(å¯é€‰ï¼Œå”¯ä¸€)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "请阅读关于集群集æˆçš„%{link_to_help_page}。"
msgid "ClusterIntegration|Remove cluster integration"
-msgstr ""
+msgstr "删除集群集æˆ"
msgid "ClusterIntegration|Remove integration"
-msgstr ""
+msgstr "删除集æˆ"
msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
-msgstr ""
+msgstr "删除集群集æˆå°†åˆ é™¤æ‚¨æ·»åŠ åˆ°æ­¤é¡¹ç›®çš„集群é…置。它ä¸ä¼šåˆ é™¤æ‚¨çš„项目。"
-msgid "ClusterIntegration|Save changes"
-msgstr ""
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "查看并编辑集群的详细信æ¯"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "å‚è§æœºå™¨ç±»åž‹"
msgid "ClusterIntegration|See your projects"
-msgstr ""
+msgstr "看到您的项目"
msgid "ClusterIntegration|See zones"
-msgstr ""
+msgstr "查看区域"
msgid "ClusterIntegration|Something went wrong on our end."
-msgstr ""
-
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
+msgstr "å‘生了内部错误"
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
-msgstr ""
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "在 Google Container Engine 上创建集群时å‘生错误"
msgid "ClusterIntegration|Toggle Cluster"
-msgstr ""
-
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
+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 ""
+msgstr "使用与此项目关è”的集群,您å¯ä»¥ä½¿ç”¨å®¡é˜…应用程åºï¼Œéƒ¨ç½²åº”用程åºï¼Œè¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚"
msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr ""
+msgstr "您的å¸æˆ·å¿…须有%{link_to_container_engine}"
msgid "ClusterIntegration|Zone"
-msgstr ""
+msgstr "区域"
msgid "ClusterIntegration|access to Google Container Engine"
-msgstr ""
+msgstr "访问 Google Container Engine"
msgid "ClusterIntegration|cluster"
-msgstr ""
+msgstr "集群"
msgid "ClusterIntegration|help page"
-msgstr ""
+msgstr "帮助页é¢"
msgid "ClusterIntegration|meets the requirements"
-msgstr ""
+msgstr "符åˆè¦æ±‚"
msgid "ClusterIntegration|properly configured"
-msgstr ""
+msgstr "正确é…ç½®"
msgid "Comments"
msgstr "评论"
@@ -575,6 +615,10 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] "æ交 %d 个文件"
+
msgid "Commit Message"
msgstr "æ交消æ¯"
@@ -606,49 +650,49 @@ msgid "Compare"
msgstr "比较"
msgid "Container Registry"
-msgstr ""
+msgstr "容器注册"
msgid "ContainerRegistry|Created"
-msgstr ""
+msgstr "已创建"
msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
-msgstr ""
+msgstr "首先使用您的 GitLab 用户å和密ç ç™»å½• GitLab 的容器注册表。如果您有%{link_2fa},您需è¦ä½¿ç”¨%{link_token}:"
msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
-msgstr ""
+msgstr "GitLab 最多支æŒ3个级别的镜åƒå称。以下镜åƒç¤ºä¾‹å¯¹æ‚¨çš„项目有效:"
msgid "ContainerRegistry|How to use the Container Registry"
-msgstr ""
+msgstr "如何使用容器注册表"
msgid "ContainerRegistry|Learn more about"
-msgstr ""
+msgstr "关于更多"
msgid "ContainerRegistry|No tags in Container Registry for this container image."
-msgstr ""
+msgstr "容器注册表中没有此容器镜åƒçš„标签。"
msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
-msgstr ""
+msgstr "登录åŽæ‚¨å¯ä»¥ä½¿ç”¨é€šç”¨çš„%{build}å’Œ%{push}命令自由创建和上传容器映åƒ"
msgid "ContainerRegistry|Remove repository"
-msgstr ""
+msgstr "删除存储库"
msgid "ContainerRegistry|Remove tag"
-msgstr ""
+msgstr "删除标签"
msgid "ContainerRegistry|Size"
-msgstr ""
+msgstr "大å°"
msgid "ContainerRegistry|Tag"
-msgstr ""
+msgstr "标签"
msgid "ContainerRegistry|Tag ID"
-msgstr ""
+msgstr "标签 ID"
msgid "ContainerRegistry|Use different image names"
-msgstr ""
+msgstr "使用ä¸åŒçš„é•œåƒå称"
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
-msgstr ""
+msgstr "å°† Docker 容器注册表集æˆåˆ° GitLab 中,æ¯ä¸ªé¡¹ç›®éƒ½å¯ä»¥æœ‰è‡ªå·±çš„空间æ¥å­˜å‚¨ Docker 的图åƒã€‚"
msgid "Contribution guide"
msgstr "贡献指å—"
@@ -656,6 +700,12 @@ msgstr "贡献指å—"
msgid "Contributors"
msgstr "贡献者"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "控制此次è¦èŠ‚点的 LFS/attachment 的最大并å‘性"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "控制此次è¦èŠ‚点的存储库的最大并å‘"
+
msgid "Copy SSH public key to clipboard"
msgstr "å¤åˆ¶ SSH 公钥到剪贴æ¿"
@@ -677,9 +727,21 @@ msgstr "创建目录"
msgid "Create empty bare repository"
msgstr "创建空的存储库"
+msgid "Create file"
+msgstr "创建文件"
+
msgid "Create merge request"
msgstr "创建åˆå¹¶è¯·æ±‚"
+msgid "Create new branch"
+msgstr "创建新分支"
+
+msgid "Create new directory"
+msgstr "创建新目录"
+
+msgid "Create new file"
+msgstr "创建新文件"
+
msgid "Create new..."
msgstr "创建..."
@@ -765,6 +827,9 @@ msgstr "目录å称"
msgid "Discard changes"
msgstr "放弃更改"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "关闭循环分æžä»‹ç»æ¡†"
+
msgid "Dismiss Merge Request promotion"
msgstr "关闭åˆå¹¶è¯·æ±‚中的促销广告"
@@ -837,12 +902,18 @@ msgstr "æ¯å‘¨æ‰§è¡Œï¼ˆå‘¨æ—¥å‡Œæ™¨ 4 点)"
msgid "Explore projects"
msgstr "查看项目"
+msgid "Explore public groups"
+msgstr "æœç´¢å…¬å…±ç¾¤ç»„"
+
msgid "Failed to change the owner"
msgstr "无法å˜æ›´æ‰€æœ‰è€…"
msgid "Failed to remove the pipeline schedule"
msgstr "无法删除æµæ°´çº¿è®¡åˆ’"
+msgid "File name"
+msgstr "文件å"
+
msgid "Files"
msgstr "文件"
@@ -886,9 +957,15 @@ msgstr "GPG 密钥"
msgid "Geo Nodes"
msgstr "Geo 节点"
+msgid "Geo|File sync capacity"
+msgstr "文件åŒæ­¥å®¹é‡"
+
msgid "Geo|Groups to replicate"
msgstr "è¦å¤åˆ¶çš„群组"
+msgid "Geo|Repository sync capacity"
+msgstr "存储库åŒæ­¥å®¹é‡"
+
msgid "Geo|Select groups to replicate."
msgstr "选择è¦å¤åˆ¶çš„群组。"
@@ -905,7 +982,7 @@ msgid "GoToYourFork|Fork"
msgstr "跳转到派生项目"
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
-msgstr ""
+msgstr "Google 身份验è¯ä¸æ˜¯%{link_to_documentation}。如果您想使用此æœåŠ¡ï¼Œè¯·å’¨è¯¢æ‚¨çš„ GitLab 管ç†å‘˜ã€‚"
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "ç¦æ­¢ä¸Žå…¶ä»–群组共享 %{group} 中的项目"
@@ -931,6 +1008,51 @@ msgstr "无法ç¦ç”¨çˆ¶ç»„的“共享群组é”â€ï¼Œåªæœ‰çˆ¶ç¾¤ç»„的所有者
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "从 %{ancestor_group_name} 中删除共享群组é”"
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "群组是几个项目的集åˆã€‚"
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "如果您在一个群组下组织项目,它的工作方å¼å°±åƒä¸€ä¸ªæ–‡ä»¶å¤¹ã€‚"
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "找ä¸åˆ°ç¾¤ç»„"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "您å¯ä»¥ç®¡ç†ç¾¤ç»„æˆå‘˜çš„æƒé™å¹¶è®¿é—®ç¾¤ç»„中的æ¯ä¸ªé¡¹ç›®ã€‚"
+
+msgid "GroupsTreeRole|as"
+msgstr "çš„"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "您确定è¦ç¦»å¼€ç¾¤ç»„“${this.group.fullName}â€å—?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "在此群组中创建一个项目。"
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "在此群组中创建一个å­ç¾¤ç»„。"
+
+msgid "GroupsTree|Edit group"
+msgstr "编辑群组"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "无法离开群组。请确ä¿æ‚¨ä¸æ˜¯å”¯ä¸€çš„所有者。"
+
+msgid "GroupsTree|Filter by name..."
+msgstr "按å称过滤..."
+
+msgid "GroupsTree|Leave this group"
+msgstr "离开这个群组"
+
+msgid "GroupsTree|Loading groups"
+msgstr "加载群组中"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ä»»ä½•ç¬¦åˆçš„群组"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰ä»»ä½•ç¾¤ç»„或项目符åˆæ‚¨çš„æœç´¢"
+
msgid "Health Check"
msgstr "å¥åº·æ£€æŸ¥"
@@ -974,6 +1096,12 @@ msgid "Instance"
msgid_plural "Instances"
msgstr[0] "例å­"
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "内部 - 任何登录的用户都å¯ä»¥æŸ¥çœ‹è¯¥ç¾¤ç»„和任何内部项目。"
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "内部 - å¯ä»¥é€šè¿‡ä»»ä½•ç™»å½•ç”¨æˆ·è®¿é—®è¯¥é¡¹ç›®ã€‚"
+
msgid "Interval Pattern"
msgstr "循环周期"
@@ -1041,6 +1169,9 @@ msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "æµæ°´çº¿è®¡åˆ’文档"
+msgid "Leave"
+msgstr "离开"
+
msgid "Leave group"
msgstr "退出群组"
@@ -1063,6 +1194,12 @@ msgstr "å·²é”定"
msgid "Locked Files"
msgstr "å·²é”定文件"
+msgid "Login"
+msgstr "登录"
+
+msgid "Maximum git storage failures"
+msgstr "最大 git 存储失败"
+
msgid "Median"
msgstr "中ä½æ•°"
@@ -1093,6 +1230,9 @@ msgstr "帮助文档"
msgid "Multiple issue boards"
msgstr "多个议题看æ¿"
+msgid "New Cluster"
+msgstr "新集群"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建议题"
@@ -1109,23 +1249,32 @@ msgstr "新建目录"
msgid "New file"
msgstr "新建文件"
+msgid "New group"
+msgstr "新群组"
+
msgid "New issue"
msgstr "新建议题"
msgid "New merge request"
msgstr "新建åˆå¹¶è¯·æ±‚"
+msgid "New project"
+msgstr "新项目"
+
msgid "New schedule"
msgstr "新建计划"
msgid "New snippet"
msgstr "新建代ç ç‰‡æ®µ"
+msgid "New subgroup"
+msgstr "æ–°å­ç¾¤ç»„"
+
msgid "New tag"
msgstr "新建标签"
msgid "No container images stored for this project. Add one by following the instructions above."
-msgstr ""
+msgstr "没有为此项目存储容器镜åƒã€‚请按照上述说明添加一个。"
msgid "No repository"
msgstr "没有存储库"
@@ -1199,6 +1348,12 @@ msgstr "关注"
msgid "Notifications"
msgstr "通知"
+msgid "Number of access attempts"
+msgstr "å°è¯•è®¿é—®æ¬¡æ•°"
+
+msgid "Number of failures before backing off"
+msgstr "退出å‰çš„失败次数"
+
msgid "OfSearchInADropdown|Filter"
msgstr "筛选"
@@ -1340,9 +1495,54 @@ msgstr "于阶段"
msgid "Preferences"
msgstr "å好设置"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "ç§äºº - å¿…é¡»å‘æ¯ä¸ªç”¨æˆ·æ˜Žç¡®æŽˆäºˆé¡¹ç›®è®¿é—®æƒé™ã€‚"
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "ç§äºº - 群组åŠå…¶é¡¹ç›®åªèƒ½ç”±æˆå‘˜æŸ¥çœ‹ã€‚"
+
msgid "Profile"
msgstr "用户信æ¯"
+msgid "Profiles|Account scheduled for removal."
+msgstr "å¸æˆ·åˆ é™¤è®¡åˆ’。"
+
+msgid "Profiles|Delete Account"
+msgstr "删除å¸æˆ·"
+
+msgid "Profiles|Delete account"
+msgstr "删除å¸æˆ·"
+
+msgid "Profiles|Delete your account?"
+msgstr "删除您的å¸æˆ·?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "删除å¸æˆ·å…·æœ‰ä»¥ä¸‹æ•ˆæžœï¼š"
+
+msgid "Profiles|Invalid password"
+msgstr "密ç æ— æ•ˆ"
+
+msgid "Profiles|Invalid username"
+msgstr "用户å无效"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "键入您的 %{confirmationValue} 以确认:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "您无æƒåˆ é™¤æ­¤ç”¨æˆ·ã€‚"
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "您必须转移所有æƒæˆ–删除这些群组,然åŽæ‰èƒ½åˆ é™¤æ‚¨çš„å¸æˆ·ã€‚"
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "您的å¸æˆ·ç›®å‰æ˜¯è¿™äº›ç¾¤ç»„的所有者:"
+
+msgid "Profiles|your account"
+msgstr "您的å¸æˆ·"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "项目 “%{project_name}†正在被删除。"
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "项目 '%{project_name}' 已进入删除队列。"
@@ -1352,9 +1552,6 @@ msgstr "项目 '%{project_name}' 已创建æˆåŠŸã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "项目 '%{project_name}' 已更新完æˆã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "项目 '%{project_name}' 将被删除。"
-
msgid "Project access must be granted explicitly to each user."
msgstr "项目访问æƒé™å¿…须明确授æƒç»™æ¯ä¸ªç”¨æˆ·ã€‚"
@@ -1412,6 +1609,12 @@ msgstr "此设置应用于æœåŠ¡å™¨çº§åˆ«ï¼Œä½†å·²è¢«è¯¥é¡¹ç›®è¦†ç›–。"
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr "此设置将应用于所有项目,除éžè¢«ç®¡ç†å‘˜è¦†ç›–。"
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr "用户åªèƒ½é€šè¿‡è‡ªå·±å·²éªŒè¯çš„电å­é‚®ä»¶åœ°å€å°†æ交到此存储库中。"
+
+msgid "Projects"
+msgstr "项目"
+
msgid "ProjectsDropdown|Frequently visited"
msgstr "ç»å¸¸è®¿é—®"
@@ -1433,12 +1636,21 @@ msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ç¬¦åˆæ¡ä»¶çš„项目"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "此功能需è¦æµè§ˆå™¨æ”¯æŒ localStorage"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "公开 - 群组和任何公共项目å¯ä»¥åœ¨æ²¡æœ‰ä»»ä½•èº«ä»½éªŒè¯çš„情况下查看。"
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "公开 - 无需任何身份验è¯å³å¯è®¿é—®è¯¥é¡¹ç›®ã€‚"
+
msgid "Push Rules"
msgstr "推é€è§„则"
msgid "Push events"
msgstr "推é€äº‹ä»¶"
+msgid "PushRule|Committer restriction"
+msgstr "æ交é™åˆ¶"
+
msgid "Read more"
msgstr "了解更多"
@@ -1502,6 +1714,9 @@ msgstr "还原此åˆå¹¶è¯·æ±‚"
msgid "SSH Keys"
msgstr "SSH 密钥"
+msgid "Save"
+msgstr "ä¿å­˜"
+
msgid "Save changes"
msgstr "ä¿å­˜ä¿®æ”¹"
@@ -1520,6 +1735,15 @@ msgstr "æµæ°´çº¿è®¡åˆ’"
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ ‡ç­¾"
+msgid "Seconds before reseting failure information"
+msgstr "é‡ç½®å¤±è´¥ä¿¡æ¯ç­‰å¾…时间(秒)"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "存储失败åŽç­‰å¾…时间(秒)"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "等待存储访问å°è¯•æ—¶é—´(秒)"
+
msgid "Select Archive Format"
msgstr "选择下载格å¼"
@@ -1564,16 +1788,19 @@ msgid "Snippets"
msgstr "代ç ç‰‡æ®µ"
msgid "Something went wrong on our end."
-msgstr ""
+msgstr "å‘生了错误。"
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "å°è¯•æ›´æ”¹ ${this.issuableDisplayName(this.issuableType)} çš„é”定状æ€æ—¶å‘生错误"
msgid "Something went wrong while fetching the projects."
-msgstr ""
+msgstr "拉å–项目时å‘生错误。"
msgid "Something went wrong while fetching the registry list."
-msgstr ""
+msgstr "拉å–注册表列表时å‘生错误。"
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "å°è¯•æ›´æ”¹ ${this.issuableDisplayName(this.issuableType)} çš„é”定状æ€æ—¶å‘生错误"
+msgid "Sort by"
+msgstr "排åº"
msgid "SortOptions|Access level, ascending"
msgstr "访问级别,å‡åºæŽ’列"
@@ -1692,6 +1919,12 @@ msgstr "由此更改 %{new_merge_request}"
msgid "Start the Runner!"
msgstr "å¯åŠ¨ Runner!"
+msgid "Subgroups"
+msgstr "å­ç¾¤ç»„"
+
+msgid "Subscribe"
+msgstr "订阅"
+
msgid "Switch branch/tag"
msgstr "切æ¢åˆ†æ”¯/标签"
@@ -1715,7 +1948,10 @@ 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 "断路器关闭阈值应该低于故障计数阈值"
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 "ç¼–ç é˜¶æ®µæ¦‚述了从第一次æ交到创建åˆå¹¶è¯·æ±‚的时间。创建第一个åˆå¹¶è¯·æ±‚åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„。"
@@ -1729,6 +1965,15 @@ msgstr "派生关系已被删除。"
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "议题阶段概述了从创建议题到将议题添加到里程碑或议题看æ¿æ‰€èŠ±è´¹çš„时间。创建第一个议题åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„.。"
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr "GitLab 访问存储的次数。"
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr "GitLab 将临时ç¦ç”¨å¯¹ä¸»å­˜å‚¨åˆ†ç‰‡çš„访问"
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "GitLab 将完全阻止访问存储的故障次数。å¯ä»¥åœ¨ç®¡ç†ç•Œé¢%{link_to_health_page}或使用%{api_documentation_link}é‡ç½®æ•…障次数。"
+
msgid "The phase of the development lifecycle."
msgstr "项目生命周期中的å„个阶段。"
@@ -1759,6 +2004,12 @@ msgstr "预å‘布阶段概述了从åˆå¹¶è¯·æ±‚被åˆå¹¶åˆ°éƒ¨ç½²è‡³ç”Ÿäº§çŽ¯å¢ƒ
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "测试阶段概述了 GitLab CI 为相关åˆå¹¶è¯·æ±‚è¿è¡Œæ¯ä¸ªæµæ°´çº¿æ‰€éœ€çš„时间。当第一个æµæ°´çº¿è¿è¡Œå®ŒæˆåŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„。"
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "GitLab å°†ä¿æŒå¤±è´¥ä¿¡æ¯çš„时间(秒)。在此期间ä¸å‘生故障时,有关安装的信æ¯å°†é‡ç½®ã€‚"
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "GitLab å°†å°è¯•è®¿é—®å­˜å‚¨çš„时间(秒)。在此时间之åŽå°†å¼•å‘超时错误。"
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "该阶段æ¯æ¡æ•°æ®æ‰€èŠ±çš„时间"
@@ -1768,6 +2019,9 @@ msgstr "中ä½æ•°æ˜¯ä¸€ä¸ªæ•°åˆ—中最中间的值。例如在 3ã€5ã€9 之间ï
msgid "There are problems accessing Git storage: "
msgstr "访问 Git 存储时出现问题:"
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "自您开始编辑åŽ, 此分支已更改。您想创建一个新的分支å—?"
+
msgid "This is a confidential issue."
msgstr "这是一个机密议题。"
@@ -1950,6 +2204,9 @@ msgstr "已解é”"
msgid "Unstar"
msgstr "å–消星标"
+msgid "Unsubscribe"
+msgstr "退订"
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "å‡çº§æ‚¨çš„方案以å¯ç”¨é«˜çº§å…¨å±€æœç´¢ã€‚"
@@ -2013,6 +2270,9 @@ msgstr "如果有新的推é€æˆ–新的议题,Webhook将自动触å‘您设置UR
msgid "Weight"
msgstr "æƒé‡"
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "访问存储失败时。 GitLab 将在此处指定的时间内阻止对存储的访问。这å…许文件系统æ¢å¤ã€‚故障分片上的存储库暂时无法使用"
+
msgid "Wiki"
msgstr "Wiki"
@@ -2133,9 +2393,21 @@ msgstr "å³å°†åˆ é™¤ä¸Žæºé¡¹ç›® %{forked_from_project} 的派生关系。确定
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å³å°† %{project_name_with_namespace} 转移给å¦ä¸€ä¸ªæ‰€æœ‰è€…。确定继续å—?"
+msgid "You are on a read-only GitLab instance."
+msgstr "您在一个åªè¯» GitLab 实例上。"
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "您在一个åªè¯»çš„ GitLab 实例上。如果您想进行任何更改,您必须访问%{link_to_primary_node}。"
+
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "您ä¸èƒ½å†™å…¥åªè¯»çš„辅助 GitLab Geo 实例。请改用%{link_to_primary_node}。"
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "您ä¸èƒ½å†™å…¥è¿™ä¸ªåªè¯»çš„ GitLab 实例。"
+
msgid "You have reached your project limit"
msgstr "您已达到项目数é‡é™åˆ¶"
@@ -2169,6 +2441,9 @@ msgstr "在账å·ä¸­ %{add_ssh_key_link} 之å‰å°†æ— æ³•é€šè¿‡ SSH 拉å–或推é
msgid "Your comment will not be visible to the public."
msgstr "您的评论将ä¸ä¼šå…¬å¼€æ˜¾ç¤ºã€‚"
+msgid "Your groups"
+msgstr "您的群组"
+
msgid "Your name"
msgstr "您的åå­—"
@@ -2192,9 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "父级"
+msgid "password"
+msgstr "密ç "
+
+msgid "personal access token"
+msgstr "个人访问令牌"
+
msgid "to help your contributors communicate effectively!"
msgstr "帮助您的贡献者进行有效沟通ï¼"
-msgid "personal access token"
-msgstr ""
+msgid "username"
+msgstr "用户å"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 1ae511b4d6d..afb40e8b75f 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 05:36-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Language: zh_HK\n"
@@ -31,6 +31,10 @@ msgstr[0] "為æ高é é¢åŠ è¼‰é€Ÿåº¦åŠæ€§èƒ½ï¼Œå·²çœç•¥äº† %s 次æ交。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "ç”± %{commit_author_link} æ交於 %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -50,6 +54,12 @@ msgstr[0] "%{storage_name}:已訪å•æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(想了解更多的安è£è¨Šæ¯è«‹æŸ¥çœ‹ %{link})"
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
@@ -105,9 +115,18 @@ msgstr "新增壹個用於推é€æˆ–拉å–çš„ SSH 秘鑰到賬號中。"
msgid "Add new directory"
msgstr "添加新目錄"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "全部"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -123,6 +142,9 @@ msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·šè¨ˆåŠƒå—Žï¼Ÿ"
msgid "Are you sure you want to discard your changes?"
msgstr "確定è¦æ”¾æ£„修改嗎?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "確定è¦é‡ç½®è¨»å†Šä»¤ç‰Œå—Žï¼Ÿ"
@@ -156,18 +178,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr ""
@@ -229,6 +254,9 @@ msgstr[0] "分支"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部署, è«‹é¸æ“‡åˆé©çš„ GitLab CI Yaml 模æ¿ä½µæ交更改。%{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -439,15 +467,24 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "é‹è¡Œä¸­"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
msgid "Close"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -490,27 +527,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -520,7 +563,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -532,18 +578,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -575,6 +615,10 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
msgid "Commit Message"
msgstr ""
@@ -656,6 +700,12 @@ msgstr "è²¢ç»æŒ‡å—"
msgid "Contributors"
msgstr "è²¢ç»è€…"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -677,9 +727,21 @@ msgstr "創建目錄"
msgid "Create empty bare repository"
msgstr "創建空的存儲庫"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "創建åˆä½µè«‹æ±‚"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "創建..."
@@ -765,6 +827,9 @@ msgstr "目錄å稱"
msgid "Discard changes"
msgstr "放棄更改"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr ""
@@ -837,12 +902,18 @@ msgstr "æ¯é€±åŸ·è¡Œï¼ˆå‘¨æ—¥æ·©æ™¨ 4 點)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "無法變更所有者"
msgid "Failed to remove the pipeline schedule"
msgstr "無法刪除æµæ°´ç·šè¨ˆåŠƒ"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "文件"
@@ -886,9 +957,15 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr ""
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr ""
@@ -931,6 +1008,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr "å¥åº·æª¢æŸ¥ (Health Check)"
@@ -974,6 +1096,12 @@ msgid "Instance"
msgid_plural "Instances"
msgstr[0] ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "循環週期"
@@ -1041,6 +1169,9 @@ msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "æµæ°´ç·šè¨ˆåŠƒæ–‡æª”"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "退出群組"
@@ -1063,6 +1194,12 @@ msgstr ""
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•¸"
@@ -1093,6 +1230,9 @@ msgstr "幫助文檔"
msgid "Multiple issue boards"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建議題"
@@ -1109,18 +1249,27 @@ msgstr "新增目錄"
msgid "New file"
msgstr "新增文件"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "新議題"
msgid "New merge request"
msgstr "新增åˆä½µè«‹æ±‚"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "新增计划"
msgid "New snippet"
msgstr "新代碼片段"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "新增標籤"
@@ -1199,6 +1348,12 @@ msgstr "關注"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
@@ -1340,9 +1495,54 @@ msgstr "於階段"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "項目 '%{project_name}' 已進入刪除隊列。"
@@ -1352,9 +1552,6 @@ msgstr "é …ç›® '%{project_name}' 已創建æˆåŠŸã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "é …ç›® '%{project_name}' 已更新完æˆã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "項目 '%{project_name}' 將被刪除。"
-
msgid "Project access must be granted explicitly to each user."
msgstr "項目訪å•æ¬Šé™å¿…須明確授權給æ¯å€‹ç”¨æˆ¶ã€‚"
@@ -1412,6 +1609,12 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1433,12 +1636,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr "推é€äº‹ä»¶ (push event) "
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "了解更多"
@@ -1502,6 +1714,9 @@ msgstr "還原此åˆä½µè«‹æ±‚"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1520,6 +1735,15 @@ msgstr "æµæ°´ç·šè¨ˆåŠƒ"
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ¨™ç±¤"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
@@ -1566,13 +1790,16 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Sort by"
msgstr ""
msgid "SortOptions|Access level, ascending"
@@ -1692,6 +1919,12 @@ msgstr "由此更改 %{new_merge_request}"
msgid "Start the Runner!"
msgstr "é‹ä½œ Runner!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯/標籤"
@@ -1717,6 +1950,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "編碼階段概述了從第壹次æ交到創建åˆä½µè«‹æ±‚的時間。創建第壹個åˆä½µè«‹æ±‚後,數據將自動添加到此處。"
@@ -1729,6 +1965,15 @@ msgstr "派生關係已被刪除。"
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "議題階段概述了從創建議題到將議題添加到è£ç¨‹ç¢‘或議題看æ¿æ‰€èŠ±è²»çš„時間。創建第壹個議題後,數據將自動添加到此處.。"
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "項目生命週期中的å„個階段。"
@@ -1759,6 +2004,12 @@ msgstr "é ç™¼å¸ƒéšŽæ®µæ¦‚述了åˆä½µè«‹æ±‚çš„åˆä½µåˆ°éƒ¨ç½²ä»£ç¢¼åˆ°ç”Ÿç”¢ç’°
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "測試階段概述了 GitLab CI 為相關åˆä½µè«‹æ±‚é‹è¡Œæ¯å€‹æµæ°´ç·šæ‰€éœ€çš„時間。當第壹個æµæ°´ç·šé‹è¡Œå®Œæˆå¾Œï¼Œæ•¸æ“šå°‡è‡ªå‹•æ·»åŠ åˆ°æ­¤è™•ã€‚"
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "該階段æ¯æ¢æ•¸æ“šæ‰€èŠ±çš„時間"
@@ -1768,6 +2019,9 @@ msgstr "中ä½æ•¸æ˜¯å£¹å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "è¨ªå• Git 存儲時出ç¾å•é¡Œï¼š"
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -1950,6 +2204,9 @@ msgstr ""
msgid "Unstar"
msgstr "å–消星標"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
@@ -2013,6 +2270,9 @@ msgstr ""
msgid "Weight"
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -2133,9 +2393,21 @@ msgstr "å³å°‡åˆªé™¤èˆ‡æºé …ç›® %{forked_from_project} 的派生關系。確定
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å³å°‡ %{project_name_with_namespace} 轉義給å¦å£¹å€‹æ‰€æœ‰è€…。確定繼續嗎?"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "您已é”到項目數é‡é™åˆ¶"
@@ -2169,6 +2441,9 @@ msgstr "在賬號中 %{add_ssh_key_link} 之å‰å°‡ç„¡æ³•é€šéŽ SSH 拉å–或推é
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "您的åå­—"
@@ -2192,9 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "父級"
-msgid "to help your contributors communicate effectively!"
+msgid "password"
msgstr ""
msgid "personal access token"
msgstr ""
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index d0c852f35ff..efef86671f0 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 22:39+0200\n"
-"PO-Revision-Date: 2017-10-17 13:01-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
@@ -22,7 +22,7 @@ msgstr[0] "%d 個更動 (commit)"
msgid "%d layer"
msgid_plural "%d layers"
-msgstr[0] ""
+msgstr[0] "%d 個圖層"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -31,6 +31,10 @@ msgstr[0] "因效能考é‡ï¼Œä¸é¡¯ç¤º %s 個更動 (commit)。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} 在 %{commit_timeago} é€äº¤"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} 個è½å¾Œ %{default_branch} 分支的修訂版æ交,%{number_commits_ahead} 個超å‰çš„修訂版æ交"
@@ -50,6 +54,12 @@ msgstr[0] "%{storage_name}:已存å–此主機失敗 %{failed_attempts} 次"
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如何安è£è«‹åƒé–± %{link})"
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
@@ -58,7 +68,7 @@ msgid "1st contribution!"
msgstr "第一次å”作"
msgid "2FA enabled"
-msgstr ""
+msgstr "已啟用雙é‡èªè­‰"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
@@ -105,9 +115,18 @@ msgstr "請先新增 SSH 金鑰到您的個人帳號,æ‰èƒ½ä½¿ç”¨ SSH 來上å‚
msgid "Add new directory"
msgstr "新增目錄"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "全部"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr "外觀"
@@ -123,6 +142,9 @@ msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·š (pipeline) 排程嗎?"
msgid "Are you sure you want to discard your changes?"
msgstr "確定è¦æ”¾æ£„修改嗎?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "確定è¦é‡ç½®è¨»å†Šæ†‘è­‰ (registration token) 嗎?"
@@ -156,18 +178,21 @@ msgstr "è‡ªå‹•å¯©æŸ¥ç¨‹åº & 自動部屬程åºéœ€è¦ %{kubernetes} æ‰èƒ½æ­£å¸¸
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr "DevOps 自動化 (測試版)"
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr "為此專案啟用 「DevOps 自動化ã€ã€‚ 它將ä¾æ“šé è¨­çš„ CI/CD é…置自動構建ã€æ¸¬è©¦ã€éƒ¨å±¬æ‡‰ç”¨ç¨‹å¼ã€‚"
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "「DevOps 自動化〠文件"
msgid "AutoDevOps|Enable in settings"
msgstr "在設定中啟用"
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr "了解更多於 %{link_to_documentation}"
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Billing"
msgstr "方案"
@@ -229,6 +254,9 @@ msgstr[0] "分支 (branch) "
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "已建立分支 (branch) <strong>%{branch_name}</strong> 。如è¦è¨­å®šè‡ªå‹•éƒ¨ç½²ï¼Œ è«‹é¸æ“‡åˆé©çš„ GitLab CI Yaml 模æ¿ï¼Œç„¶å¾Œè¨˜å¾—è¦é€äº¤ (commit) 您的編輯內容。%{link_to_autodeploy_doc}\n"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœå°‹åˆ†æ”¯ (branches)"
@@ -439,15 +467,24 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "執行中"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr "克隆倉庫"
msgid "Close"
msgstr "關閉"
+msgid "Cluster"
+msgstr ""
+
msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
msgstr ""
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
msgid "ClusterIntegration|Cluster integration"
msgstr ""
@@ -490,27 +527,33 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
@@ -520,7 +563,10 @@ 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|Save changes"
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -532,18 +578,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -575,6 +615,10 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "更動記錄 (commit) "
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
msgid "Commit Message"
msgstr "更動訊æ¯"
@@ -656,6 +700,12 @@ msgstr "å”作指å—"
msgid "Contributors"
msgstr "å”作者"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr "複製 SSH 公鑰到剪貼簿"
@@ -677,9 +727,21 @@ msgstr "建立目錄"
msgid "Create empty bare repository"
msgstr "建立一個新的 bare repository"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "發出åˆä½µè«‹æ±‚ (merge request) "
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "建立..."
@@ -765,6 +827,9 @@ msgstr "目錄å稱"
msgid "Discard changes"
msgstr "放棄修改"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Dismiss Merge Request promotion"
msgstr "關閉åˆä½µè«‹æ±‚中的促銷廣告"
@@ -837,12 +902,18 @@ msgstr "æ¯é€±åŸ·è¡Œï¼ˆé€±æ—¥æ·©æ™¨ 四點)"
msgid "Explore projects"
msgstr "ç€è¦½é …ç›®"
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "無法變更所有權"
msgid "Failed to remove the pipeline schedule"
msgstr "無法刪除æµæ°´ç·š (pipeline) 排程"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "檔案"
@@ -886,9 +957,15 @@ msgstr "GPG 金鑰"
msgid "Geo Nodes"
msgstr "Geo 節點"
+msgid "Geo|File sync capacity"
+msgstr ""
+
msgid "Geo|Groups to replicate"
msgstr "è¦è¤‡è£½çš„群組"
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
msgid "Geo|Select groups to replicate."
msgstr "é¸æ“‡æ¬²è¤‡è£½ä¹‹ç¾¤çµ„。"
@@ -931,6 +1008,51 @@ msgstr "無法調用父組的 \"共享群組鎖\",僅父群組的所有者æ‰å
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr "從 %{ancestor_group_name} 中移除共享群組鎖"
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr "å¥åº·æª¢æŸ¥"
@@ -974,6 +1096,12 @@ msgid "Instance"
msgid_plural "Instances"
msgstr[0] "實例"
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "循環週期"
@@ -1041,6 +1169,9 @@ msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "æµæ°´ç·š (pipeline) 排程說明文件"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "退出群組"
@@ -1063,6 +1194,12 @@ msgstr "鎖定"
msgid "Locked Files"
msgstr "被鎖定檔案"
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•¸"
@@ -1093,6 +1230,9 @@ msgstr "å¥åº·æª¢æŸ¥"
msgid "Multiple issue boards"
msgstr "多個å•é¡Œçœ‹æ¿"
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "建立議題 (issue) "
@@ -1109,18 +1249,27 @@ msgstr "新增目錄"
msgid "New file"
msgstr "新增檔案"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "新增議題 (issue) "
msgid "New merge request"
msgstr "新增åˆä½µè«‹æ±‚ (merge request) "
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "新增排程"
msgid "New snippet"
msgstr "新文字片段"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "新增標籤"
@@ -1199,6 +1348,12 @@ msgstr "關注"
msgid "Notifications"
msgstr "通知"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
@@ -1340,9 +1495,54 @@ msgstr "於階段"
msgid "Preferences"
msgstr "å好設定"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr "個人資料"
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "專案 '%{project_name}' 已加入刪除佇列。"
@@ -1352,9 +1552,6 @@ msgstr "專案 '%{project_name}' 建立完æˆã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "專案 '%{project_name}' 更新完æˆã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "專案 '%{project_name}' 將被刪除。"
-
msgid "Project access must be granted explicitly to each user."
msgstr "專案權é™å¿…須一一指派給æ¯å€‹ä½¿ç”¨è€…。"
@@ -1412,6 +1609,12 @@ msgstr "此設定已經套用於伺æœå™¨ç´šåˆ¥ï¼Œä½†å·²ç¶“在這個專案被覆
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr "此設定將套用於所有專案,除éžè¢«ç®¡ç†å“¡è¦†å¯«ã€‚"
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr "經常使用"
@@ -1433,12 +1636,21 @@ msgstr "抱歉,沒有符åˆæœå°‹æ¢ä»¶çš„專案"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "此功能需è¦ç€è¦½å™¨æ”¯æ´ localStorage"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr "æŽ¨é€ [Push] è¦å‰‡"
msgid "Push events"
msgstr "æŽ¨é€ (push) 事件"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "瞭解更多"
@@ -1502,6 +1714,9 @@ msgstr "還原此åˆä½µè«‹æ±‚ (merge request) "
msgid "SSH Keys"
msgstr "SSH 金鑰"
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr "儲存變更"
@@ -1520,6 +1735,15 @@ msgstr "æµæ°´ç·š (pipeline) 排程"
msgid "Search branches and tags"
msgstr "æœå°‹åˆ†æ”¯ (branch) 和標籤"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
@@ -1566,14 +1790,17 @@ msgstr "文字片段"
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "有個地方出錯了,因為他嘗試去變更 ${this.issuableDisplayName(this.issuableType)} 的鎖定狀態。"
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
-msgstr "有個地方出錯了,因為他嘗試去變更 ${this.issuableDisplayName(this.issuableType)} 的鎖定狀態。"
+msgid "Sort by"
+msgstr ""
msgid "SortOptions|Access level, ascending"
msgstr "訪å•ç´šåˆ¥ã€å‡å†ªæŽ’列"
@@ -1692,6 +1919,12 @@ msgstr "以這些改動建立一個新的 %{new_merge_request} "
msgid "Start the Runner!"
msgstr "å•Ÿå‹• Runner!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯ (branch) 或標籤"
@@ -1717,6 +1950,9 @@ msgstr "æ„Ÿè¬ï¼è«‹ä¸è¦å†æ¬¡é¡¯ç¤º"
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr "GitLab 的進階全局æœå°‹åŠŸèƒ½æ˜¯éžå¸¸å¼·å¤§çš„æœå°‹æœå‹™ã€‚您å¯ä»¥æœå°‹å…¶ä»–團隊的代碼以幫助您完善自己項目中的代碼。從而é¿å…建立é‡è¤‡çš„代碼浪費時間。"
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•è¨˜éŒ„ (commit) 到建立åˆä½µè«‹æ±‚ (merge request) 的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
@@ -1729,6 +1965,15 @@ msgstr "åˆ†æ”¯èˆ‡ä¸»å¹¹é–“çš„é—œè¯ (fork relationship) 已被刪除。"
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "è­°é¡Œ (issue) éšŽæ®µé¡¯ç¤ºå¾žè­°é¡Œå»ºç«‹åˆ°è¨­å®šé‡Œç¨‹ç¢‘æ‰€èŠ±çš„æ™‚é–“ï¼Œæˆ–æ˜¯è­°é¡Œè¢«åˆ†é¡žåˆ°è­°é¡Œçœ‹æ¿ (issue board) 中所花的時間。建立第一個議題後,資料將自動填入。"
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "專案開發週期的å„個階段。"
@@ -1759,6 +2004,12 @@ msgstr "試營é‹æ®µé¡¯ç¤ºå¾žåˆä½µè«‹æ±‚ (merge request) 被åˆä½µå¾Œè‡³éƒ¨ç½²ç
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "測試階段顯示相關åˆä½µè«‹æ±‚ (merge request) çš„æµæ°´ç·š (pipeline) 所花的時間。當第一個æµæ°´ç·š (pipeline) 執行完畢後,資料將自動填入。"
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "該階段中æ¯ä¸€å€‹è³‡æ–™é …目所花的時間。"
@@ -1768,6 +2019,9 @@ msgstr "中ä½æ•¸æ˜¯ä¸€å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "å­˜å– Git 儲存空間時出ç¾å•é¡Œï¼š"
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This is a confidential issue."
msgstr "這是個隱密å•é¡Œã€‚"
@@ -1950,6 +2204,9 @@ msgstr "已解鎖"
msgid "Unstar"
msgstr "å–消收è—"
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "å‡ç´šæ‚¨çš„方案以啟用進階全局æœå°‹ã€‚"
@@ -2013,6 +2270,9 @@ msgstr "如果有新的推é€æˆ–新的议题,Webhook 将自动触å‘您设置
msgid "Weight"
msgstr "權é‡"
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr "Wiki"
@@ -2133,9 +2393,21 @@ msgstr "å°‡è¦åˆªé™¤æœ¬åˆ†æ”¯å°ˆæ¡ˆèˆ‡ä¸»å¹¹ %{forked_from_project} 的所有關
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å°‡è¦æŠŠ %{project_name_with_namespace} 的所有權轉移給å¦ä¸€å€‹äººã€‚真的「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ (branch) 上建立檔案"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "您已é”到專案數é‡é™åˆ¶"
@@ -2169,6 +2441,9 @@ msgstr "在個人帳號中 %{add_ssh_key_link} 之å‰ï¼Œ 將無法使用 SSH 上
msgid "Your comment will not be visible to the public."
msgstr "你的留言將ä¸æœƒè¢«å…¬é–‹ã€‚"
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "您的åå­—"
@@ -2192,9 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "上層"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
msgid "to help your contributors communicate effectively!"
msgstr "幫助你的貢ç»è€…進行有效的æºé€šï¼"
-msgid "personal access token"
+msgid "username"
msgstr ""
diff --git a/package.json b/package.json
index 057cd8f7bc7..16a6e45e820 100644
--- a/package.json
+++ b/package.json
@@ -13,13 +13,16 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
+ "autosize": "^4.0.0",
"axios": "^0.16.2",
+ "axios-mock-adapter": "^1.10.0",
"babel-core": "^6.22.1",
"babel-eslint": "^7.2.1",
"babel-loader": "^7.1.1",
"babel-plugin-transform-define": "^1.2.0",
"babel-preset-latest": "^6.24.0",
"babel-preset-stage-2": "^6.22.0",
+ "blackst0ne-mermaid": "^7.1.0-fixed",
"bootstrap-sass": "^3.3.6",
"brace-expansion": "^1.1.8",
"compression-webpack-plugin": "^1.0.0",
@@ -35,6 +38,7 @@
"eslint-plugin-html": "^2.0.1",
"exports-loader": "^0.6.4",
"file-loader": "^0.11.1",
+ "fuzzaldrin-plus": "^0.5.0",
"imports-loader": "^0.7.1",
"jed": "^1.1.1",
"jquery": "^2.2.1",
@@ -62,16 +66,17 @@
"underscore": "^1.8.3",
"url-loader": "^0.5.8",
"visibilityjs": "^1.2.4",
- "vue": "^2.2.6",
+ "vue": "^2.5.2",
"vue-loader": "^11.3.4",
"vue-resource": "^1.3.4",
- "vue-template-compiler": "^2.2.6",
+ "vue-template-compiler": "^2.5.2",
"vuex": "^3.0.0",
"webpack": "^3.5.5",
"webpack-bundle-analyzer": "^2.8.2",
"webpack-stats-plugin": "^0.1.5"
},
"devDependencies": {
+ "@gitlab-org/gitlab-svgs": "^1.0.2",
"babel-plugin-istanbul": "^4.0.0",
"eslint": "^3.10.1",
"eslint-config-airbnb-base": "^10.0.1",
@@ -80,7 +85,6 @@
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
"eslint-plugin-promise": "^3.5.0",
- "gitlab-svgs": "https://gitlab.com/gitlab-org/gitlab-svgs.git",
"istanbul": "^0.4.5",
"jasmine-core": "^2.6.3",
"jasmine-jquery": "^2.1.1",
diff --git a/qa/.gitignore b/qa/.gitignore
index 3fec32c8427..19ec17d0005 100644
--- a/qa/.gitignore
+++ b/qa/.gitignore
@@ -1 +1,2 @@
tmp/
+.ruby-version
diff --git a/qa/Dockerfile b/qa/Dockerfile
index f3a81a7e355..9b6ffff7c4d 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -9,6 +9,13 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list
RUN apt-get update && apt-get install -y wget git unzip xvfb
##
+# Install Docker
+#
+RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \
+ tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \
+ rm docker-17.09.0-ce.tgz
+
+##
# Install Google Chrome version with headless support
#
RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
diff --git a/qa/bin/qa b/qa/bin/qa
index cecdeac14db..6a772e93cee 100755
--- a/qa/bin/qa
+++ b/qa/bin/qa
@@ -4,4 +4,4 @@ require_relative '../qa'
QA::Scenario
.const_get(ARGV.shift)
- .perform(*ARGV)
+ .launch!(ARGV)
diff --git a/qa/qa.rb b/qa/qa.rb
index 59d9dd131c2..dc1cd9abc6a 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -8,6 +8,7 @@ module QA
autoload :Release, 'qa/runtime/release'
autoload :User, 'qa/runtime/user'
autoload :Namespace, 'qa/runtime/namespace'
+ autoload :Scenario, 'qa/runtime/scenario'
end
##
@@ -17,6 +18,7 @@ module QA
##
# Support files
#
+ autoload :Bootable, 'qa/scenario/bootable'
autoload :Actable, 'qa/scenario/actable'
autoload :Entrypoint, 'qa/scenario/entrypoint'
autoload :Template, 'qa/scenario/template'
@@ -60,6 +62,7 @@ module QA
module Main
autoload :Entry, 'qa/page/main/entry'
+ autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu'
end
@@ -80,6 +83,11 @@ module QA
module Admin
autoload :Menu, 'qa/page/admin/menu'
end
+
+ module Mattermost
+ autoload :Main, 'qa/page/mattermost/main'
+ autoload :Login, 'qa/page/mattermost/login'
+ end
end
##
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index b9e199000d6..59cd147e055 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -23,7 +23,7 @@ module QA
def password=(pass)
@password = pass
- @uri.password = pass
+ @uri.password = CGI.escape(pass)
end
def use_default_credentials
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index d55326c5262..bdddfb877c5 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -5,7 +5,7 @@ module QA
include Scenario::Actable
def refresh
- visit current_path
+ visit current_url
end
end
end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
index a9810beeb29..ae6484b4bfe 100644
--- a/qa/qa/page/main/entry.rb
+++ b/qa/qa/page/main/entry.rb
@@ -2,29 +2,23 @@ module QA
module Page
module Main
class Entry < Page::Base
- def initialize
- visit('/')
+ def visit_login_page
+ visit("#{Runtime::Scenario.gitlab_address}/users/sign_in")
+ wait_for_instance_to_be_ready
+ end
+
+ private
+ def wait_for_instance_to_be_ready
# This resolves cold boot / background tasks problems
#
start = Time.now
while Time.now - start < 240
break if page.has_css?('.application', wait: 10)
- refresh
- end
- end
- def sign_in_using_credentials
- if page.has_content?('Change your password')
- fill_in :user_password, with: Runtime::User.password
- fill_in :user_password_confirmation, with: Runtime::User.password
- click_button 'Change your password'
+ refresh
end
-
- fill_in :user_login, with: Runtime::User.name
- fill_in :user_password, with: Runtime::User.password
- click_button 'Sign in'
end
end
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
new file mode 100644
index 00000000000..8b0111a78a2
--- /dev/null
+++ b/qa/qa/page/main/login.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Main
+ class Login < Page::Base
+ def sign_in_using_credentials
+ if page.has_content?('Change your password')
+ fill_in :user_password, with: Runtime::User.password
+ fill_in :user_password_confirmation, with: Runtime::User.password
+ click_button 'Change your password'
+ end
+
+ fill_in :user_login, with: Runtime::User.name
+ fill_in :user_password, with: Runtime::User.password
+ click_button 'Sign in'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb
new file mode 100644
index 00000000000..42ab9c6f675
--- /dev/null
+++ b/qa/qa/page/mattermost/login.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Mattermost
+ class Login < Page::Base
+ def initialize
+ visit(Runtime::Scenario.mattermost_address + '/login')
+ end
+
+ def sign_in_using_oauth
+ click_link class: 'btn btn-custom-login gitlab'
+
+ if page.has_content?('Authorize GitLab Mattermost to use your account?')
+ click_button 'Authorize'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb
new file mode 100644
index 00000000000..4b8fc28e53f
--- /dev/null
+++ b/qa/qa/page/mattermost/main.rb
@@ -0,0 +1,11 @@
+module QA
+ module Page
+ module Mattermost
+ class Main < Page::Base
+ def initialize
+ visit(Runtime::Scenario.mattermost_address)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb
new file mode 100644
index 00000000000..7ef59046640
--- /dev/null
+++ b/qa/qa/runtime/scenario.rb
@@ -0,0 +1,28 @@
+module QA
+ module Runtime
+ ##
+ # Singleton approach to global test scenario arguments.
+ #
+ module Scenario
+ extend self
+
+ attr_reader :attributes
+
+ def define(attribute, value)
+ (@attributes ||= {}).store(attribute.to_sym, value)
+
+ define_singleton_method(attribute) do
+ @attributes[attribute.to_sym].tap do |value|
+ if value.to_s.empty?
+ raise ArgumentError, "Empty `#{attribute}` attribute!"
+ end
+ end
+ end
+ end
+
+ def method_missing(name, *)
+ raise ArgumentError, "Scenario attribute `#{name}` not defined!"
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb
new file mode 100644
index 00000000000..cf8996cd597
--- /dev/null
+++ b/qa/qa/scenario/bootable.rb
@@ -0,0 +1,45 @@
+require 'optparse'
+
+module QA
+ module Scenario
+ module Bootable
+ Option = Struct.new(:name, :arg, :desc)
+
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def launch!(argv)
+ return self.perform(*argv) unless has_attributes?
+
+ arguments = OptionParser.new do |parser|
+ options.to_a.each do |opt|
+ parser.on(opt.arg, opt.desc) do |value|
+ Runtime::Scenario.define(opt.name, value)
+ end
+ end
+ end
+
+ arguments.parse!(argv)
+
+ self.perform(**Runtime::Scenario.attributes)
+ end
+
+ private
+
+ def attribute(name, arg, desc)
+ options.push(Option.new(name, arg, desc))
+ end
+
+ def options
+ @options ||= []
+ end
+
+ def has_attributes?
+ options.any?
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb
index 33cb2696f8f..b9d924651a0 100644
--- a/qa/qa/scenario/entrypoint.rb
+++ b/qa/qa/scenario/entrypoint.rb
@@ -5,18 +5,11 @@ module QA
# including staging and on-premises installation.
#
class Entrypoint < Template
- def self.tags(*tags)
- @tags = tags
- end
-
- def self.get_tags
- @tags
- end
+ include Bootable
def perform(address, *files)
- Specs::Config.perform do |specs|
- specs.address = address
- end
+ Specs::Config.act { configure_capybara! }
+ Runtime::Scenario.define(:gitlab_address, address)
##
# Perform before hooks, which are different for CE and EE
@@ -24,13 +17,19 @@ module QA
Runtime::Release.perform_before_hooks
Specs::Runner.perform do |specs|
- specs.rspec(
- tty: true,
- tags: self.class.get_tags,
- files: files.any? ? files : 'qa/specs/features'
- )
+ specs.tty = true
+ specs.tags = self.class.get_tags
+ specs.files = files.any? ? files : 'qa/specs/features'
end
end
+
+ def self.tags(*tags)
+ @tags = tags
+ end
+
+ def self.get_tags
+ @tags
+ end
end
end
end
diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb
index 4732f2b635b..7d0702afdb1 100644
--- a/qa/qa/scenario/test/integration/mattermost.rb
+++ b/qa/qa/scenario/test/integration/mattermost.rb
@@ -8,6 +8,12 @@ module QA
#
class Mattermost < Scenario::Entrypoint
tags :core, :mattermost
+
+ def perform(address, mattermost, *files)
+ Runtime::Scenario.define(:mattermost_address, mattermost)
+
+ super(address, *files)
+ end
end
end
end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
index 79c681168cc..bce7923e52d 100644
--- a/qa/qa/specs/config.rb
+++ b/qa/qa/specs/config.rb
@@ -9,15 +9,9 @@ require 'selenium-webdriver'
module QA
module Specs
class Config < Scenario::Template
- attr_writer :address
-
- def initialize
- @address = ENV['GITLAB_URL']
- end
+ include Scenario::Actable
def perform
- raise 'Please configure GitLab address!' unless @address
-
configure_rspec!
configure_capybara!
end
@@ -56,10 +50,9 @@ module QA
end
Capybara.configure do |config|
- config.app_host = @address
config.default_driver = :chrome
config.javascript_driver = :chrome
- config.default_max_wait_time = 4
+ config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = 'tmp'
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
index ba19ce17ee5..b155708c387 100644
--- a/qa/qa/specs/features/login/standard_spec.rb
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -1,7 +1,8 @@
module QA
feature 'standard root login', :core do
scenario 'user logs in using credentials' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb
index c4afd83c8e4..853a9a6a4f4 100644
--- a/qa/qa/specs/features/mattermost/group_create_spec.rb
+++ b/qa/qa/specs/features/mattermost/group_create_spec.rb
@@ -1,7 +1,8 @@
module QA
feature 'create a new group', :mattermost do
scenario 'creating a group with a mattermost team' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb
new file mode 100644
index 00000000000..1fde3f89a99
--- /dev/null
+++ b/qa/qa/specs/features/mattermost/login_spec.rb
@@ -0,0 +1,24 @@
+module QA
+ feature 'logging in to Mattermost', :mattermost do
+ scenario 'can use gitlab oauth' do
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
+ Page::Mattermost::Login.act { sign_in_using_oauth }
+
+ Page::Mattermost::Main.perform do |page|
+ expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
+ end
+ end
+
+ ##
+ # TODO, temporary workaround for gitlab-org/gitlab-qa#102.
+ #
+ after do
+ visit Runtime::Scenario.mattermost_address
+ reset_session!
+
+ visit Runtime::Scenario.gitlab_address
+ reset_session!
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
index 27eb22f15a6..aba0c2b4c14 100644
--- a/qa/qa/specs/features/project/create_spec.rb
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -1,7 +1,8 @@
module QA
feature 'create a new project', :core do
scenario 'user creates a new project' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |project|
project.name = 'awesome-project'
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index 3571173783d..5cc3b3b9c1b 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -9,7 +9,8 @@ module QA
end
before do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project-with-code'
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index 0e691fb0d75..30935dc1e13 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -2,7 +2,8 @@ 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 { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project_with_code'
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 2aa18d5d3a1..f98b8f88e9a 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -2,16 +2,22 @@ require 'rspec/core'
module QA
module Specs
- class Runner
- include Scenario::Actable
+ class Runner < Scenario::Template
+ attr_accessor :tty, :tags, :files
- def rspec(tty: false, tags: [], files: ['qa/specs/features'])
+ def initialize
+ @tty = false
+ @tags = []
+ @files = ['qa/specs/features']
+ end
+
+ def perform
args = []
- args << '--tty' if tty
- tags.to_a.each do |tag|
- args << ['-t', tag.to_s]
- end
- args << files
+ args.push('--tty') if tty
+ tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
+ args.push(files)
+
+ Specs::Config.perform
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
diff --git a/qa/spec/runtime/scenario_spec.rb b/qa/spec/runtime/scenario_spec.rb
new file mode 100644
index 00000000000..7009192bcc0
--- /dev/null
+++ b/qa/spec/runtime/scenario_spec.rb
@@ -0,0 +1,27 @@
+describe QA::Runtime::Scenario do
+ subject do
+ Module.new.extend(described_class)
+ end
+
+ it 'makes it possible to define global scenario attributes' do
+ subject.define(:my_attribute, 'some-value')
+ subject.define(:another_attribute, 'another-value')
+
+ expect(subject.my_attribute).to eq 'some-value'
+ expect(subject.another_attribute).to eq 'another-value'
+ expect(subject.attributes)
+ .to eq(my_attribute: 'some-value', another_attribute: 'another-value')
+ end
+
+ it 'raises error when attribute is not known' do
+ expect { subject.invalid_accessor }
+ .to raise_error ArgumentError, /invalid_accessor/
+ end
+
+ it 'raises error when attribute is empty' do
+ subject.define(:empty_attribute, '')
+
+ expect { subject.empty_attribute }
+ .to raise_error ArgumentError, /empty_attribute/
+ end
+end
diff --git a/qa/spec/scenario/bootable_spec.rb b/qa/spec/scenario/bootable_spec.rb
new file mode 100644
index 00000000000..273aac7677e
--- /dev/null
+++ b/qa/spec/scenario/bootable_spec.rb
@@ -0,0 +1,24 @@
+describe QA::Scenario::Bootable do
+ subject do
+ Class.new(QA::Scenario::Template)
+ .include(described_class)
+ end
+
+ it 'makes it possible to define the scenario attribute' do
+ subject.class_eval do
+ attribute :something, '--something SOMETHING', 'Some attribute'
+ attribute :another, '--another ANOTHER', 'Some other attribute'
+ end
+
+ expect(subject).to receive(:perform)
+ .with(something: 'test', another: 'other')
+
+ subject.launch!(%w[--another other --something test])
+ end
+
+ it 'does not require attributes to be defined' do
+ expect(subject).to receive(:perform).with('some', 'argv')
+
+ subject.launch!(%w[some argv])
+ end
+end
diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/entrypoint_spec.rb
new file mode 100644
index 00000000000..aec79dcea04
--- /dev/null
+++ b/qa/spec/scenario/entrypoint_spec.rb
@@ -0,0 +1,44 @@
+describe QA::Scenario::Entrypoint do
+ subject do
+ Class.new(QA::Scenario::Entrypoint) do
+ tags :rspec
+ end
+ end
+
+ context '#perform' do
+ let(:arguments) { spy('Runtime::Scenario') }
+ let(:release) { spy('Runtime::Release') }
+ let(:runner) { spy('Specs::Runner') }
+
+ before do
+ stub_const('QA::Runtime::Release', release)
+ stub_const('QA::Runtime::Scenario', arguments)
+ stub_const('QA::Specs::Runner', runner)
+
+ allow(runner).to receive(:perform).and_yield(runner)
+ end
+
+ it 'sets an address of the subject' do
+ subject.perform("hello")
+
+ expect(arguments).to have_received(:define)
+ .with(:gitlab_address, "hello")
+ end
+
+ context 'no paths' do
+ it 'should call runner with default arguments' do
+ subject.perform("test")
+
+ expect(runner).to have_received(:files=).with('qa/specs/features')
+ end
+ end
+
+ context 'specifying paths' do
+ it 'should call runner with paths' do
+ subject.perform('test', 'path1', 'path2')
+
+ expect(runner).to have_received(:files=).with(%w[path1 path2])
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/line_break_after_guard_clauses.rb b/rubocop/cop/line_break_after_guard_clauses.rb
new file mode 100644
index 00000000000..67477f064ab
--- /dev/null
+++ b/rubocop/cop/line_break_after_guard_clauses.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Ensures a line break after guard clauses.
+ #
+ # @example
+ # # bad
+ # return unless condition
+ # do_stuff
+ #
+ # # good
+ # return unless condition
+ #
+ # do_stuff
+ #
+ # # bad
+ # raise if condition
+ # do_stuff
+ #
+ # # good
+ # raise if condition
+ #
+ # do_stuff
+ #
+ # Multiple guard clauses are allowed without
+ # line break.
+ #
+ # # good
+ # return unless condition_a
+ # return unless condition_b
+ #
+ # do_stuff
+ #
+ # Guard clauses in case statement are allowed without
+ # line break.
+ #
+ # # good
+ # case model
+ # when condition_a
+ # return true unless condition_b
+ # when
+ # ...
+ # end
+ #
+ # Guard clauses before end are allowed without
+ # line break.
+ #
+ # # good
+ # if condition_a
+ # do_something
+ # else
+ # do_something_else
+ # return unless condition
+ # end
+ #
+ # do_something_more
+ class LineBreakAfterGuardClauses < RuboCop::Cop::Cop
+ MSG = 'Add a line break after guard clauses'
+
+ def_node_matcher :guard_clause_node?, <<-PATTERN
+ [{(send nil? {:raise :fail :throw} ...) return break next} single_line?]
+ PATTERN
+
+ def on_if(node)
+ return unless node.single_line?
+ return unless guard_clause?(node)
+ return if next_line(node).blank? || clause_last_line?(next_line(node)) || guard_clause?(next_sibling(node))
+
+ add_offense(node, :expression, MSG)
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ corrector.insert_after(node.loc.expression, "\n")
+ end
+ end
+
+ private
+
+ def guard_clause?(node)
+ return false unless node.if_type?
+
+ guard_clause_node?(node.if_branch)
+ end
+
+ def next_sibling(node)
+ node.parent.children[node.sibling_index + 1]
+ end
+
+ def next_line(node)
+ processed_source[node.loc.line]
+ end
+
+ def clause_last_line?(line)
+ line =~ /^\s*(?:end|elsif|else|when|rescue|ensure)/
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/add_column_with_default_to_large_table.rb b/rubocop/cop/migration/update_large_table.rb
index fb363f95b56..3ae3fb1b68e 100644
--- a/rubocop/cop/migration/add_column_with_default_to_large_table.rb
+++ b/rubocop/cop/migration/update_large_table.rb
@@ -12,12 +12,12 @@ module RuboCop
#
# See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more
# information.
- class AddColumnWithDefaultToLargeTable < RuboCop::Cop::Cop
+ class UpdateLargeTable < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = 'Using `add_column_with_default` on the `%s` table will take a ' \
- 'long time to complete, and should be avoided unless absolutely ' \
- 'necessary'.freeze
+ MSG = 'Using `%s` on the `%s` table will take a long time to ' \
+ 'complete, and should be avoided unless absolutely ' \
+ 'necessary'.freeze
LARGE_TABLES = %i[
ci_pipelines
@@ -34,20 +34,22 @@ module RuboCop
users
].freeze
- def_node_matcher :add_column_with_default?, <<~PATTERN
- (send nil :add_column_with_default $(sym ...) ...)
+ def_node_matcher :batch_update?, <<~PATTERN
+ (send nil ${:add_column_with_default :update_column_in_batches} $(sym ...) ...)
PATTERN
def on_send(node)
return unless in_migration?(node)
- matched = add_column_with_default?(node)
- return unless matched
+ matches = batch_update?(node)
+ return unless matches
+
+ update_method = matches.first
+ table = matches.last.to_a.first
- table = matched.to_a.first
return unless LARGE_TABLES.include?(table)
- add_offense(node, :expression, format(MSG, table))
+ add_offense(node, :expression, format(MSG, update_method, table))
end
end
end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 4ebbe010e90..7621ea50da9 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -3,11 +3,11 @@ 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/migration/add_column'
-require_relative 'cop/migration/add_column_with_default_to_large_table'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
@@ -20,6 +20,7 @@ require_relative 'cop/migration/reversible_add_column_with_default'
require_relative 'cop/migration/safer_boolean_column'
require_relative 'cop/migration/timestamps'
require_relative 'cop/migration/update_column_in_batches'
+require_relative 'cop/migration/update_large_table'
require_relative 'cop/rspec/env_assignment'
require_relative 'cop/rspec/single_line_hook'
require_relative 'cop/rspec/verbose_include_metadata'
diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh
new file mode 100644
index 00000000000..28f6cfb50ae
--- /dev/null
+++ b/scripts/create_mysql_user.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+mysql --user=root --host=mysql <<EOF
+CREATE DATABASE IF NOT EXISTS gitlabhq_test;
+CREATE USER IF NOT EXISTS 'gitlab'@'%';
+GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
+FLUSH PRIVILEGES;
+EOF
diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh
new file mode 100644
index 00000000000..8a744df3226
--- /dev/null
+++ b/scripts/create_postgres_user.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+psql -h postgres -U postgres postgres <<EOF
+DROP DATABASE IF EXISTS gitlabhq_test;
+CREATE DATABASE gitlabhq_test;
+CREATE USER gitlab;
+GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
+EOF
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 7abadef5e89..36bcf087cd9 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,6 +1,7 @@
. scripts/utils.sh
export SETUP_DB=${SETUP_DB:-true}
+export CREATE_DB_USER=${CREATE_DB_USER:-$SETUP_DB}
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
@@ -26,6 +27,9 @@ fi
cp config/database.yml.$GITLAB_DATABASE config/database.yml
+# Set user to a non-superuser to ensure we test permissions
+sed -i 's/username: root/username: gitlab/g' config/database.yml
+
if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
sed -i 's/localhost/postgres/g' config/database.yml
else # Assume it's mysql
@@ -44,6 +48,16 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml
+# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
+# user but not necessarily a full schema loaded
+if [ "$CREATE_DB_USER" != "false" ]; then
+ if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
+ . scripts/create_postgres_user.sh
+ else
+ . scripts/create_mysql_user.sh
+ fi
+fi
+
if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate
diff --git a/scripts/static-analysis b/scripts/static-analysis
index aeefb2bc96f..51a2fd81a79 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -11,31 +11,40 @@ tasks = [
%w[bundle exec rake brakeman],
%w[bundle exec license_finder],
%w[yarn run eslint],
- %w[bundle exec rubocop --require rubocop-rspec],
+ %w[bundle exec rubocop --parallel],
%w[scripts/lint-conflicts.sh],
%w[bundle exec rake gettext:lint],
%w[scripts/lint-changelog-yaml]
]
failed_tasks = tasks.reduce({}) do |failures, task|
- output, status = Gitlab::Popen.popen(task)
+ start = Time.now
+ puts
+ puts "$ #{task.join(' ')}"
- puts "Running: #{task.join(' ')}"
- puts output
+ output, status = Gitlab::Popen.popen(task)
+ puts "==> Finished in #{Time.now - start} seconds"
+ puts
failures[task.join(' ')] = output unless status.zero?
failures
end
+puts
+puts '==================================================='
+puts
+puts
+
if failed_tasks.empty?
puts 'All static analyses passed successfully.'
else
- puts "\n===================================================\n\n"
- puts "Some static analyses failed:"
+ puts 'Some static analyses failed:'
failed_tasks.each do |failed_task, output|
- puts "\n**** #{failed_task} failed with the following error:\n\n"
+ puts
+ puts "**** #{failed_task} failed with the following error:"
+ puts
puts output
end
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index d3a9f5ff4ea..a270823b857 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -27,14 +27,7 @@ def docs_branch
# Prefix the remote branch with 'preview-' in order to avoid
# name conflicts in the rare case the branch name already
# exists in the docs repo and truncate to max length.
- "preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
-end
-
-#
-# Dummy way to find out in which repo we are, CE or EE
-#
-def ee?
- File.exist?('CHANGELOG-EE.md')
+ "#{slug}-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
end
#
@@ -56,14 +49,34 @@ def remove_remote_branch
end
#
+# Define suffix in review app URL based on project
+#
+def slug
+ case ENV["CI_PROJECT_NAME"]
+ when 'gitlab-ce'
+ 'ce'
+ when 'gitlab-ee'
+ 'ee'
+ when 'gitlab-runner'
+ 'runner'
+ when 'omnibus-gitlab'
+ 'omnibus'
+ end
+end
+
+#
+# Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml
+#
+def param_name
+ "BRANCH_#{slug.upcase}"
+end
+
+#
# Trigger a pipeline in gitlab-docs
#
def trigger_pipeline
- # Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml
- param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE'
-
# The review app URL
- app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
+ app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{slug}"
# Create the pipeline
puts "=> Triggering a pipeline..."
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 6802b839eaa..768c7e99c96 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -6,6 +6,10 @@ describe ApplicationController do
describe '#check_password_expiration' do
let(:controller) { described_class.new }
+ before do
+ allow(controller).to receive(:session).and_return({})
+ end
+
it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002)
@@ -50,70 +54,36 @@ describe ApplicationController do
end
end
- describe "#authenticate_user_from_token!" do
- describe "authenticating a user from a private token" do
- controller(described_class) do
- def index
- render text: "authenticated"
- end
- end
-
- context "when the 'private_token' param is populated with the private token" do
- it "logs the user in" do
- get :index, private_token: user.private_token
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq("authenticated")
- end
- end
-
- context "when the 'PRIVATE-TOKEN' header is populated with the private token" do
- it "logs the user in" do
- @request.headers['PRIVATE-TOKEN'] = user.private_token
- get :index
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq("authenticated")
- end
- end
-
- it "doesn't log the user in otherwise" do
- @request.headers['PRIVATE-TOKEN'] = "token"
- get :index, private_token: "token", authenticity_token: "token"
- expect(response.status).not_to eq(200)
- expect(response.body).not_to eq("authenticated")
+ describe "#authenticate_user_from_personal_access_token!" do
+ controller(described_class) do
+ def index
+ render text: 'authenticated'
end
end
- describe "authenticating a user from a personal access token" do
- controller(described_class) do
- def index
- render text: 'authenticated'
- end
- end
-
- let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
- context "when the 'personal_access_token' param is populated with the personal access token" do
- it "logs the user in" do
- get :index, private_token: personal_access_token.token
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq('authenticated')
- end
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it "logs the user in" do
+ get :index, private_token: personal_access_token.token
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq('authenticated')
end
+ end
- context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
- it "logs the user in" do
- @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
- get :index
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq('authenticated')
- end
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it "logs the user in" do
+ @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
+ get :index
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq('authenticated')
end
+ end
- it "doesn't log the user in otherwise" do
- get :index, private_token: "token"
- expect(response.status).not_to eq(200)
- expect(response.body).not_to eq('authenticated')
- end
+ it "doesn't log the user in otherwise" do
+ get :index, private_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq('authenticated')
end
end
@@ -152,11 +122,15 @@ describe ApplicationController do
end
end
+ before do
+ sign_in user
+ end
+
context 'when format is handled' do
let(:requested_format) { :json }
it 'returns 200 response' do
- get :index, private_token: user.private_token, format: requested_format
+ get :index, format: requested_format
expect(response).to have_gitlab_http_status 200
end
@@ -164,7 +138,7 @@ describe ApplicationController do
context 'when format is not handled' do
it 'returns 404 response' do
- get :index, private_token: user.private_token
+ get :index
expect(response).to have_gitlab_http_status 404
end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index c9687af4dd2..d7825364ed5 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -12,71 +12,70 @@ describe IssuableCollections do
controller = klass.new
- allow(controller).to receive(:params).and_return(state: 'opened')
+ allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params))
controller
end
- describe '#redirect_out_of_range' do
- before do
- allow(controller).to receive(:url_for)
- end
-
- it 'returns true and redirects if the offset is out of range' do
- relation = double(:relation, current_page: 10)
-
- expect(controller).to receive(:redirect_to)
- expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(true)
- end
-
- it 'returns false if the offset is not out of range' do
- relation = double(:relation, current_page: 1)
-
- expect(controller).not_to receive(:redirect_to)
- expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(false)
- end
- end
-
- describe '#issues_page_count' do
- it 'returns the number of issue pages' do
- project = create(:project, :public)
-
- create(:issue, project: project)
-
- finder = IssuesFinder.new(user)
- issues = finder.execute
+ describe '#page_count_for_relation' do
+ let(:params) { { state: 'opened' } }
- allow(controller).to receive(:issues_finder)
- .and_return(finder)
+ it 'returns the number of pages' do
+ relation = double(:relation, limit_value: 20)
+ pages = controller.send(:page_count_for_relation, relation, 28)
- expect(controller.send(:issues_page_count, issues)).to eq(1)
+ expect(pages).to eq(2)
end
end
- describe '#merge_requests_page_count' do
- it 'returns the number of merge request pages' do
- project = create(:project, :public)
-
- create(:merge_request, source_project: project, target_project: project)
-
- finder = MergeRequestsFinder.new(user)
- merge_requests = finder.execute
-
- allow(controller).to receive(:merge_requests_finder)
- .and_return(finder)
-
- pages = controller.send(:merge_requests_page_count, merge_requests)
-
- expect(pages).to eq(1)
+ describe '#filter_params' do
+ let(:params) do
+ {
+ assignee_id: '1',
+ assignee_username: 'user1',
+ author_id: '2',
+ author_username: 'user2',
+ authorized_only: 'true',
+ due_date: '2017-01-01',
+ group_id: '3',
+ iids: '4',
+ label_name: 'foo',
+ milestone_title: 'bar',
+ my_reaction_emoji: 'thumbsup',
+ non_archived: 'true',
+ project_id: '5',
+ scope: 'all',
+ search: 'baz',
+ sort: 'priority',
+ state: 'opened',
+ invalid_param: 'invalid_param'
+ }
end
- end
- describe '#page_count_for_relation' do
- it 'returns the number of pages' do
- relation = double(:relation, limit_value: 20)
- pages = controller.send(:page_count_for_relation, relation, 28)
-
- expect(pages).to eq(2)
+ it 'filters params' do
+ allow(controller).to receive(:cookies).and_return({})
+
+ filtered_params = controller.send(:filter_params)
+
+ expect(filtered_params).to eq({
+ 'assignee_id' => '1',
+ 'assignee_username' => 'user1',
+ 'author_id' => '2',
+ 'author_username' => 'user2',
+ 'authorized_only' => 'true',
+ 'due_date' => '2017-01-01',
+ 'group_id' => '3',
+ 'iids' => '4',
+ 'label_name' => 'foo',
+ 'milestone_title' => 'bar',
+ 'my_reaction_emoji' => 'thumbsup',
+ 'non_archived' => 'true',
+ 'project_id' => '5',
+ 'scope' => 'all',
+ 'search' => 'baz',
+ 'sort' => 'priority',
+ 'state' => 'opened'
+ })
end
end
end
diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb
new file mode 100644
index 00000000000..33b23db302a
--- /dev/null
+++ b/spec/controllers/concerns/lfs_request_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe LfsRequest do
+ include ProjectForksHelper
+
+ controller(Projects::GitHttpClientController) do
+ # `described_class` is not available in this context
+ include LfsRequest # rubocop:disable RSpec/DescribedClass
+
+ def show
+ storage_project
+
+ render nothing: true
+ end
+
+ def project
+ @project ||= Project.find(params[:id])
+ end
+
+ def download_request?
+ true
+ end
+
+ def ci?
+ false
+ end
+ end
+
+ let(:project) { create(:project, :public) }
+
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ describe '#storage_project' do
+ it 'assigns the project as storage project' do
+ get :show, id: project.id
+
+ expect(assigns(:storage_project)).to eq(project)
+ end
+
+ it 'assigns the source of a forked project' do
+ forked_project = fork_project(project)
+
+ get :show, id: forked_project.id
+
+ expect(assigns(:storage_project)).to eq(project)
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index d862e1447e3..f9faa4fa59a 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -44,11 +44,11 @@ describe Dashboard::TodosController do
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
- let!(:issues) { create_list(:issue, 2, project: project, assignees: [user]) }
+ let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
it 'redirects to last_page if page number is larger than number of pages' do
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index 4262d474e59..cb1b460fc0e 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -280,6 +280,17 @@ describe Groups::ChildrenController do
expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1))
end
+
+ context 'with a mixed first page' do
+ let!(:first_page_subgroups) { [create(:group, :public, parent: group)] }
+ let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) }
+
+ it 'correctly calculates the counts' do
+ get :index, group_id: group.to_param, sort: 'id_asc', page: 2, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 45c3fa075ef..9bbd97ec305 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -21,9 +21,9 @@ describe Import::GithubController do
describe "GET callback" do
it "updates access token" do
token = "asdasd12345"
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:get_token).and_return(token)
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:github_options).and_return({})
stub_omniauth_provider('github')
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 7b0976e3e67..9e8a37171ec 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -59,17 +59,6 @@ describe MetricsController do
expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/)
end
- it 'returns file system check metrics' do
- get :index
-
- expect(response.body).to match(/^filesystem_access_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/)
- expect(response.body).to match(/^filesystem_write_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/)
- expect(response.body).to match(/^filesystem_read_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/)
- end
-
context 'prometheus metrics are disabled' do
before do
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false)
@@ -78,7 +67,8 @@ describe MetricsController do
it 'returns proper response' do
get :index
- expect(response.status).to eq(404)
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("# Metrics are disabled, see: http://test.host/help/administration/monitoring/prometheus/gitlab_metrics#gitlab-prometheus-metrics\n")
end
end
end
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
new file mode 100644
index 00000000000..8b460646059
--- /dev/null
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Projects::Clusters::ApplicationsController do
+ include AccessMatchersForController
+
+ def current_application
+ Clusters::Cluster::APPLICATIONS[application]
+ end
+
+ describe 'POST create' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:application) { 'helm' }
+ let(:params) { { application: application, id: cluster.id } }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'schedule an application installation' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
+
+ expect { go }.to change { current_application.count }
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.application_helm).to be_scheduled
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it 'return 404' do
+ expect { go }.not_to change { current_application.count }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is unknown' do
+ let(:application) { 'unkwnown-app' }
+
+ it 'return 404' do
+ go
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is already installing' do
+ before do
+ create(:cluster_applications_helm, :installing, cluster: cluster)
+ end
+
+ it 'returns 400' do
+ go
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ end
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index bd924a1c7be..ca2bcb2b5ae 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -1,68 +1,108 @@
require 'spec_helper'
describe Projects::ClustersController do
- set(:user) { create(:user) }
- set(:project) { create(:project) }
- let(:role) { :master }
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
- before do
- project.team << [user, role]
+ describe 'GET index' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
- sign_in(user)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- describe 'GET index' do
- subject do
- get :index, namespace_id: project.namespace,
- project_id: project
- end
+ context 'when project has a cluster' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- context 'when cluster is already created' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
+ end
- it 'redirects to show a cluster' do
- subject
+ context 'when project does not have a cluster' do
+ let(:project) { create(:project) }
- expect(response).to redirect_to(project_cluster_path(project, cluster))
+ it { expect(go).to redirect_to(new_project_cluster_path(project)) }
end
end
- context 'when we do not have cluster' do
- it 'redirects to create a cluster' do
- subject
+ describe 'security' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to redirect_to(new_project_cluster_path(project))
- end
+ def go
+ get :index, namespace_id: project.namespace.to_param, project_id: project
end
end
describe 'GET login' do
- render_views
+ let(:project) { create(:project) }
- subject do
- get :login, namespace_id: project.namespace,
- project_id: project
- end
-
- context 'when we do have omniauth configured' do
- it 'shows login button' do
- subject
+ describe 'functionality' do
+ let(:user) { create(:user) }
- expect(response.body).to include('auth_buttons/signin_with_google')
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- end
- context 'when we do not have omniauth configured' do
- before do
- stub_omniauth_setting(providers: [])
+ context 'when omniauth has been configured' do
+ let(:key) { 'secere-key' }
+
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(providers_gcp_new_project_clusters_url(project))
+ end
end
- it 'shows notice message' do
- subject
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
- expect(response.body).to include('Ask your GitLab administrator if you want to use this service.')
+ expect(assigns(:authorize_url)).to be_nil
+ end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :login, namespace_id: project.namespace, project_id: project
+ end
end
shared_examples 'requires to login' do
@@ -73,236 +113,336 @@ describe Projects::ClustersController do
end
end
- describe 'GET new' do
- render_views
+ describe 'GET new_gcp' do
+ let(:project) { create(:project) }
- subject do
- get :new, namespace_id: project.namespace,
- project_id: project
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
end
- it 'shows a creation form' do
- subject
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
+ end
- expect(response.body).to include('Create cluster')
+ context 'when access token is not stored in session' do
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
end
end
- context 'when not logged' do
- it_behaves_like 'requires to login'
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new_gcp, namespace_id: project.namespace, project_id: project
end
end
describe 'POST create' do
- subject do
- post :create, params.merge(namespace_id: project.namespace,
- project_id: project)
+ let(:project) { create(:project) }
+
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '111'
+ }
+ }
+ }
end
- context 'when not logged' do
- let(:params) { {} }
-
- it_behaves_like 'requires to login'
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged in' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
end
- context 'when all required parameters are set' do
- let(:params) do
- {
- cluster: {
- gcp_cluster_name: 'new-cluster',
- gcp_project_id: '111'
- }
- }
- end
-
+ context 'when access token is valid' do
before do
- expect(ClusterProvisionWorker).to receive(:perform_async) { }
+ stub_google_api_validate_token
end
- it 'creates a new cluster' do
- expect { subject }.to change { Gcp::Cluster.count }
-
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ context 'when creates a cluster on gke' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
end
end
- context 'when not all required parameters are set' do
- render_views
-
- let(:params) do
- {
- cluster: {
- project_namespace: 'some namespace'
- }
- }
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
end
- it 'shows an error message' do
- expect { subject }.not_to change { Gcp::Cluster.count }
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
+ end
+ end
- expect(response).to render_template(:new)
+ context 'when access token is not stored in session' do
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
end
describe 'GET status' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:project) { cluster.project }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it "responds with matching schema" do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
get :status, namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json
end
-
- it "responds with matching schema" do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('cluster_status')
- end
end
describe 'GET show' do
- render_views
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
- get :show, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
-
- context 'when logged as master' do
- it "allows to update cluster" do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include("Save")
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- it "allows remove integration" do
- subject
+ it "renders view" do
+ go
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include("Remove integration")
+ expect(assigns(:cluster)).to eq(cluster)
end
end
- context 'when logged as developer' do
- let(:role) { :developer }
-
- it "does not allow to access page" do
- subject
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def go
+ get :show, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
end
describe 'PUT update' do
- render_views
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- let(:service) { project.build_kubernetes_service }
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) }
- let(:params) { {} }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
- put :update, params.merge(namespace_id: project.namespace,
- project_id: project,
- id: cluster)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- context 'when logged as master' do
- context 'when valid params are used' do
+ context 'when update enabled' do
let(:params) do
{
cluster: { enabled: false }
}
end
- it "redirects back to show page" do
- subject
+ it "updates and redirects back to show page" do
+ go
+ cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
end
- end
- context 'when invalid params are used' do
- let(:params) do
- {
- cluster: { project_namespace: 'my Namespace 321321321 #' }
- }
- end
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- it "rejects changes" do
- subject
+ it "rejects changes" do
+ go
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(cluster.enabled).to be_truthy
+ end
end
end
end
- context 'when logged as developer' do
- let(:role) { :developer }
+ describe 'security' do
+ let(:params) do
+ {
+ cluster: { enabled: false }
+ }
+ end
- it "does not allow to update cluster" do
- subject
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def go
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster)
end
end
describe 'delete update' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- subject do
- delete :destroy, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged as master' do
- it "redirects back to clusters list" do
- subject
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
expect(response).to redirect_to(project_clusters_path(project))
expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
end
- end
- context 'when logged as developer' do
- let(:role) { :developer }
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it "does not allow to destroy cluster" do
- subject
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(0)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
end
end
- end
- def make_logged_in
- session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234'
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s
- end
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- def in_hour
- Time.now + 1.hour
+ def go
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 4612fc6e441..fd90c0d8bad 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe Projects::CommitController do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
let(:commit) { project.commit("master") }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
describe 'GET show' do
@@ -134,8 +134,8 @@ describe Projects::CommitController do
end
end
- describe "GET branches" do
- it "contains branch and tags information" do
+ describe 'GET branches' do
+ it 'contains branch and tags information' do
commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
get(:branches,
@@ -143,8 +143,26 @@ describe Projects::CommitController do
project_id: project,
id: commit.id)
- expect(assigns(:branches)).to include("master", "feature_conflict")
- expect(assigns(:tags)).to include("v1.1.0")
+ expect(assigns(:branches)).to include('master', 'feature_conflict')
+ expect(assigns(:branches_limit_exceeded)).to be_falsey
+ expect(assigns(:tags)).to include('v1.1.0')
+ expect(assigns(:tags_limit_exceeded)).to be_falsey
+ end
+
+ it 'returns :limit_exceeded when number of branches/tags reach a threshhold' do
+ commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ allow_any_instance_of(Repository).to receive(:branch_count).and_return(1001)
+ allow_any_instance_of(Repository).to receive(:tag_count).and_return(1001)
+
+ get(:branches,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: commit.id)
+
+ expect(assigns(:branches)).to eq([])
+ expect(assigns(:branches_limit_exceeded)).to be_truthy
+ expect(assigns(:tags)).to eq([])
+ expect(assigns(:tags_limit_exceeded)).to be_truthy
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index aecdfb50759..4dbbaecdd6d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -248,6 +248,45 @@ describe Projects::IssuesController do
end
end
+ describe 'PUT #update' do
+ subject do
+ put :update,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.to_param,
+ issue: { title: 'New title' }, format: :json
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when user has access to update issue' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'updates the issue' do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(issue.reload.title).to eq('New title')
+ end
+ end
+
+ context 'when user does not have access to update issue' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds with 404' do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
describe 'Confidential Issues' do
let(:project) { create(:project_empty_repo, :public) }
let(:assignee) { create(:assignee) }
@@ -822,7 +861,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_issue).with(issue, owner).once
+ expect_any_instance_of(TodoService).to receive(:destroy_issuable).with(issue, owner).once
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index f9688949a19..7490f8fefce 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -371,8 +371,10 @@ describe Projects::JobsController do
end
describe 'POST erase' do
+ let(:role) { :master }
+
before do
- project.add_developer(user)
+ project.team << [user, role]
sign_in(user)
post_erase
@@ -404,6 +406,27 @@ describe Projects::JobsController do
end
end
+ context 'when user is developer' do
+ let(:role) { :developer }
+ let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline, user: triggered_by) }
+
+ context 'when triggered by same user' do
+ let(:triggered_by) { user }
+
+ it 'has successful status' do
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+
+ context 'when triggered by different user' do
+ let(:triggered_by) { create(:user) }
+
+ it 'does not have successful status' do
+ expect(response).not_to have_gitlab_http_status(:found)
+ end
+ end
+ end
+
def post_erase
post :erase, namespace_id: project.namespace,
project_id: project,
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index b7cccddefdd..bfdad85c082 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -83,15 +83,15 @@ describe Projects::MergeRequestsController do
end
describe 'as json' do
- context 'with basic param' do
+ context 'with basic serializer param' do
it 'renders basic MR entity as json' do
- go(basic: true, format: :json)
+ go(serializer: 'basic', format: :json)
expect(response).to match_response_schema('entities/merge_request_basic')
end
end
- context 'without basic param' do
+ context 'without basic serializer param' do
it 'renders the merge request in the json format' do
go(format: :json)
@@ -186,17 +186,23 @@ describe Projects::MergeRequestsController do
end
describe 'PUT update' do
+ def update_merge_request(mr_params, additional_params = {})
+ params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request.iid,
+ merge_request: mr_params
+ }.merge(additional_params)
+
+ put :update, params
+ end
+
context 'changing the assignee' do
it 'limits the attributes exposed on the assignee' do
assignee = create(:user)
project.add_developer(assignee)
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- merge_request: { assignee_id: assignee.id },
- format: :json
+ update_merge_request({ assignee_id: assignee.id }, format: :json)
body = JSON.parse(response.body)
expect(body['assignee'].keys)
@@ -204,6 +210,20 @@ describe Projects::MergeRequestsController do
end
end
+ context 'when user does not have access to update issue' do
+ before do
+ reporter = create(:user)
+ project.add_reporter(reporter)
+ sign_in(reporter)
+ end
+
+ it 'responds with 404' do
+ update_merge_request(title: 'New title')
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
context 'there is no source project' do
let(:project) { create(:project, :repository) }
let(:forked_project) { fork_project_with_submodules(project) }
@@ -214,13 +234,7 @@ describe Projects::MergeRequestsController do
end
it 'closes MR without errors' do
- post :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- state_event: 'close'
- }
+ update_merge_request(state_event: 'close')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
@@ -229,13 +243,7 @@ describe Projects::MergeRequestsController do
it 'allows editing of a closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- title: 'New title'
- }
+ update_merge_request(title: 'New title')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.title).to eq 'New title'
@@ -244,13 +252,7 @@ describe Projects::MergeRequestsController do
it 'does not allow to update target branch closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- target_branch: 'new_branch'
- }
+ update_merge_request(target_branch: 'new_branch')
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end
@@ -454,7 +456,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_merge_request).with(merge_request, owner).once
+ expect_any_instance_of(TodoService).to receive(:destroy_issuable).with(merge_request, owner).once
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 573530d0db0..209979e642d 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -86,4 +86,32 @@ describe Projects::MilestonesController do
expect(last_note).to eq('removed milestone')
end
end
+
+ describe '#promote' do
+ context 'promotion succeeds' do
+ before do
+ group = create(:group)
+ group.add_developer(user)
+ milestone.project.update(namespace: group)
+ end
+
+ it 'shows group milestone' do
+ post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+
+ group_milestone = assigns(:milestone)
+
+ expect(response).to redirect_to(group_milestone_path(project.group, group_milestone.iid))
+ expect(flash[:notice]).to eq('Milestone has been promoted to group milestone.')
+ end
+ end
+
+ context 'promotion fails' do
+ it 'shows project milestone' do
+ post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+
+ expect(response).to redirect_to(project_milestone_path(project, milestone))
+ expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 1184c55e540..37e9f863fc4 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -59,6 +59,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -74,6 +75,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).not_to be_nil
+ expect(note_json[:discussion_line_code]).not_to be_nil
end
end
@@ -92,6 +94,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -104,6 +107,20 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
+ end
+
+ context 'when user cannot read commit' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :download_code, project).and_return(false)
+ end
+
+ it 'renders 404' do
+ get :index, params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
@@ -120,6 +137,7 @@ describe Projects::NotesController do
expect(note_json[:html]).not_to be_nil
expect(note_json[:discussion_html]).to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -318,6 +336,29 @@ describe Projects::NotesController do
end
end
+ describe 'PUT update' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :json,
+ note: {
+ note: "New comment"
+ }
+ }
+ end
+
+ before do
+ sign_in(note.author)
+ project.team << [note.author, :developer]
+ end
+
+ it "updates the note" do
+ expect { put :update, request_params }.to change { note.reload.note }
+ end
+ end
+
describe 'DELETE destroy' do
let(:request_params) do
{
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 3a3e7467ef2..748ae040928 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -23,12 +23,15 @@ describe Projects::RefsController do
xhr :get,
:logs_tree,
namespace_id: project.namespace.to_param,
- project_id: project, id: 'master',
- path: 'foo/bar/baz.html', format: format
+ project_id: project,
+ id: 'master',
+ path: 'foo/bar/baz.html',
+ format: format
end
it 'never throws MissingTemplate' do
expect { default_get }.not_to raise_error
+ expect { xhr_get(:json) }.not_to raise_error
expect { xhr_get }.not_to raise_error
end
@@ -42,5 +45,12 @@ describe Projects::RefsController do
xhr_get(:js)
expect(response).to be_success
end
+
+ it 'renders JSON' do
+ xhr_get(:json)
+
+ expect(response).to be_success
+ expect(json_response).to be_kind_of(Array)
+ end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index b1d7157e447..e7ab714c550 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -503,13 +503,14 @@ describe ProjectsController do
describe "GET refs" do
let(:public_project) { create(:project, :public, :repository) }
- it "gets a list of branches and tags" do
- get :refs, namespace_id: public_project.namespace, id: public_project
+ it 'gets a list of branches and tags' do
+ get :refs, namespace_id: public_project.namespace, id: public_project, sort: 'updated_desc'
parsed_body = JSON.parse(response.body)
- expect(parsed_body["Branches"]).to include("master")
- expect(parsed_body["Tags"]).to include("v1.0.0")
- expect(parsed_body["Commits"]).to be_nil
+ expect(parsed_body['Branches']).to include('master')
+ expect(parsed_body['Tags'].first).to eq('v1.1.0')
+ expect(parsed_body['Tags'].last).to eq('v1.0.0')
+ expect(parsed_body['Commits']).to be_nil
end
it "gets a list of branches, tags and commits" do
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
new file mode 100644
index 00000000000..fab37195113
--- /dev/null
+++ b/spec/factories/clusters/applications/helm.rb
@@ -0,0 +1,35 @@
+FactoryGirl.define do
+ factory :cluster_applications_helm, class: Clusters::Applications::Helm do
+ cluster factory: %i(cluster provided_by_gcp)
+
+ trait :not_installable do
+ status(-2)
+ end
+
+ trait :installable do
+ status 0
+ end
+
+ trait :scheduled do
+ status 1
+ end
+
+ trait :installing do
+ status 2
+ end
+
+ trait :installed do
+ status 3
+ end
+
+ trait :errored do
+ status(-1)
+ status_reason 'something went wrong'
+ end
+
+ trait :timeouted do
+ installing
+ updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
+ end
+ end
+end
diff --git a/spec/factories/clusters/applications/ingress.rb b/spec/factories/clusters/applications/ingress.rb
new file mode 100644
index 00000000000..b103a980655
--- /dev/null
+++ b/spec/factories/clusters/applications/ingress.rb
@@ -0,0 +1,35 @@
+FactoryGirl.define do
+ factory :cluster_applications_ingress, class: Clusters::Applications::Ingress do
+ cluster factory: %i(cluster provided_by_gcp)
+
+ trait :not_installable do
+ status(-2)
+ end
+
+ trait :installable do
+ status 0
+ end
+
+ trait :scheduled do
+ status 1
+ end
+
+ trait :installing do
+ status 2
+ end
+
+ trait :installed do
+ status 3
+ end
+
+ trait :errored do
+ status(-1)
+ status_reason 'something went wrong'
+ end
+
+ trait :timeouted do
+ installing
+ updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
+ end
+ end
+end
diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb
new file mode 100644
index 00000000000..c4261178f2d
--- /dev/null
+++ b/spec/factories/clusters/cluster.rb
@@ -0,0 +1,39 @@
+FactoryGirl.define do
+ factory :cluster, class: Clusters::Cluster do
+ user
+ name 'test-cluster'
+
+ trait :project do
+ after(:create) do |cluster, evaluator|
+ cluster.projects << create(:project)
+ end
+ end
+
+ trait :provided_by_user do
+ provider_type :user
+ platform_type :kubernetes
+
+ platform_kubernetes do
+ create(:cluster_platform_kubernetes, :configured)
+ end
+ end
+
+ trait :provided_by_gcp do
+ provider_type :gcp
+ platform_type :kubernetes
+
+ before(:create) do |cluster, evaluator|
+ cluster.platform_kubernetes = build(:cluster_platform_kubernetes, :configured)
+ cluster.provider_gcp = build(:cluster_provider_gcp, :created)
+ end
+ end
+
+ trait :providing_by_gcp do
+ provider_type :gcp
+
+ provider_gcp do
+ create(:cluster_provider_gcp, :creating)
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..8b3e6ff35fa
--- /dev/null
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :cluster_platform_kubernetes, class: Clusters::Platforms::Kubernetes do
+ cluster
+ namespace nil
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+
+ trait :configured do
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+ username 'xxxxxx'
+ password 'xxxxxx'
+
+ after(:create) do |platform_kubernetes, evaluator|
+ pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
+ platform_kubernetes.ca_cert = File.read(pem_file)
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..a815410512a
--- /dev/null
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -0,0 +1,32 @@
+FactoryGirl.define do
+ factory :cluster_provider_gcp, class: Clusters::Providers::Gcp do
+ cluster
+ gcp_project_id 'test-gcp-project'
+
+ trait :scheduled do
+ access_token 'access_token_123'
+ end
+
+ trait :creating do
+ access_token 'access_token_123'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_creating('operation-123')
+ end
+ end
+
+ trait :created do
+ endpoint '111.111.111.111'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_created
+ end
+ end
+
+ trait :errored do
+ after(:build) do |gcp, evaluator|
+ gcp.make_errored('Something wrong')
+ end
+ end
+ end
+end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 169590deb8e..abbe37df90e 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -1,6 +1,7 @@
FactoryGirl.define do
factory :commit_status, class: CommitStatus do
name 'default'
+ stage 'test'
status 'success'
description 'commit status'
pipeline factory: :ci_pipeline_with_one_job
diff --git a/spec/factories/fork_network_members.rb b/spec/factories/fork_network_members.rb
new file mode 100644
index 00000000000..509c4e1fa1c
--- /dev/null
+++ b/spec/factories/fork_network_members.rb
@@ -0,0 +1,8 @@
+FactoryGirl.define do
+ factory :fork_network_member do
+ association :project
+ association :fork_network
+
+ forked_from_project { fork_network.root_project }
+ end
+end
diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb
deleted file mode 100644
index 630e40da888..00000000000
--- a/spec/factories/gcp/cluster.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-FactoryGirl.define do
- factory :gcp_cluster, class: Gcp::Cluster do
- project
- user
- enabled true
- gcp_project_id 'gcp-project-12345'
- gcp_cluster_name 'test-cluster'
- gcp_cluster_zone 'us-central1-a'
- gcp_cluster_size 1
- gcp_machine_type 'n1-standard-4'
-
- trait :with_kubernetes_service do
- after(:create) do |cluster, evaluator|
- create(:kubernetes_service, project: cluster.project).tap do |service|
- cluster.update(service: service)
- end
- end
- end
-
- trait :custom_project_namespace do
- project_namespace 'sample-app'
- end
-
- trait :created_on_gke do
- status_event :make_created
- endpoint '111.111.111.111'
- ca_cert 'xxxxxx'
- kubernetes_token 'xxxxxx'
- username 'xxxxxx'
- password 'xxxxxx'
- end
-
- trait :errored do
- status_event :make_errored
- status_reason 'general error'
- end
- end
-end
diff --git a/spec/factories/group_custom_attributes.rb b/spec/factories/group_custom_attributes.rb
new file mode 100644
index 00000000000..7ff5f376e8b
--- /dev/null
+++ b/spec/factories/group_custom_attributes.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :group_custom_attribute do
+ group
+ sequence(:key) { |n| "key#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 7c4a22c94c2..cc6cef63b47 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -83,10 +83,10 @@ FactoryGirl.define do
target_project = merge_request.target_project
source_project = merge_request.source_project
- # Fake `write_ref` if we don't have repository
+ # Fake `fetch_ref!` if we don't have repository
# We have too many existing tests replying on this behaviour
unless [target_project, source_project].all?(&:repository_exists?)
- allow(merge_request).to receive(:write_ref)
+ allow(merge_request).to receive(:fetch_ref!)
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index f0d05504b7e..ab4ae123429 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -130,6 +130,7 @@ FactoryGirl.define do
before(:create) do |note, evaluator|
discussion = evaluator.in_reply_to
next unless discussion
+
discussion = discussion.to_discussion if discussion.is_a?(Note)
next unless discussion
diff --git a/spec/factories/project_custom_attributes.rb b/spec/factories/project_custom_attributes.rb
new file mode 100644
index 00000000000..5eedeb86304
--- /dev/null
+++ b/spec/factories/project_custom_attributes.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :project_custom_attribute do
+ project
+ sequence(:key) { |n| "key#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index 6a97378391b..2abdd3c9ef2 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -7,7 +7,7 @@ feature 'Admin disables 2FA for a user' do
edit_user(user)
page.within('.two-factor-status') do
- click_link 'Disable'
+ accept_confirm { click_link 'Disable' }
end
page.within('.two-factor-status') do
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 771fb5253da..a5f22848031 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -152,7 +152,7 @@ feature 'Admin Groups' do
expect(page).to have_content('Developer')
end
- find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+ accept_confirm { find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click }
visit group_group_members_path(group)
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 2e65fcc5231..eec44549a03 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -62,14 +62,14 @@ describe 'Admin::Hooks', :js do
it 'from hooks list page' do
visit admin_hooks_path
- expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
end
it 'from hook edit page' do
visit admin_hooks_path
click_link 'Edit'
- expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
end
end
end
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index a5834056a1d..de406d7d966 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'admin issues labels' do
it 'deletes all labels', :js do
page.within '.labels' do
page.all('.btn-remove').each do |remove|
- remove.click
+ accept_confirm { remove.click }
wait_for_requests
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 85561511101..1218ea52227 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -73,7 +73,7 @@ feature 'Admin updates settings' do
context 'sign-in restrictions', :js do
it 'de-activates oauth sign-in source' do
- find('.btn', text: 'GitLab.com').click
+ find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
end
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 388d30828a7..e16eae219a4 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -24,7 +24,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do
fill_in "Name", with: name
# Set date to 1st of next month
- find_field("Expires at").trigger('focus')
+ find_field("Expires at").click
find(".pika-next").click
click_on "1"
@@ -60,7 +60,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do
it "allows revocation of an active impersonation token" do
visit admin_user_impersonation_tokens_path(user_id: user.username)
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
expect(no_personal_access_tokens_message).to have_text("This user has no active Impersonation Tokens.")
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index f9f4bd6f5b9..a69b428d117 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -167,19 +167,36 @@ describe "Admin::Users" do
it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret')
- expect(icon).not_to eql nil
+ expect(icon).not_to be nil
end
it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username)
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
find(:css, 'li.impersonation a').click
- expect(current_path).to eql "/admin/users/#{another_user.username}"
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
+ end
+ end
+
+ context 'when impersonating a user with an expired password' do
+ before do
+ another_user.update(password_expires_at: Time.now - 5.minutes)
+ click_link 'Impersonate'
+ end
+
+ it 'does not redirect to password change page' do
+ expect(current_path).to eq('/')
+ end
+
+ it 'is redirected back to the impersonated users page in the admin after stopping' do
+ find(:css, 'li.impersonation a').click
+
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
end
end
end
@@ -290,7 +307,7 @@ describe "Admin::Users" do
it 'allows group membership to be revoked', :js do
page.within(first('.group_member')) do
- find('.btn-remove').click
+ accept_confirm { find('.btn-remove').click }
end
wait_for_requests
@@ -319,7 +336,7 @@ describe "Admin::Users" do
expect(page).to have_content("Secondary email: #{secondary_email.email}")
- find("#remove_email_#{secondary_email.id}").click
+ accept_confirm { find("#remove_email_#{secondary_email.id}").click }
expect(page).not_to have_content(secondary_email.email)
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 42f5b5eb8dc..f1ac73ff819 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -37,7 +37,7 @@ feature 'Admin uses repository checks' do
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
- click_link 'Clear all repository checks'
+ accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
expect(page).to have_content('Started asynchronous removal of all repository check states.')
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 5aae2dbaf91..89c9d377003 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -13,8 +13,10 @@ describe "Dashboard Issues Feed" do
end
describe "atom feed" do
- it "renders atom feed via private token" do
- visit issues_dashboard_path(:atom, private_token: user.private_token)
+ it "renders atom feed via personal access token" do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit issues_dashboard_path(:atom, private_token: personal_access_token.token)
expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index 321c8a2a670..2c0c331b6db 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -4,9 +4,11 @@ describe "Dashboard Feed" do
describe "GET /" do
let!(:user) { create(:user, name: "Jonh") }
- context "projects atom feed via private token" do
+ context "projects atom feed via personal access token" do
it "renders projects atom feed" do
- visit dashboard_projects_path(:atom, private_token: user.private_token)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit dashboard_projects_path(:atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title')
end
end
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 3eeb4d35131..4102ac0588a 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -28,10 +28,12 @@ describe 'Issues Feed' do
end
end
- context 'when authenticated via private token' do
+ context 'when authenticated via personal access token' do
it 'renders atom feed' do
+ personal_access_token = create(:personal_access_token, user: user)
+
visit project_issues_path(project, :atom,
- private_token: user.private_token)
+ private_token: personal_access_token.token)
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 9ce687afb31..2b934d81674 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -4,9 +4,11 @@ describe "User Feed" do
describe "GET /" do
let!(:user) { create(:user) }
- context 'user atom feed via private token' do
+ context 'user atom feed via personal access token' do
it "renders user atom feed" do
- visit user_path(user, :atom, private_token: user.private_token)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit user_path(user, :atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title')
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index c480b5b7e34..e4cfcea45a5 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -101,7 +101,7 @@ describe 'Issue Boards add issue modal', :js do
click_button 'Cancel'
end
- first('.board-delete').click
+ accept_confirm { first('.board-delete').click }
click_button('Add issues')
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 60ed17c0c81..e8d779f5772 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Issue Boards', :js do
include DragTo
+ include MobileHelpers
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
@@ -13,7 +14,7 @@ describe 'Issue Boards', :js do
project.team << [user, :master]
project.team << [user2, :master]
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
sign_in(user)
end
@@ -135,7 +136,7 @@ describe 'Issue Boards', :js do
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
- find('.board-delete').click
+ accept_confirm { find('.board-delete').click }
end
wait_for_requests
@@ -150,7 +151,7 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close').click
page.within(find('.board:nth-child(2)')) do
- find('.board-delete').click
+ accept_confirm { find('.board-delete').click }
end
wait_for_requests
@@ -379,7 +380,7 @@ describe 'Issue Boards', :js do
end
it 'filters by milestone' do
- set_filter("milestone", "\"#{milestone.title}\"")
+ set_filter("milestone", "\"#{milestone.title}")
click_filter_link(milestone.title)
submit_filter
@@ -400,7 +401,7 @@ describe 'Issue Boards', :js do
end
it 'filters by label with space after reload' do
- set_filter("label", "\"#{accepting.title}\"")
+ set_filter("label", "\"#{accepting.title}")
click_filter_link(accepting.title)
submit_filter
@@ -521,7 +522,7 @@ describe 'Issue Boards', :js do
end
it 'allows user to use keyboard shortcuts' do
- find('.boards-list').native.send_keys('i')
+ find('body').native.send_keys('i')
expect(page).to have_content('New Issue')
end
end
@@ -538,7 +539,7 @@ describe 'Issue Boards', :js do
end
it 'does not show create new list' do
- expect(page).not_to have_selector('.js-new-board-list')
+ expect(page).not_to have_button('.js-new-board-list')
end
it 'does not allow dragging' do
@@ -563,6 +564,9 @@ describe 'Issue Boards', :js do
end
def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+ # ensure there is enough horizontal space for four boards
+ resize_window(2000, 800)
+
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 4965f803883..205900615c4 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -51,7 +51,7 @@ describe 'Issue Boards', :js do
expect(page).to have_selector('.issue-boards-sidebar')
- find('.gutter-toggle').trigger('click')
+ find('.gutter-toggle').click
expect(page).not_to have_selector('.issue-boards-sidebar')
end
@@ -171,7 +171,7 @@ describe 'Issue Boards', :js do
end
page.within(find('.board:nth-child(2)')) do
- find('.card:nth-child(2)').trigger('click')
+ find('.card:nth-child(2)').click
end
page.within('.assignee') do
@@ -331,11 +331,29 @@ describe 'Issue Boards', :js do
context 'subscription' do
it 'changes issue subscription' do
click_card(card)
+ wait_for_requests
- page.within('.subscription') do
+ page.within('.subscriptions') do
click_button 'Subscribe'
wait_for_requests
- expect(page).to have_content("Unsubscribe")
+
+ expect(page).to have_content('Unsubscribe')
+ end
+ end
+
+ it 'has "Unsubscribe" button when already subscribed' do
+ create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true)
+ visit project_board_path(project, board)
+ wait_for_requests
+
+ click_card(card)
+ wait_for_requests
+
+ page.within('.subscriptions') do
+ click_button 'Unsubscribe'
+ wait_for_requests
+
+ expect(page).to have_content('Subscribe')
end
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 4fc6956d111..a9530becb65 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -63,8 +63,8 @@ feature 'Contributions Calendar', :js do
Event.create(note_comment_params)
end
- def selected_day_activities
- find('.user-calendar-activities').text
+ def selected_day_activities(visible: true)
+ find('.user-calendar-activities', visible: visible).text
end
before do
@@ -112,7 +112,7 @@ feature 'Contributions Calendar', :js do
end
it 'hides calendar day activities' do
- expect(selected_day_activities).to be_empty
+ expect(selected_day_activities(visible: false)).to be_empty
end
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 479fb713297..98586ddbd81 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Commits' do
- include CiStatusHelper
-
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -33,7 +31,7 @@ describe 'Commits' do
describe 'Commit builds' do
before do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it { expect(page).to have_content pipeline.sha[0..7] }
@@ -79,7 +77,7 @@ describe 'Commits' do
describe 'Commit builds', :js do
before do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it 'shows pipeline`s data' do
@@ -95,7 +93,7 @@ describe 'Commits' do
end
it do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
click_on 'Download artifacts'
expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
end
@@ -103,7 +101,7 @@ describe 'Commits' do
describe 'Cancel all builds' do
it 'cancels commit', :js do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
click_on 'Cancel running'
expect(page).to have_content 'canceled'
end
@@ -111,7 +109,7 @@ describe 'Commits' do
describe 'Cancel build' do
it 'cancels build', :js do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
find('.js-btn-cancel-pipeline').click
expect(page).to have_content 'canceled'
end
@@ -120,13 +118,13 @@ describe 'Commits' do
describe '.gitlab-ci.yml not found warning' do
context 'ci builds enabled' do
it "does not show warning" do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
end
it 'shows warning' do
stub_ci_pipeline_yaml_file(nil)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
expect(page).to have_content '.gitlab-ci.yml not found in this commit'
end
end
@@ -135,7 +133,7 @@ describe 'Commits' do
before do
stub_ci_builds_disabled
stub_ci_pipeline_yaml_file(nil)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it 'does not show warning' do
@@ -149,7 +147,7 @@ describe 'Commits' do
before do
project.team << [user, :reporter]
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it 'Renders header', :js do
@@ -171,7 +169,7 @@ describe 'Commits' do
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it do
@@ -202,5 +200,12 @@ describe 'Commits' do
expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}")
end
end
+
+ it 'shows the ref switcher with the multi-file editor enabled', :js do
+ set_cookie('new_repo', 'true')
+ visit project_commits_path(project, branch_name)
+
+ expect(find('.js-project-refs-dropdown')).to have_content branch_name
+ end
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index d5e9de20e59..bef2aa9e0e5 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -47,7 +47,7 @@ describe "Container Registry", :js do
scenario 'user removes a specific tag from container repository' do
visit_container_registry
- find('.js-toggle-repo').trigger('click')
+ find('.js-toggle-repo').click
wait_for_requests
expect_any_instance_of(ContainerRegistry::Tag)
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index c6ba1211b9e..1fcb8d5bc67 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -664,7 +664,7 @@ describe 'Copy as GFM', :js do
def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<-JS.strip_heredoc
(function(html) {
- var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
+ var transformer = window.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div');
$(html).each(function() { node.appendChild(this) });
@@ -678,7 +678,7 @@ describe 'Copy as GFM', :js do
node = transformer(node, target);
if (!node) return null;
- return window.gl.CopyAsGFM.nodeToGFM(node);
+ return window.CopyAsGFM.nodeToGFM(node);
})("#{escape_javascript(html)}")
JS
page.evaluate_script(js)
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index 1213f8c32eb..1c7932e7964 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Dashboard Group' do
it 'creates new group', :js do
visit dashboard_groups_path
- find('.btn-new').trigger('click')
+ find('.btn-new').click
new_path = 'Samurai'
new_description = 'Tokugawa Shogunate'
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index c6873d1923c..d92c002b4e7 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -138,7 +138,7 @@ feature 'Dashboard Groups page', :js do
expect(page).not_to have_selector("#group-#{group.id}")
# Go to next page
- find(".gl-pagination .page:not(.active) a").trigger('click')
+ find(".gl-pagination .page:not(.active) a").click
wait_for_requests
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index a8919976c31..5b4c00b3c7e 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Dashboard Issues' do
end
it 'shows issues when current user is author', :js do
- find('#assignee_id', visible: false).set('')
+ execute_script("document.querySelector('#assignee_id').value=''")
find('.js-author-search', match: :first).click
expect(find('li[data-user-id="null"] a.is-active')).to be_visible
@@ -71,7 +71,7 @@ RSpec.describe 'Dashboard Issues' do
describe 'new issue dropdown' do
it 'shows projects only with issues feature enabled', :js do
- find('.new-project-item-select-button').trigger('click')
+ find('.new-project-item-select-button').click
page.within('.select2-results') do
expect(page).to have_content(project.name_with_namespace)
@@ -80,7 +80,7 @@ RSpec.describe 'Dashboard Issues' do
end
it 'shows the new issue page', :js do
- find('.new-project-item-select-button').trigger('click')
+ find('.new-project-item-select-button').click
wait_for_requests
@@ -93,7 +93,7 @@ RSpec.describe 'Dashboard Issues' do
find('#select2-drop-mask', visible: false)
execute_script("$('#select2-drop-mask').remove();")
- find('.new-project-item-link').trigger('click')
+ find('.new-project-item-link').click
expect(page).to have_current_path("#{project_path}/issues/new")
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index f01ba442e58..991d360ccaf 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Dashboard Merge Requests' do
end
it 'shows projects only with merge requests feature enabled', :js do
- find('.new-project-item-select-button').trigger('click')
+ find('.new-project-item-select-button').click
page.within('.select2-results') do
expect(page).to have_content(project.name_with_namespace)
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 01aca443f4a..6f916078b1a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -52,7 +52,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Todos 0'
expect(page).to have_content 'Done 1'
end
@@ -81,7 +81,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Todos 1'
expect(page).to have_content 'Done 0'
end
end
@@ -200,7 +200,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Todos 1'
expect(page).to have_content 'Done 0'
end
end
@@ -252,11 +252,11 @@ feature 'Dashboard Todos' do
describe 'mark all as done', :js do
before do
visit dashboard_todos_path
- find('.js-todos-mark-all').trigger('click')
+ find('.js-todos-mark-all').click
end
it 'shows "All done" message!' do
- expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Todos 0'
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
@@ -283,7 +283,7 @@ feature 'Dashboard Todos' do
it 'updates todo count' do
mark_all_and_undo
- expect(page).to have_content 'To do 2'
+ expect(page).to have_content 'Todos 2'
expect(page).to have_content 'Done 0'
end
@@ -309,9 +309,9 @@ feature 'Dashboard Todos' do
end
def mark_all_and_undo
- find('.js-todos-mark-all').trigger('click')
+ find('.js-todos-mark-all').click
wait_for_requests
- find('.js-todos-undo-all').trigger('click')
+ find('.js-todos-undo-all').click
wait_for_requests
end
end
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
index 0375d0bf8ff..69d35cdbc72 100644
--- a/spec/features/discussion_comments/commit_spec.rb
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Merge Request', :js do
+describe 'Discussion Comments Commit', :js do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
index 1e6389d9a13..4a236c4639b 100644
--- a/spec/features/discussion_comments/snippets_spec.rb
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Issue', :js do
+describe 'Discussion Comments Snippet', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index c5ec495a418..8d5233d0c0f 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -65,9 +65,9 @@ feature 'Top Plus Menu', :js do
visit project_path(project)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
- find('.header-new-project-snippet a').trigger('click')
+ find('.header-new-project-snippet a').click
end
expect(page).to have_content('New Snippet')
@@ -87,9 +87,9 @@ feature 'Top Plus Menu', :js do
visit group_path(group)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
- find('.header-new-group-project a').trigger('click')
+ find('.header-new-group-project a').click
end
expect(page).to have_content('Project path')
@@ -155,7 +155,7 @@ feature 'Top Plus Menu', :js do
def click_topmenuitem(item_name)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
click_link item_name
end
diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb
index 9039b283393..da1e17225db 100644
--- a/spec/features/groups/members/manage_members.rb
+++ b/spec/features/groups/members/manage_members.rb
@@ -44,7 +44,11 @@ feature 'Groups > Members > Manage members' do
visit group_group_members_path(group)
- find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
+ accept_confirm do
+ find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
+ end
+
+ wait_for_requests
expect(page).not_to have_content(user2.name)
expect(group.users).not_to include(user2)
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index cc8906fa969..c1f3d94bc20 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -65,7 +65,7 @@ feature 'Group' do
end
it 'updates the team URL on graph path update', :js do
- out_span = find('span[data-bind-out="create_chat_team"]')
+ out_span = find('span[data-bind-out="create_chat_team"]', visible: false)
expect(out_span.text).to be_empty
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 3223eb20b55..fa4d3a55c62 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -405,7 +405,7 @@ feature 'Issues > Labels bulk assignment' do
end
def update_issues
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb
index 546dc7e8a49..edea95c6699 100644
--- a/spec/features/issues/create_branch_merge_request_spec.rb
+++ b/spec/features/issues/create_branch_merge_request_spec.rb
@@ -64,6 +64,19 @@ feature 'Create Branch/Merge Request Dropdown on issue page', :feature, :js do
end
end
+ context 'when merge requests are disabled' do
+ before do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows only create branch button' do
+ expect(page).not_to have_button('Create a merge request')
+ expect(page).to have_button('Create a branch')
+ end
+ end
+
context 'when issue is confidential' do
it 'disables the create branch button' do
issue = create(:issue, :confidential, project: project)
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 1c4649d0ba9..2e4a25ee15d 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -43,15 +43,16 @@ describe 'Dropdown assignee', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('assignee:')
+ slow_requests do
+ filtered_search.set('assignee:')
- expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
filtered_search.set('assignee:')
- expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading')
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 5e20fb48768..2fb5e7cdba4 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -51,9 +51,11 @@ describe 'Dropdown author', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('author:')
+ slow_requests do
+ filtered_search.set('author:')
- expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 3012c77f2b9..8db435634fd 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -70,9 +70,11 @@ describe 'Dropdown emoji', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('my-reaction:')
+ slow_requests do
+ filtered_search.set('my-reaction:')
- expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index cbc4f8d4c50..18cdb199c70 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -66,9 +66,11 @@ describe 'Dropdown label', :js do
end
it 'shows loading indicator when opened and hides it when loaded' do
- filtered_search.set('label:')
+ slow_requests do
+ filtered_search.set('label:')
- expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading')
+ expect(page).to have_css("#{js_dropdown_label} .filter-dropdown-loading", visible: true)
+ end
expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index f6c2e952bea..031eb06723a 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -50,15 +50,16 @@ describe 'Dropdown milestone', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('milestone:')
+ slow_requests do
+ filtered_search.set('milestone:')
- expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
filtered_search.set('milestone:')
- expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading')
expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 2974016c6a7..b3c50964810 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -139,7 +139,7 @@ describe 'Filter issues', :js do
input_filtered_search('label:none')
expect_tokens([label_token('none', false)])
- expect_issues_list_count(8)
+ expect_issues_list_count(4)
expect_filtered_search_input_empty
end
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index eef7988e2bd..f355cec3ba9 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -27,9 +27,8 @@ describe 'Recent searches', :js do
input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true)
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('bar')
expect(items[1].text).to eq('foo')
end
@@ -38,9 +37,8 @@ describe 'Recent searches', :js do
visit project_issues_path(project_1, label_name: 'foo', search: 'bar')
visit project_issues_path(project_1, label_name: 'qux', search: 'garply')
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('label:~qux garply')
expect(items[1].text).to eq('label:~foo bar')
end
@@ -50,9 +48,8 @@ describe 'Recent searches', :js do
visit project_issues_path(project_1, search: 'foo')
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 3)
- expect(items.count).to eq(3)
expect(items[0].text).to eq('foo')
expect(items[1].text).to eq('saved1')
expect(items[2].text).to eq('saved2')
@@ -69,9 +66,8 @@ describe 'Recent searches', :js do
input_filtered_search('more', submit: true)
input_filtered_search('things', submit: true)
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('things')
expect(items[1].text).to eq('more')
end
@@ -80,7 +76,8 @@ describe 'Recent searches', :js do
set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
visit project_issues_path(project_1)
- all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
+ find('.filtered-search-history-dropdown-toggle-button').click
+ all('.filtered-search-history-dropdown-item', count: 2)[0].click
wait_for_filtered_search('foo')
expect(find('.filtered-search').value.strip).to eq('foo')
@@ -90,12 +87,11 @@ describe 'Recent searches', :js do
set_recent_searches(project_1_local_storage_key, '["foo"]')
visit project_issues_path(project_1)
- items_before = all('.filtered-search-history-dropdown-item', visible: false)
+ find('.filtered-search-history-dropdown-toggle-button').click
+ all('.filtered-search-history-dropdown-item', count: 1)
- expect(items_before.count).to eq(1)
-
- find('.filtered-search-history-clear-button', visible: false).trigger('click')
- items_after = all('.filtered-search-history-dropdown-item', visible: false)
+ find('.filtered-search-history-clear-button').click
+ items_after = all('.filtered-search-history-dropdown-item', count: 0)
expect(items_after.count).to eq(0)
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 920f5546eef..0ae70c855db 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Visual tokens', :js do
include FilteredSearchHelpers
- include WaitForRequests
let!(:project) { create(:project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
@@ -28,7 +27,7 @@ describe 'Visual tokens', :js do
sign_in(user)
create(:issue, project: project)
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
visit project_issues_path(project)
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 15041ff04ea..95d637265e0 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -17,9 +17,9 @@ feature 'GFM autocomplete', :js do
it 'updates issue descripton with GFM reference' do
find('.issuable-edit').click
- find('#issue-description').native.send_keys("@#{user.name[0...3]}")
+ simulate_input('#issue-description', "@#{user.name[0...3]}")
- find('.atwho-view .cur').trigger('click')
+ find('.atwho-view .cur').click
click_button 'Save changes'
@@ -28,7 +28,6 @@ feature 'GFM autocomplete', :js do
it 'opens autocomplete menu when field starts with text' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys('@')
end
@@ -46,7 +45,6 @@ feature 'GFM autocomplete', :js do
it 'doesnt select the first item for non-assignee dropdowns' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys(':')
end
@@ -86,7 +84,6 @@ feature 'GFM autocomplete', :js do
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys('@')
end
@@ -100,7 +97,7 @@ feature 'GFM autocomplete', :js do
it 'includes items for assignee dropdowns with non-ASCII characters in name' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('')
- find('#note-body').native.send_keys("@#{user.name[0...8]}")
+ simulate_input('#note-body', "@#{user.name[0...8]}")
end
expect(page).to have_selector('.atwho-container')
@@ -112,7 +109,6 @@ feature 'GFM autocomplete', :js do
it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys(':1')
end
@@ -127,9 +123,8 @@ feature 'GFM autocomplete', :js do
it 'wraps the result in double quotes' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
- note.native.send_keys("~#{label.title[0]}")
- note.click
+ find('#note-body').native.send_keys('')
+ simulate_input('#note-body', "~#{label.title[0]}")
end
label_item = find('.atwho-view li', text: label.title)
@@ -152,16 +147,13 @@ feature 'GFM autocomplete', :js do
it "does not show dropdown when preceded with a special character" do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys("@")
- note.click
end
expect(page).to have_selector('.atwho-container')
page.within '.timeline-content-form' do
note.native.send_keys("@")
- note.click
end
expect(page).to have_selector('.atwho-container', visible: false)
@@ -170,9 +162,7 @@ feature 'GFM autocomplete', :js do
it "does not throw an error if no labels exist" do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys('~')
- note.click
end
expect(page).to have_selector('.atwho-container', visible: false)
@@ -181,9 +171,7 @@ feature 'GFM autocomplete', :js do
it 'doesn\'t wrap for assignee values' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
- note.click
end
user_item = find('.atwho-view li', text: user.username)
@@ -194,9 +182,7 @@ feature 'GFM autocomplete', :js do
it 'doesn\'t wrap for emoji values' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
- note.native.send_keys(":cartwheel")
- note.click
+ note.native.send_keys(":cartwheel_")
end
emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
@@ -223,28 +209,27 @@ feature 'GFM autocomplete', :js do
it 'triggers autocomplete after selecting a quick action' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys('/as')
- note.click
end
- find('.atwho-view li', text: '/assign').native.send_keys(:tab)
+ find('.atwho-view li', text: '/assign')
+ note.native.send_keys(:tab)
user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username)
end
+ end
- def expect_to_wrap(should_wrap, item, note, value)
- expect(item).to have_content(value)
- expect(item).not_to have_content("\"#{value}\"")
+ def expect_to_wrap(should_wrap, item, note, value)
+ expect(item).to have_content(value)
+ expect(item).not_to have_content("\"#{value}\"")
- item.click
+ item.click
- if should_wrap
- expect(note.value).to include("\"#{value}\"")
- else
- expect(note.value).not_to include("\"#{value}\"")
- end
+ if should_wrap
+ expect(note.value).to include("\"#{value}\"")
+ else
+ expect(note.value).not_to include("\"#{value}\"")
end
end
end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 6fbee0ebcb5..4224a8fe5d4 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -1,9 +1,9 @@
require 'rails_helper'
feature 'Issue Detail', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, project: project, author: user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, author: user) }
context 'when user displays the issue' do
before do
@@ -27,6 +27,7 @@ feature 'Issue Detail', :js do
click_link 'Edit'
fill_in 'issuable-title', with: 'issue title'
click_button 'Save'
+ wait_for_requests
Users::DestroyService.new(user).execute(user)
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index bc9c3d825c1..a9de52bd8d5 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -130,8 +130,8 @@ feature 'Issue Sidebar' do
it 'adds new label' do
page.within('.block.labels') do
fill_in 'new_label_name', with: 'wontfix'
- page.find('.suggest-colors a', match: :first).trigger('click')
- page.find('button', text: 'Create').trigger('click')
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
page.within('.dropdown-page-one') do
expect(page).to have_content 'wontfix'
@@ -142,8 +142,8 @@ feature 'Issue Sidebar' do
it 'shows error message if label title is taken' do
page.within('.block.labels') do
fill_in 'new_label_name', with: label.title
- page.find('.suggest-colors a', match: :first).trigger('click')
- page.find('button', text: 'Create').trigger('click')
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
page.within('.dropdown-page-two') do
expect(page).to have_content 'Title has already been taken'
@@ -170,7 +170,7 @@ feature 'Issue Sidebar' do
end
def open_issue_sidebar
- find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').trigger('click')
+ find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').click
find('aside.right-sidebar.right-sidebar-expanded')
end
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 6d7b1b1cd8f..17035b5501c 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -38,7 +38,7 @@ feature 'issue move to another project' do
end
scenario 'moving issue to another project', :js do
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
all('.js-move-issue-dropdown-item')[0].click
find('.js-move-issue-confirmation-button').click
@@ -52,7 +52,7 @@ feature 'issue move to another project' do
scenario 'searching project dropdown', :js do
new_project_search.team << [user, :reporter]
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
@@ -69,7 +69,7 @@ feature 'issue move to another project' do
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 1f57c110c11..bcc6e9bab0f 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -118,7 +118,7 @@ feature 'Multiple issue updating from issues#index', :js do
end
def click_update_issues_button
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 9f5e25ff2cb..c4c06ed514b 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -226,7 +226,7 @@ feature 'Issues > User uses quick actions', :js do
end
it 'applies the commands to both issues and moves the issue' do
- write_note("/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"\n/move #{target_project.full_path}")
+ write_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
expect(page).to have_content 'Commands applied'
expect(issue.reload).to be_closed
@@ -245,7 +245,7 @@ feature 'Issues > User uses quick actions', :js do
end
it 'moves the issue and applies the commands to both issues' do
- write_note("/move #{target_project.full_path}\n/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"")
+ write_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
expect(page).to have_content 'Commands applied'
expect(issue.reload).to be_closed
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index d4fd3a50008..b9af77f918a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -367,7 +367,7 @@ describe 'Issues' do
it 'changes incoming email address token', :js do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
- find('.incoming-email-token-reset').trigger('click')
+ find('.incoming-email-token-reset').click
wait_for_requests
@@ -583,6 +583,18 @@ describe 'Issues' do
expect(page.find_field("issue_description").value).not_to match /\n\n$/
end
+
+ it "cancels a file upload correctly" do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ click_button 'Cancel'
+ end
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_button('Cancel')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
end
context 'form filled by URL parameters' do
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index b70d3060f05..cc1b187ff54 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -69,6 +69,12 @@ describe 'GitLab Markdown' do
end
end
+ it 'parses mermaid code block' do
+ aggregate_failures do
+ expect(doc).to have_selector('pre.code.js-render-mermaid')
+ end
+ end
+
it 'parses strikethroughs' do
expect(doc).to have_selector(%{del:contains("and this text doesn't")})
end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index ba976bc7216..4e2963c116d 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -23,11 +23,11 @@ feature 'Merge request conflict resolution', :js do
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
all('button', text: 'Use ours').each do |button|
- button.trigger('click')
+ button.send_keys(:return)
end
end
- click_button 'Commit conflict resolution'
+ find_button('Commit conflict resolution').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -71,7 +71,7 @@ feature 'Merge request conflict resolution', :js do
execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
end
- click_button 'Commit conflict resolution'
+ find_button('Commit conflict resolution').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 5402d61da54..db5ce2d11a8 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
+ it 'allows filtering multiple dropdowns' do
+ visit project_new_merge_request_path(project)
+
+ first('.js-source-branch').click
+
+ input = find('.dropdown-source-branch .dropdown-input-field')
+ input.click
+ input.send_keys('orphaned-branch')
+
+ find('.dropdown-source-branch .dropdown-content li', match: :first)
+ source_items = all('.dropdown-source-branch .dropdown-content li')
+
+ expect(source_items.count).to eq(1)
+
+ first('.js-target-branch').click
+
+ find('.dropdown-target-branch .dropdown-content li', match: :first)
+ target_items = all('.dropdown-target-branch .dropdown-content li')
+
+ expect(target_items.count).to be > 1
+ end
+
context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do
private_project = create(:project, :private, :repository)
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 9aa0672feae..9e816cf041b 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -22,7 +22,7 @@ feature 'Diff note avatars', :js do
project.team << [user, :master]
sign_in user
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
end
context 'discussion tab' do
@@ -56,7 +56,7 @@ feature 'Diff note avatars', :js do
end
it 'does not render avatar after commenting' do
- first('.diff-line-num').trigger('mouseover')
+ first('.diff-line-num').click
find('.js-add-diff-note-button').click
page.within('.js-discussion-note-form') do
@@ -85,7 +85,7 @@ feature 'Diff note avatars', :js do
it 'shows note avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
end
@@ -93,7 +93,7 @@ feature 'Diff note avatars', :js do
it 'shows comment on note avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
end
@@ -101,7 +101,7 @@ feature 'Diff note avatars', :js do
it 'toggles comments when clicking avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
end
expect(page).to have_selector('.notes_holder', visible: false)
@@ -117,7 +117,7 @@ feature 'Diff note avatars', :js do
open_more_actions_dropdown(note)
page.within find(".note-row-#{note.id}") do
- find('.js-note-delete').click
+ accept_confirm { find('.js-note-delete').click }
end
wait_for_requests
@@ -139,7 +139,7 @@ feature 'Diff note avatars', :js do
end
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').trigger('click')
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
end
@@ -152,14 +152,14 @@ feature 'Diff note avatars', :js do
page.within '.js-discussion-note-form' do
find('.js-note-text').native.send_keys('Test')
- find('.js-comment-button').trigger('click')
+ find('.js-comment-button').click
wait_for_requests
end
end
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').trigger('click')
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
@@ -177,7 +177,7 @@ feature 'Diff note avatars', :js do
it 'shows extra comment count' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(find('.diff-comments-more-count')).to have_content '+1'
end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 3db0729cafb..15d380b1bf4 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -192,7 +192,7 @@ feature 'Diff notes resolve', :js do
page.find('.discussion-next-btn').click
end
- expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ expect(page.evaluate_script("window.pageYOffset")).to be > 0
end
it 'hides jump to next button when all resolved' do
@@ -241,10 +241,8 @@ feature 'Diff notes resolve', :js do
end
it 'resolves discussion' do
- page.all('.note').each do |note|
- note.all('.line-resolve-btn').each do |button|
- button.click
- end
+ page.all('.note .line-resolve-btn').each do |button|
+ button.click
end
expect(page).to have_content('Resolved by')
@@ -305,10 +303,10 @@ feature 'Diff notes resolve', :js do
end
page.within '.line-resolve-all-container' do
- page.find('.discussion-next-btn').trigger('click')
+ page.find('.discussion-next-btn').click
end
- expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ expect(page.evaluate_script("window.pageYOffset")).to be > 0
end
it 'updates updated text after resolving note' do
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index 2adca58620f..1bf77296ae6 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -7,14 +7,12 @@ feature 'Diffs URL', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
context 'when visit with */* as accept header' do
- before do
- page.driver.add_header('Accept', '*/*')
- end
-
it 'renders the notes' do
create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master'
- visit diffs_project_merge_request_path(project, merge_request)
+ inspect_requests(inject_headers: { 'Accept' => '*/*' }) do
+ visit diffs_project_merge_request_path(project, merge_request)
+ end
# Load notes and diff through AJAX
expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
@@ -90,7 +88,7 @@ feature 'Diffs URL', :js do
visit diffs_project_merge_request_path(project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
- find("[id=\"#{changelog_id}\"] .js-edit-blob").trigger('click')
+ find("[id=\"#{changelog_id}\"] .js-edit-blob").click
expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 9912e8165e6..7adae08e499 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -79,22 +79,6 @@ feature 'Merge Request filtering by Labels', :js do
end
end
- context 'clear button' do
- before do
- input_filtered_search('label:~bug')
- end
-
- it 'allows user to remove filtered labels' do
- first('.clear-search').click
- filtered_search.send_keys(:enter)
-
- expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
- expect(page).to have_content "Bugfix2"
- expect(page).to have_content "Feature1"
- expect(page).to have_content "Bugfix1"
- end
- end
-
context 'filter dropdown' do
it 'filters by label name' do
init_label_search
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 758fc9b139d..1dcc1e139a0 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -43,7 +43,7 @@ describe 'New/edit merge request', :js do
expect(page).to have_content user2.name
end
- find('a', text: 'Assign to me').trigger('click')
+ find('a', text: 'Assign to me').click
expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index bf21a719901..bac56270362 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -92,7 +92,7 @@ feature 'Mini Pipeline Graph', :js do
end
it 'should close when toggle is clicked again' do
- toggle.trigger('click')
+ toggle.click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index 1a41fd36a4f..c5498563b39 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -127,7 +127,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
def click_update_merge_requests_button
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
end
diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
index 7a773fb2baa..d44eb23d7f4 100644
--- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -8,7 +8,7 @@ feature 'Merge requests > User posts diff notes', :js do
let(:project) { merge_request.source_project }
before do
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
project.add_developer(user)
sign_in(user)
@@ -103,7 +103,10 @@ feature 'Merge requests > User posts diff notes', :js do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- first('.js-note-delete', visible: false).trigger('click')
+ accept_confirm do
+ first('button.more-actions-toggle').click
+ first('.js-note-delete').click
+ end
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
@@ -236,7 +239,7 @@ feature 'Merge requests > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side)
- find('.js-close-discussion-note-form').trigger('click')
+ find('.js-close-discussion-note-form').click
assert_comment_dismissal(line_holder)
end
diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index d7cda73ab40..f4c75a2f265 100644
--- a/spec/features/merge_requests/user_posts_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -141,7 +141,7 @@ describe 'Merge requests > User posts notes', :js do
end
it 'removes the attachment div and resets the edit form' do
- find('.js-note-attachment-delete').click
+ accept_confirm { find('.js-note-attachment-delete').click }
is_expected.not_to have_css('.note-attachment')
is_expected.not_to have_css('.current-note-edit-form')
wait_for_requests
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 50f7d721ff3..29f95039af8 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -67,8 +67,8 @@ feature 'Merge Request versions', :js do
line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2'
page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{line_code}'] button").trigger 'click'
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
page.within("form[data-line-code='#{line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
@@ -137,8 +137,8 @@ feature 'Merge Request versions', :js do
line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4'
page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{line_code}'] button").trigger 'click'
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
page.within("form[data-line-code='#{line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index 5658c2c5122..72a52c979b3 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -42,7 +42,7 @@ feature 'Widget Deployments Header', :js do
end
scenario 'does start build when stop button clicked' do
- click_button('Stop environment')
+ accept_confirm { click_button('Stop environment') }
expect(page).to have_content('close_app')
end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 6c9dc67ad74..27efc32c95b 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -65,4 +65,33 @@ feature 'Milestone' do
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
end
end
+
+ feature 'Open a milestone' do
+ scenario 'shows total issue time spent correctly when no time has been logged' do
+ milestone = create(:milestone, project: project, title: 8.7)
+
+ visit project_milestone_path(project, milestone)
+
+ page.within('.block.time_spent') do
+ expect(page).to have_content 'No time spent'
+ expect(page).to have_content 'None'
+ end
+ end
+
+ scenario 'shows total issue time spent' do
+ milestone = create(:milestone, project: project, title: 8.7)
+ issue1 = create(:issue, project: project, milestone: milestone)
+ issue2 = create(:issue, project: project, milestone: milestone)
+ issue1.spend_time(duration: 3600, user: user)
+ issue1.save!
+ issue2.spend_time(duration: 7200, user: user)
+ issue2.save!
+
+ visit project_milestone_path(project, milestone)
+
+ page.within('.block.time_spent') do
+ expect(page).to have_content '3h'
+ end
+ end
+ end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 1cddd35fd8a..c60883911f7 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Profile account page' do
+describe 'Profile account page', :js do
let(:user) { create(:user) }
before do
@@ -56,47 +56,38 @@ describe 'Profile account page' do
end
end
- describe 'when I reset private token' do
- before do
- visit profile_account_path
- end
-
- it 'resets private token' do
- previous_token = find("#private-token").value
-
- click_link('Reset private token')
-
- expect(find('#private-token').value).not_to eq(previous_token)
- end
- end
-
describe 'when I reset RSS token' do
before do
- visit profile_account_path
+ visit profile_personal_access_tokens_path
end
it 'resets RSS token' do
- previous_token = find("#rss-token").value
+ within('.rss-token-reset') do
+ previous_token = find("#rss_token").value
- click_link('Reset RSS token')
+ accept_confirm { click_link('reset it') }
+
+ expect(find('#rss_token').value).not_to eq(previous_token)
+ end
expect(page).to have_content 'RSS token was successfully reset'
- expect(find('#rss-token').value).not_to eq(previous_token)
end
end
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- visit profile_account_path
+ visit profile_personal_access_tokens_path
end
it 'resets incoming email token' do
- previous_token = find('#incoming-email-token').value
+ within('.incoming-email-token-reset') do
+ previous_token = find('#incoming_email_token').value
- click_link('Reset incoming email token')
+ accept_confirm { click_link('reset it') }
- expect(find('#incoming-email-token').value).not_to eq(previous_token)
+ expect(find('#incoming_email_token').value).not_to eq(previous_token)
+ end
end
end
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 8cb240077eb..d1edeef8da4 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -14,7 +14,7 @@ describe 'Profile > Applications' do
page.within('.oauth-applications') do
expect(page).to have_content('Your applications (1)')
- click_button 'Destroy'
+ accept_confirm { click_button 'Destroy' }
end
expect(page).to have_content('The application was deleted successfully')
@@ -28,7 +28,7 @@ describe 'Profile > Applications' do
page.within('.oauth-authorized-applications') do
expect(page).to have_content('Authorized applications (1)')
- click_button 'Revoke'
+ accept_confirm { click_button 'Revoke' }
end
expect(page).to have_content('The application was revoked access.')
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index a572160dae9..8461cd0027c 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -34,7 +34,7 @@ describe 'Profile > Personal Access Tokens', :js do
fill_in "Name", with: name
# Set date to 1st of next month
- find_field("Expires at").trigger('focus')
+ find_field("Expires at").click
find(".pika-next").click
click_on "1"
@@ -78,7 +78,7 @@ describe 'Profile > Personal Access Tokens', :js do
it "allows revocation of an active token" do
visit profile_personal_access_tokens_path
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
expect(no_personal_access_tokens_message).to have_text("This user has no active Personal Access Tokens.")
@@ -100,7 +100,7 @@ describe 'Profile > Personal Access Tokens', :js do
errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
expect(page).to have_content("Could not revoke")
end
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index 923ca8b1c80..df89918f17a 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -13,7 +13,7 @@ feature 'User visits the notifications tab', :js do
it 'changes the project notifications setting' do
expect(page).to have_content('Notifications')
- first('#notifications-button').trigger('click')
+ first('#notifications-button').click
click_link('On mention')
expect(page).to have_content('On mention')
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 924ee0e4174..90d6841af0e 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -53,7 +53,7 @@ describe 'User visits the profile preferences page' do
expect(page).to have_content("You don't have starred projects yet")
expect(page.current_path).to eq starred_dashboard_projects_path
- find('.shortcuts-activity').trigger('click')
+ find('.shortcuts-activity').click
expect(page).not_to have_content("You don't have starred projects yet")
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb
index f1bdb2812c6..6f76c14910b 100644
--- a/spec/features/projects/artifacts/download_spec.rb
+++ b/spec/features/projects/artifacts/download_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Download artifact', :js do
+feature 'Download artifact' do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) }
let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
index b2be10a7e0c..df1d17bdcb7 100644
--- a/spec/features/projects/artifacts/file_spec.rb
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -39,7 +39,6 @@ feature 'Artifact file', :js do
context 'JPG file' do
before do
- page.driver.browser.url_blacklist = []
visit_file('rails_sample.jpg')
wait_for_requests
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 941d34dd660..7a77df83034 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -67,7 +67,7 @@ describe 'Branches' do
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
- find('.js-branch-fix .btn-remove').trigger(:click)
+ accept_confirm { find('.js-branch-fix .btn-remove').click }
expect(page).not_to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 0)
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 810f2c39b43..197e6df4997 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Clusters', :js do
+ include GoogleApi::CloudPlatformHelpers
+
let!(:project) { create(:project, :repository) }
let!(:user) { create(:user) }
@@ -11,13 +13,17 @@ feature 'Clusters', :js do
context 'when user has signed in Google' do
before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:validate_token).and_return(true)
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
end
context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
+
+ click_link 'Create on GKE'
end
it 'user sees a new page' do
@@ -36,18 +42,32 @@ feature 'Clusters', :js do
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
- fill_in 'cluster_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster'
+ fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster'
end
it 'user sees a cluster details page and creation status' do
expect(page).to have_content('Cluster is being created on Google Container Engine...')
- Gcp::Cluster.last.make_created!
+ # Application Installation buttons
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
+ end
+
+ Clusters::Cluster.last.provider.make_created!
expect(page).to have_content('Cluster was successfully created on Google Container Engine')
end
+
+ it 'user sees a error if something worng during creation' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+
+ expect(page).to have_content('Something wrong!')
+ end
end
context 'when user filled form with invalid parameters' do
@@ -62,7 +82,8 @@ feature 'Clusters', :js do
end
context 'when user has a cluster and visits cluster index page' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
before do
visit project_clusters_path(project)
@@ -70,7 +91,79 @@ feature 'Clusters', :js do
it 'user sees an cluster details page' do
expect(page).to have_button('Save')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name)
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+
+ # Application Installation buttons
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ end
+ end
+
+ context 'when user installs application: Helm Tiller' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+
+ page.within('.js-cluster-application-row-helm') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'user sees status transition' do
+ page.within('.js-cluster-application-row-helm') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_helm.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_helm.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
+ end
+ end
+
+ context 'when user installs application: Ingress' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+ # Helm Tiller needs to be installed before you can install Ingress
+ create(:cluster_applications_helm, :installed, cluster: cluster)
+
+ visit project_clusters_path(project)
+
+ page.within('.js-cluster-application-row-ingress') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'user sees status transition' do
+ page.within('.js-cluster-application-row-ingress') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_ingress.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_ingress.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Ingress was successfully installed on your cluster')
+ end
end
context 'when user disables the cluster' do
@@ -93,7 +186,7 @@ feature 'Clusters', :js do
it 'user sees creation form with the succeccful message' do
expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_button('Create cluster')
+ expect(page).to have_link('Create on GKE')
end
end
end
@@ -102,6 +195,8 @@ feature 'Clusters', :js do
context 'when user has not signed in Google' do
before do
visit project_clusters_path(project)
+
+ click_link 'Create on GKE'
end
it 'user sees a login page' do
diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb
index f0fe4e00acc..4dbfc6f6edf 100644
--- a/spec/features/projects/commit/diff_notes_spec.rb
+++ b/spec/features/projects/commit/diff_notes_spec.rb
@@ -20,8 +20,8 @@ feature 'Commit diff', :js do
it "adds comment to diff" do
diff_line_num = first('.diff-line-num.new')
- diff_line_num.trigger('mouseover')
- diff_line_num.find('.js-add-diff-note-button').trigger('click')
+ diff_line_num.hover
+ diff_line_num.find('.js-add-diff-note-button').click
page.within(first('.diff-viewer')) do
find('.js-note-text').set 'test comment'
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 807a2189cc4..91282063a8d 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -12,6 +12,13 @@ feature 'Mini Pipeline Graph in Commit View', :js do
end
let(:build) { create(:ci_build, pipeline: pipeline) }
+ it 'display icon with status' do
+ build.run
+ visit project_commit_path(project, project.commit.id)
+
+ expect(page).to have_selector('.ci-status-icon-running')
+ end
+
it 'displays a mini pipeline graph' do
build.run
visit project_commit_path(project, project.commit.id)
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 2d1a9b931b5..e445758cb5e 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -20,7 +20,7 @@ describe 'Project deploy keys', :js do
page.within(find('.deploy-keys')) do
expect(page).to have_selector('.deploy-keys li', count: 1)
- click_on 'Remove'
+ accept_confirm { find(:button, text: 'Remove').send_keys(:return) }
expect(page).not_to have_selector('.fa-spinner', count: 0)
expect(page).to have_selector('.deploy-keys li', count: 0)
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 0fe1eb4c293..5fc3ba54f65 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -193,12 +193,14 @@ feature 'Environment' do
create(:environment, project: project,
name: 'staging-1.0/review',
state: :available)
-
- visit folder_project_environments_path(project, id: 'staging-1.0')
end
it 'renders a correct environment folder' do
- expect(page).to have_gitlab_http_status(:ok)
+ reqs = inspect_requests do
+ visit folder_project_environments_path(project, id: 'staging-1.0')
+ end
+
+ expect(reqs.first.status_code).to eq(200)
expect(page).to have_content('Environments / staging-1.0')
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 610f566c0cf..b4eb5795470 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -151,7 +151,7 @@ feature 'Environments page', :js do
find('.js-dropdown-play-icon-container').click
expect(page).to have_content(action.name.humanize)
- expect { find('.js-manual-action-link').trigger('click') }
+ expect { find('.js-manual-action-link').click }
.not_to change { Ci::Pipeline.count }
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index e5282b42a4f..951456763dc 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -22,7 +22,7 @@ describe 'Edit Project Settings' do
# disable by clicking toggle
toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- click_button 'Save changes'
+ find('input[value="Save changes"]').click
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
@@ -30,7 +30,7 @@ describe 'Edit Project Settings' do
# re-enable by clicking toggle again
toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- click_button 'Save changes'
+ find('input[value="Save changes"]').click
end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
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 25f7e18ac5c..3ab43b3c656 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -7,18 +7,18 @@ feature 'User uses soft wrap whilst editing file', :js do
project.team << [user, :master]
sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
- editor = find('.file-editor.code')
- editor.click
- editor.send_keys 'Touch water with paw then recoil in horror chase dog then
- run away chase the pig around the house eat owner\'s food, and knock
- dish off table head butt cant eat out of my own dish. Cat is love, cat
- is life rub face on everything poop on grasses so meow. Playing with
- balls of wool flee in terror at cucumber discovered on floor run in
- circles tuxedo cats always looking dapper, but attack dog, run away
- and pretend to be victim so all of a sudden cat goes crazy, yet chase
- laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
- hanging out of own butt jump off balcony, onto stranger\'s head yet
- chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ page.within('.file-editor.code') do
+ find('.ace_text-input', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
+ run away chase the pig around the house eat owner\'s food, and knock
+ dish off table head butt cant eat out of my own dish. Cat is love, cat
+ is life rub face on everything poop on grasses so meow. Playing with
+ balls of wool flee in terror at cucumber discovered on floor run in
+ circles tuxedo cats always looking dapper, but attack dog, run away
+ and pretend to be victim so all of a sudden cat goes crazy, yet chase
+ laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
+ hanging out of own butt jump off balcony, onto stranger\'s head yet
+ chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ end
end
let(:toggle_button) { find('.soft-wrap-toggle') }
@@ -36,6 +36,6 @@ feature 'User uses soft wrap whilst editing file', :js do
end
def get_content_width
- find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/)
+ find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 05776c50f9d..461aa39d0ad 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -41,7 +41,7 @@ feature 'Import/Export - project export integration test', :js do
expect(page).to have_content('Export project')
- click_link 'Export project'
+ find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 026aa03f7cf..af125e1b9d3 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -94,6 +94,6 @@ feature 'Import/Export - project import integration test', :js do
end
def click_import_project_tab
- find('#import-project-tab').trigger('click')
+ find('#import-project-tab').click
end
end
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index b6a7c3cdcdb..e76bc6f1220 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -52,7 +52,7 @@ feature 'Import/Export - Namespace export file cleanup', :js do
expect(page).to have_content('Export project')
- click_link 'Export project'
+ find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 9614c72cdc3..fb6a3b8e733 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/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 21c9acc7ac0..5d9208ebadd 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -21,12 +21,12 @@ describe 'User browses a job', :js do
expect(page).to have_content("Job ##{build.id}")
expect(page).to have_css('#build-trace')
- click_link('Erase')
+ accept_confirm { click_link('Erase') }
+ expect(page).to have_no_css('.artifacts')
expect(build).not_to have_trace
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
- expect(page).to have_no_css('.artifacts')
page.within('.erased') do
expect(page).to have_content('Job has been erased')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index b095c3e6f7b..c2a0d2395a9 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -380,7 +380,6 @@ feature 'Jobs' do
end
it 'loads the page and shows all needed controls' do
- expect(page.status_code).to eq(200)
expect(page).to have_content 'Retry'
end
end
@@ -392,11 +391,10 @@ feature 'Jobs' do
job.run!
visit project_job_path(project, job)
find('.js-cancel-job').click()
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it 'shows the right status and buttons', :js do
- expect(page).to have_gitlab_http_status(200)
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
@@ -443,28 +441,30 @@ feature 'Jobs' do
context 'access source' do
context 'job from project' do
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
job.run!
- visit project_job_path(project, job)
- find('.js-raw-link-controller').click()
end
it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path))
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job)
+ end
+
+ expect(requests.first.status_code).to eq(200)
+ expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(requests.first.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path))
end
end
context 'job from other project' do
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
job2.run!
- visit raw_project_job_path(project, job2)
end
it 'sends the right headers' do
- expect(page.status_code).to eq(404)
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job2)
+ end
+ expect(requests.first.status_code).to eq(404)
end
end
end
@@ -473,8 +473,6 @@ feature 'Jobs' do
let(:existing_file) { Tempfile.new('existing-trace-file').path }
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
-
job.run!
end
@@ -483,16 +481,14 @@ feature 'Jobs' do
allow_any_instance_of(Gitlab::Ci::Trace)
.to receive(:paths)
.and_return([existing_file])
-
- visit project_job_path(project, job)
-
- find('.js-raw-link-controller').click
end
it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job)
+ end
+ expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(requests.first.response_headers['X-Sendfile']).to eq(existing_file)
end
end
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index b1053982eee..7f067aadec6 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -31,6 +31,7 @@ feature 'Projects > Members > Groups with access list', :js do
tomorrow = Date.today + 3
fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
+ find('body').click
wait_for_requests
page.within(find('li.group_member')) do
@@ -40,7 +41,7 @@ feature 'Projects > Members > Groups with access list', :js do
scenario 'deletes group link' do
page.within(first('.group_member')) do
- find('.btn-remove').click
+ accept_confirm { find('.btn-remove').click }
end
wait_for_requests
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 237c059e595..65b11a1d9e7 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -55,6 +55,22 @@ feature 'Project members list' do
end
end
+ scenario 'remove user from project', :js do
+ other_user = create(:user)
+ project.add_developer(other_user)
+
+ visit_members_page
+
+ accept_confirm do
+ find(:css, 'li.project_member', text: other_user.name).find(:css, 'a.btn-remove').click
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_content(other_user.name)
+ expect(project.users).not_to include(other_user)
+ end
+
scenario 'invite user to project', :js do
visit_members_page
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index 5f7b4ee0e77..0f88f4cb1e8 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -20,7 +20,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
- fill_in 'expires_at', with: date.to_s(:medium)
+ fill_in 'expires_at', with: date.to_s(:medium) + "\n"
click_on 'Add to project'
end
@@ -37,7 +37,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do
visit project_project_members_path(project)
page.within "#project_member_#{new_member.project_members.first.id}" do
- find('.js-access-expiration-date').set date.to_s(:medium)
+ find('.js-access-expiration-date').set date.to_s(:medium) + "\n"
wait_for_requests
expect(page).to have_content('Expires in 3 days')
end
diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb
index 63b5df5a8f5..3198798306c 100644
--- a/spec/features/projects/members/share_with_group_spec.rb
+++ b/spec/features/projects/members/share_with_group_spec.rb
@@ -41,7 +41,7 @@ feature 'Project > Members > Share with Group', :js do
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
- find('.btn-create').trigger('click')
+ find('.btn-create').click
page.within('.project-members-groups') do
expect(page).to have_content(group_to_share_with.name)
@@ -123,7 +123,7 @@ feature 'Project > Members > Share with Group', :js do
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
- find('.btn-create').trigger('click')
+ find('.btn-create').click
end
scenario 'the group link shows the expiration time with a warning class' do
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 0fbe1ddb2a5..4eb36156812 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -60,7 +60,7 @@ feature 'Projects > Members > User requests access', :js do
expect(project.requesters.exists?(user_id: user)).to be_truthy
- click_link 'Withdraw Access Request'
+ accept_confirm { click_link 'Withdraw Access Request' }
expect(project.requesters.exists?(user_id: user)).to be_falsey
expect(page).to have_content 'Your access request to the project has been withdrawn.'
diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
index f34302f25f8..e3f90a78cb5 100644
--- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -31,7 +31,7 @@ describe 'User comments on a diff', :js do
page.within('.files > div:nth-child(3)') do
expect(page).to have_content('Line is wrong')
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -64,7 +64,7 @@ describe 'User comments on a diff', :js do
# Hide the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -77,7 +77,7 @@ describe 'User comments on a diff', :js do
# Show the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
end
# Now both the comments should be shown.
@@ -90,6 +90,7 @@ describe 'User comments on a diff', :js do
end
# Check the same comments in the side-by-side view.
+ execute_script("window.scrollTo(0,0);")
click_link('Side-by-side')
wait_for_requests
@@ -153,11 +154,11 @@ describe 'User comments on a diff', :js do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
- find('.js-note-delete').click
+ accept_confirm { find('.js-note-delete').click }
end
page.within('.merge-request-tabs') do
- find('.notes-tab').trigger('click')
+ find('.notes-tab').click
end
wait_for_requests
diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
index f6e3997383f..3d19a2923b9 100644
--- a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'User edits a merge request', :js do
+ include Select2Helper
+
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -15,8 +17,7 @@ describe 'User edits a merge request', :js do
it 'changes the target branch' do
expect(page).to have_content('Target branch')
- first('.target_branch').click
- select('merge-test', from: 'merge_request_target_branch', visible: false)
+ select2('merge-test', from: '#merge_request_target_branch')
click_button('Save changes')
expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test")
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
index 30a80f8e652..4ca435491cb 100644
--- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -13,7 +13,7 @@ describe 'User manages subscription', :js do
end
it 'toggles subscription' do
- subscribe_button = find('.issuable-subscribe-button span')
+ subscribe_button = find('.js-issuable-subscribe-button')
expect(subscribe_button).to have_content('Subscribe')
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 8e11cb94350..6f097ad16c7 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -15,7 +15,7 @@ feature 'New project' do
expect(page).to have_content('Project path')
expect(page).to have_content('Project name')
- find('#import-project-tab').trigger('click')
+ find('#import-project-tab').click
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
@@ -137,7 +137,7 @@ feature 'New project' do
context 'Import project options', :js do
before do
visit new_project_path
- find('#import-project-tab').trigger('click')
+ find('#import-project-tab').click
end
context 'from git repository url' do
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 24b335a7068..fa2f7a1fd78 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -54,7 +54,7 @@ feature 'Pipeline Schedules', :js do
end
it 'deletes the pipeline' do
- click_link 'Delete'
+ accept_confirm { click_link 'Delete' }
expect(page).not_to have_css(".pipeline-schedule-table-row")
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index acbc5b046e6..b8fa1a54c24 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -67,13 +67,13 @@ describe 'Pipeline', :js do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
expect(page).to have_selector('.js-ci-status-icon-running')
- expect(page).to have_selector('.js-icon-action-cancel')
+ expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('deploy')
end
end
it 'should be possible to cancel the running build' do
- find('#ci-badge-deploy .ci-action-icon-container').trigger('click')
+ find('#ci-badge-deploy .ci-action-icon-container').click
expect(page).not_to have_content('Cancel running')
end
@@ -86,13 +86,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('build')
end
- page.within('#ci-badge-build .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-retry')
+ page.within('#ci-badge-build .ci-action-icon-container.js-icon-retry') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to retry the success job' do
- find('#ci-badge-build .ci-action-icon-container').trigger('click')
+ find('#ci-badge-build .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
@@ -105,13 +105,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('test')
end
- page.within('#ci-badge-test .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-retry')
+ page.within('#ci-badge-test .ci-action-icon-container.js-icon-retry') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to retry the failed build' do
- find('#ci-badge-test .ci-action-icon-container').trigger('click')
+ find('#ci-badge-test .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
@@ -124,13 +124,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('manual')
end
- page.within('#ci-badge-manual-build .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-play')
+ page.within('#ci-badge-manual-build .ci-action-icon-container.js-icon-play') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to play the manual job' do
- find('#ci-badge-manual-build .ci-action-icon-container').trigger('click')
+ find('#ci-badge-manual-build .ci-action-icon-container').click
expect(page).not_to have_content('Play job')
end
@@ -165,7 +165,7 @@ describe 'Pipeline', :js do
context 'when retrying' do
before do
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it { expect(page).not_to have_content('Retry') }
@@ -231,7 +231,7 @@ describe 'Pipeline', :js do
context 'when retrying' do
before do
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it { expect(page).not_to have_content('Retry') }
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index ae888fd4343..50f8f13d261 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -56,31 +56,37 @@ describe 'Pipelines', :js do
end
it 'shows a tab for All pipelines and count' do
- expect(page.find('.js-pipelines-tab-all a').text).to include('All')
+ expect(page.find('.js-pipelines-tab-all').text).to include('All')
expect(page.find('.js-pipelines-tab-all .badge').text).to include('1')
end
it 'shows a tab for Pending pipelines and count' do
- expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending')
+ expect(page.find('.js-pipelines-tab-pending').text).to include('Pending')
expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0')
end
it 'shows a tab for Running pipelines and count' do
- expect(page.find('.js-pipelines-tab-running a').text).to include('Running')
+ expect(page.find('.js-pipelines-tab-running').text).to include('Running')
expect(page.find('.js-pipelines-tab-running .badge').text).to include('1')
end
it 'shows a tab for Finished pipelines and count' do
- expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished')
+ expect(page.find('.js-pipelines-tab-finished').text).to include('Finished')
expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0')
end
it 'shows a tab for Branches' do
- expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches')
+ expect(page.find('.js-pipelines-tab-branches').text).to include('Branches')
end
it 'shows a tab for Tags' do
- expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags')
+ expect(page.find('.js-pipelines-tab-tags').text).to include('Tags')
+ end
+
+ it 'updates content when tab is clicked' do
+ page.find('.js-pipelines-tab-pending').click
+ wait_for_requests
+ expect(page).to have_content('No pipelines to show.')
end
end
@@ -103,7 +109,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
- find('.js-pipelines-cancel-button').click
+ accept_confirm { find('.js-pipelines-cancel-button').click }
wait_for_requests
end
@@ -232,7 +238,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
- find('.js-pipelines-cancel-button').trigger('click')
+ accept_alert { find('.js-pipelines-cancel-button').click }
end
it 'indicates that pipeline was canceled' do
@@ -345,14 +351,14 @@ describe 'Pipelines', :js do
context 'when clicking a stage badge' do
it 'should open a dropdown' do
- find('.js-builds-dropdown-button').trigger('click')
+ find('.js-builds-dropdown-button').click
expect(page).to have_link build.name
end
it 'should be possible to cancel pending build' do
- find('.js-builds-dropdown-button').trigger('click')
- find('a.js-ci-action-icon').trigger('click')
+ find('.js-builds-dropdown-button').click
+ find('a.js-ci-action-icon').click
expect(page).to have_content('canceled')
expect(build.reload).to be_canceled
@@ -361,11 +367,16 @@ describe 'Pipelines', :js do
context 'dropdown jobs list' do
it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
- find('.js-builds-dropdown-button').trigger('click')
-
- execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);')
-
- find('.mini-pipeline-graph-dropdown-item').trigger('click')
+ find('.js-builds-dropdown-button').click
+ dropdown_item = find('.mini-pipeline-graph-dropdown-item').native
+
+ %i(alt control).each do |meta_key|
+ page.driver.browser.action
+ .key_down(meta_key)
+ .click(dropdown_item)
+ .key_up(meta_key)
+ .perform
+ end
expect(page).to have_selector('.js-ci-action-icon')
end
@@ -391,6 +402,14 @@ describe 'Pipelines', :js do
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
+
+ it 'should show updated content' do
+ visit project_pipelines_path(project)
+ wait_for_requests
+ page.find('.js-next-button a').click
+
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ end
end
end
@@ -525,7 +544,6 @@ describe 'Pipelines', :js do
let(:project) { create(:project, :public, :repository) }
it { expect(page).to have_content 'Build with confidence' }
- it { expect(page).to have_gitlab_http_status(:success) }
end
context 'when project is private' do
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 15a5cd9990b..a3ea778d401 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -144,7 +144,10 @@ describe 'Edit Project Settings' do
specify 'the project is accessible via the new path' do
transfer_project(project, group)
new_path = namespace_project_path(group, project)
+
visit new_path
+ wait_for_requests
+
expect(current_path).to eq(new_path)
expect(find('.breadcrumbs')).to have_content(project.name)
end
@@ -153,7 +156,10 @@ describe 'Edit Project Settings' do
old_path = project_path(project)
transfer_project(project, group)
new_path = namespace_project_path(group, project)
+
visit old_path
+ wait_for_requests
+
expect(current_path).to eq(new_path)
expect(find('.breadcrumbs')).to have_content(project.name)
end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 50c0bfd580d..33ccbc1a29f 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -6,7 +6,7 @@ feature 'Ref switcher', :js do
before do
project.team << [user, :master]
- page.driver.set_cookie('new_repo', 'true')
+ set_cookie('new_repo', 'true')
sign_in(user)
visit project_tree_path(project, 'master')
end
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb
index 0a86292ae6c..ac78b1dfb1c 100644
--- a/spec/features/projects/services/user_activates_jira_spec.rb
+++ b/spec/features/projects/services/user_activates_jira_spec.rb
@@ -65,7 +65,7 @@ describe 'User activates Jira', :js do
expect(find('.flash-container-page')).to have_content 'Test failed. message'
expect(find('.flash-container-page')).to have_content 'Save anyway'
- find('.flash-alert .flash-action').trigger('click')
+ find('.flash-alert .flash-action').click
wait_for_requests
expect(page).to have_content('JIRA activated.')
diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
index 95d5e8b14b9..6f057137867 100644
--- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
@@ -76,7 +76,7 @@ feature 'Setup Mattermost slash commands', :js do
select_element = find('#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to be(true)
+ expect(select_element['disabled']).to eq("true")
expect(selected_option).to have_content(team_name.to_s)
end
@@ -104,7 +104,7 @@ feature 'Setup Mattermost slash commands', :js do
select_element = find('#mattermost_team_id')
- expect(select_element['disabled']).to be(false)
+ expect(select_element['disabled']).to be_falsey
expect(select_element.all('option').count).to eq(3)
end
@@ -122,7 +122,7 @@ feature 'Setup Mattermost slash commands', :js do
click_link 'Add to Mattermost'
- expect(find('input[type="submit"]')['disabled']).not_to be(true)
+ expect(find('input[type="submit"]')['disabled']).not_to eq("true")
end
it 'disables the submit button if the required fields are not provided', :js do
@@ -132,7 +132,7 @@ feature 'Setup Mattermost slash commands', :js do
fill_in('mattermost_trigger', with: '')
- expect(find('input[type="submit"]')['disabled']).to be(true)
+ expect(find('input[type="submit"]')['disabled']).to eq("true")
end
def stub_teams(count: 0)
diff --git a/spec/features/projects/services/user_activates_packagist_spec.rb b/spec/features/projects/services/user_activates_packagist_spec.rb
new file mode 100644
index 00000000000..b0cc818f093
--- /dev/null
+++ b/spec/features/projects/services/user_activates_packagist_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'User activates Packagist' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Packagist')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Username', with: 'theUser')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('Packagist activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
index f86591c2633..5c5e8b66642 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -21,5 +21,6 @@ describe 'User views services' do
expect(page).to have_content('JetBrains TeamCity')
expect(page).to have_content('Asana')
expect(page).to have_content('Irker (IRC gateway)')
+ expect(page).to have_content('Packagist')
end
end
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
new file mode 100644
index 00000000000..28954a4fb40
--- /dev/null
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Settings for a forked project', :js do
+ include ProjectForksHelper
+ let(:user) { create(:user) }
+ let(:original_project) { create(:project) }
+ let(:forked_project) { fork_project(original_project, user) }
+
+ before do
+ original_project.add_master(user)
+ forked_project.add_master(user)
+ sign_in(user)
+ end
+
+ shared_examples 'project settings for a forked projects' do
+ it 'allows deleting the link to the forked project' do
+ visit edit_project_path(forked_project)
+
+ click_button 'Remove fork relationship'
+
+ wait_for_requests
+
+ fill_in('confirm_name_input', with: forked_project.name)
+ click_button('Confirm')
+
+ expect(page).to have_content('The fork relationship has been removed.')
+ expect(forked_project.reload.forked?).to be_falsy
+ end
+ end
+
+ it_behaves_like 'project settings for a forked projects'
+
+ context 'when the original project is deleted' do
+ before do
+ original_project.destroy!
+ end
+
+ it_behaves_like 'project settings for a forked projects'
+ end
+end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index b1ec556bf16..ac76c30cc7c 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -21,7 +21,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
@@ -41,7 +41,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
@@ -62,7 +62,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index de8fbb15b9c..ea8f997409d 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -22,7 +22,7 @@ feature "Pipelines settings" do
context 'for master' do
given(:role) { :master }
- scenario 'be allowed to change', :js do
+ scenario 'be allowed to change' do
fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes'
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index a4fefb0d0e7..e2a5619c22b 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -34,7 +34,6 @@ feature 'Repository settings' do
visit project_settings_repository_path(project)
- expect(page.status_code).to eq(200)
expect(page).to have_content('private_deploy_key')
expect(page).to have_content('public_deploy_key')
end
@@ -86,7 +85,7 @@ feature 'Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- find('li', text: private_deploy_key.title).click_button('Remove')
+ accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') }
expect(page).not_to have_content(private_deploy_key.title)
end
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index 3e79dba3f19..e4215291f99 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -10,7 +10,7 @@ feature 'Create Snippet', :js do
fill_in 'project_snippet_title', with: 'My Snippet Title'
fill_in 'project_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- find('.ace_editor').native.send_keys('Hello World!')
+ find('.ace_text-input', visible: false).send_keys('Hello World!')
end
end
@@ -59,7 +59,7 @@ feature 'Create Snippet', :js do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
- click_button('Create snippet')
+ find("input[value='Create snippet']").send_keys(:return)
wait_for_requests
expect(page).to have_content('My Snippet Title')
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 4c1fa5a666e..1686e7fa342 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Multi-file editor new directory', :js do
- include WaitForRequests
-
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -10,7 +8,7 @@ feature 'Multi-file editor new directory', :js do
project.add_master(user)
sign_in(user)
- page.driver.set_cookie('new_repo', 'true')
+ set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
@@ -32,12 +30,6 @@ feature 'Multi-file editor new directory', :js do
click_button('Commit 1 file')
- expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
-
- click_link('foldername')
-
- expect(page).to have_selector('td', text: 'commit message', count: 2)
- expect(page).to have_selector('td', text: '.gitkeep')
end
end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index a67ec891e7c..1e2de0711b8 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Multi-file editor new file', :js do
- include WaitForRequests
-
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -10,7 +8,7 @@ feature 'Multi-file editor new file', :js do
project.add_master(user)
sign_in(user)
- page.driver.set_cookie('new_repo', 'true')
+ set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
@@ -28,13 +26,10 @@ feature 'Multi-file editor new file', :js do
click_button('Create file')
end
- find('.inputarea').send_keys('file content')
-
fill_in('commit-message', with: 'commit message')
click_button('Commit 1 file')
- expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
end
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
new file mode 100644
index 00000000000..8439bb5a69e
--- /dev/null
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+feature 'Multi-file editor upload file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
+ let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ set_cookie('new_repo', 'true')
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+ end
+
+ it 'uploads text file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', txt_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.repo-tab', text: 'doc_sample.txt')
+ expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
+ end
+
+ it 'uploads image file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', img_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.repo-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 f43b11c9485..f5e4d7f5130 100644
--- a/spec/features/projects/user_browses_files_spec.rb
+++ b/spec/features/projects/user_browses_files_spec.rb
@@ -175,10 +175,11 @@ describe 'User browses files' do
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ fill_in(:branch_name, with: 'new_branch_name', visible: true)
+ click_button('Upload file')
end
- fill_in(:branch_name, with: 'new_branch_name', visible: true)
- click_button('Upload file')
+ wait_for_all_requests
visit(project_blob_path(project, 'new_branch_name/logo_sample.svg'))
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 4a152572502..f95469ad070 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -6,10 +6,11 @@ feature 'User creates a project', :js do
before do
sign_in(user)
create(:personal_key, user: user)
- visit(new_project_path)
end
it 'creates a new project' do
+ visit(new_project_path)
+
fill_in(:project_path, with: 'Empty')
page.within('#content-body') do
@@ -24,4 +25,32 @@ feature 'User creates a project', :js do
expect(page).to have_content('git remote')
expect(page).to have_content(project.url_to_repo)
end
+
+ context 'in a subgroup they do not own', :nested_groups do
+ let(:parent) { create(:group) }
+ let!(:subgroup) { create(:group, parent: parent) }
+
+ before do
+ parent.add_owner(user)
+ end
+
+ it 'creates a new project' do
+ visit(new_project_path)
+
+ fill_in :project_path, with: 'a-subgroup-project'
+
+ page.find('.js-select-namespace').click
+ page.find("div[role='option']", text: subgroup.full_path).click
+
+ page.within('#content-body') do
+ click_button('Create project')
+ end
+
+ expect(page).to have_content("Project 'a-subgroup-project' was successfully created")
+
+ project = Project.last
+
+ expect(project.namespace).to eq(subgroup)
+ end
+ end
end
diff --git a/spec/features/projects/user_transfers_a_project_spec.rb b/spec/features/projects/user_transfers_a_project_spec.rb
new file mode 100644
index 00000000000..78f72b644ff
--- /dev/null
+++ b/spec/features/projects/user_transfers_a_project_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+feature 'User transfers a project', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ before do
+ sign_in user
+ end
+
+ def transfer_project(project, group)
+ visit edit_project_path(project)
+
+ page.within('.js-project-transfer-form') do
+ page.find('.select2-container').click
+ end
+
+ page.find("div[role='option']", text: group.full_name).click
+
+ click_button('Transfer project')
+
+ fill_in 'confirm_name_input', with: project.name
+
+ click_button 'Confirm'
+
+ wait_for_requests
+ end
+
+ it 'allows transferring a project to a subgroup of a namespace' do
+ group = create(:group)
+ group.add_owner(user)
+
+ transfer_project(project, group)
+
+ expect(project.reload.namespace).to eq(group)
+ end
+
+ context 'when nested groups are available', :nested_groups do
+ it 'allows transferring a project to a subgroup' do
+ parent = create(:group)
+ parent.add_owner(user)
+ subgroup = create(:group, parent: parent)
+
+ transfer_project(project, subgroup)
+
+ expect(project.reload.namespace).to eq(subgroup)
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index d63cbe578d8..337baaf4dcd 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -18,13 +18,13 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
sign_in(user)
visit project_path(project)
- find('.shortcuts-wiki').trigger('click')
+ find('.shortcuts-wiki').click
end
context "while creating a new wiki page" do
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- find('.add-new-wiki').trigger('click')
+ find('.add-new-wiki').click
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: 'a/b/c/d'
click_button 'Create page'
@@ -91,7 +91,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
context "while editing a wiki page" do
def create_wiki_page(path)
- find('.add-new-wiki').trigger('click')
+ find('.add-new-wiki').click
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: path
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index e72b7dc0dd5..4a9d1cb87e1 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -77,14 +77,14 @@ describe 'User creates wiki page' do
[stem]
++++
- \sqrt{4} = 2
+ \\sqrt{4} = 2
++++
another part
[latexmath]
++++
- \beta_x \gamma
+ \\beta_x \\gamma
++++
stem:[2+2] is 4
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 470391dc66b..ff325aeadd3 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -81,10 +81,15 @@ describe 'User views a wiki page' do
end
it 'shows a file stored in a page' do
- file = Gollum::File.new(project.wiki)
+ gollum_file_double = double('Gollum::File',
+ mime_type: 'image/jpeg',
+ name: 'images/image.jpg',
+ path: 'images/image.jpg',
+ raw_data: '')
+ wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
- allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master').and_return(file)
- allow_any_instance_of(Gollum::File).to receive(:mime_type).and_return('image/jpeg')
+ allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
+ allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
expect(page).to have_xpath('//img[@data-src="image.jpg"]')
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
@@ -133,7 +138,7 @@ describe 'User views a wiki page' do
it 'opens a default wiki page', :js do
visit(project_path(project))
- find('.shortcuts-wiki').trigger('click')
+ find('.shortcuts-wiki').click
expect(page).to have_content('Home · Create Page')
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 3b01ed442bf..63e6051b571 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -13,8 +13,8 @@ feature 'Project' do
end
it "allows creation from templates", :js do
- find('#create-from-template-tab').trigger('click')
- find("##{template.name}").trigger('click')
+ find('#create-from-template-tab').click
+ find("label[for=#{template.name}]").click
fill_in("project_path", with: template.name)
page.within '#content-body' do
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 2ab1eda90f1..a4084818284 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -48,7 +48,7 @@ feature 'Protected Branches', :js do
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
- page.find('[data-target="#modal-delete-branch"]').trigger(:click)
+ page.find('[data-target="#modal-delete-branch"]').click
expect(page).to have_css('.js-delete-branch[disabled]')
fill_in 'delete_branch_input', with: 'fix'
@@ -67,9 +67,9 @@ feature 'Protected Branches', :js do
form = '.js-new-protected-branch'
within form do
- find(".js-allowed-to-merge").trigger('click')
+ find(".js-allowed-to-merge").click
click_link 'No one'
- find(".js-allowed-to-push").trigger('click')
+ find(".js-allowed-to-push").click
click_link 'Developers + Masters'
end
@@ -171,7 +171,7 @@ feature 'Protected Branches', :js do
end
def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").trigger('click')
+ find(".js-protected-branch-select").click
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index b1f51959d54..74890c86047 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'RavenJS', :js do
+feature 'RavenJS' do
let(:raven_path) { '/raven.bundle.js' }
it 'should not load raven if sentry is disabled' do
@@ -18,6 +18,8 @@ feature 'RavenJS', :js do
end
def has_requested_raven
- page.driver.network_traffic.one? {|request| request.url.end_with?(raven_path)}
+ page.all('script', visible: false).one? do |elm|
+ elm[:src] =~ /#{raven_path}$/
+ end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 0ed797a62ea..77212fb105b 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -32,14 +32,14 @@ describe 'User searches for code' do
include_examples 'top right search form'
it 'finds code' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: 'rspec')
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.results') do
expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index 630a81b1c5e..ef9553f2a91 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -18,7 +18,7 @@ describe 'User searches for issues', :js do
it 'finds an issue' do
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
@@ -31,14 +31,14 @@ describe 'User searches for issues', :js do
context 'when on a project page' do
it 'finds an issue' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
@@ -62,7 +62,7 @@ describe 'User searches for issues', :js do
it 'finds an issue' do
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 116256682f4..3b6739aecbd 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -17,7 +17,7 @@ describe 'User searches for merge requests', :js do
it 'finds a merge request' do
fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Merge requests')
@@ -30,14 +30,14 @@ describe 'User searches for merge requests', :js do
context 'when on a project page' do
it 'finds a merge request' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Merge requests')
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 4fa9fe9ce8c..6e197aee498 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -17,7 +17,7 @@ describe 'User searches for milestones', :js do
it 'finds a milestone' do
fill_in('dashboard_search', with: milestone1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Milestones')
@@ -30,14 +30,14 @@ describe 'User searches for milestones', :js do
context 'when on a project page' do
it 'finds a milestone' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: milestone1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Milestones')
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 1ea56479ecc..00af625dc86 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -15,14 +15,14 @@ describe 'User searches for wiki pages', :js do
include_examples 'top right search form'
it 'finds a page' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: 'content')
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Wiki')
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index 95f3eb5e805..aa883c964d2 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -16,7 +16,7 @@ describe 'User uses search filters', :js do
context' when filtering by group' do
it 'shows group projects' do
- find('.js-search-group-dropdown').trigger('click')
+ find('.js-search-group-dropdown').click
wait_for_requests
@@ -27,7 +27,7 @@ describe 'User uses search filters', :js do
expect(find('.js-search-group-dropdown')).to have_content(group.name)
page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
wait_for_requests
@@ -39,7 +39,7 @@ describe 'User uses search filters', :js do
context' when filtering by project' do
it 'shows a project' do
page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
wait_for_requests
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index bf79974b8c6..269351e55c9 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -74,24 +74,21 @@ describe 'Comments on personal snippets', :js do
it 'should not have autocomplete' do
wait_for_requests
- request_count_before = page.driver.network_traffic.count
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
wait_for_requests
- request_count_after = page.driver.network_traffic.count
# This selector probably won't be in place even if autocomplete was enabled
# but we want to make sure
expect(page).not_to have_selector('.atwho-view')
- expect(request_count_before).to eq(request_count_after)
end
end
context 'when editing a note' do
it 'changes the text' do
- find('.js-note-edit').trigger('click')
+ find('.js-note-edit').click
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'new content'
@@ -113,7 +110,7 @@ describe 'Comments on personal snippets', :js do
open_more_actions_dropdown(snippet_notes[0])
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
- click_on 'Delete comment'
+ accept_confirm { click_on 'Delete comment' }
end
wait_for_requests
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index d732383a1e1..941765b7578 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -14,7 +14,7 @@ feature 'User creates snippet', :js do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- find('.ace_editor').native.send_keys 'Hello World!'
+ find('.ace_text-input', visible: false).send_keys 'Hello World!'
end
end
@@ -43,8 +43,8 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
end
@@ -61,8 +61,8 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
scenario 'validation fails for the first time' do
@@ -86,15 +86,15 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
scenario 'Authenticated user creates a snippet with + in filename' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
- find('.ace_editor').native.send_keys 'Hello World!'
+ find('.ace_text-input', visible: false).send_keys 'Hello World!'
end
click_button 'Create snippet'
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 1455345bd56..1f8bd8d681e 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -63,7 +63,7 @@ feature 'Master creates tag' do
expect(ref_input.value).to eq 'master'
expect(find('.dropdown-toggle-text')).to have_content 'master'
- find('.js-branch-select').trigger('click')
+ find('.js-branch-select').click
expect(find('.dropdown-menu')).to have_content 'empty-branch'
end
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index f5b3774122b..dfda664d673 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -64,7 +64,7 @@ feature 'Master deletes tag' do
def delete_first_tag
page.within('.content') do
- first('.btn-remove').click
+ accept_confirm { first('.btn-remove').click }
end
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 548d8372a07..bc472e74997 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -45,7 +45,7 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
# See if edit page has correct descrption
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
expect(page.find('#trigger_description').value).to have_content 'trigger desc'
end
@@ -54,7 +54,7 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
# See if edit page opens, then fill in new description and save
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
fill_in 'trigger_description', with: new_trigger_title
click_button 'Save trigger'
@@ -70,7 +70,7 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
# See if the trigger can be edited and description is blank
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
expect(page.find('#trigger_description').value).to have_content ''
# See if trigger can be updated with description and saved successfully
@@ -94,12 +94,13 @@ feature 'Triggers', :js do
scenario 'take trigger ownership' do
# See if "Take ownership" on trigger works post trigger creation
- find('a.btn-trigger-take-ownership').click
page.accept_confirm do
- expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
- expect(page.find('.triggers-list')).to have_content trigger_title
- expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
+ first(:link, "Take ownership").send_keys(:return)
end
+
+ expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
+ expect(page.find('.triggers-list')).to have_content trigger_title
+ expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
end
@@ -116,11 +117,12 @@ feature 'Triggers', :js do
scenario 'revoke trigger' do
# See if "Revoke" on trigger works post trigger creation
- find('a.btn-trigger-revoke').click
page.accept_confirm do
- expect(page.find('.flash-notice')).to have_content 'Trigger removed'
- expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
+ find('a.btn-trigger-revoke').send_keys(:return)
end
+
+ expect(page.find('.flash-notice')).to have_content 'Trigger removed'
+ expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index f3662cb184f..c9afef2a8de 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -79,7 +79,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
first_u2f_device = register_u2f_device
second_u2f_device = register_u2f_device(name: 'My other device')
- click_on "Delete", match: :first
+ accept_confirm { click_on "Delete", match: :first }
expect(page).to have_content('Successfully deleted')
expect(page.body).not_to match(first_u2f_device.name)
@@ -162,7 +162,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
end
@@ -174,23 +173,10 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
end
- it 'persists remember_me value via hidden field' do
- gitlab_sign_in(user, remember: true)
-
- @u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
-
- within 'div#js-authenticate-u2f' do
- field = first('input#user_remember_me', visible: false)
- expect(field.value).to eq '1'
- end
- end
-
describe "when a given U2F device has already been registered by another user" do
describe "but not the current user" do
it "does not allow logging in with that particular device" do
@@ -205,7 +191,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
# Try authenticating user with the old U2F device
gitlab_sign_in(current_user)
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_content('Authentication via U2F device failed')
end
end
@@ -223,7 +208,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
# Try authenticating user with the same U2F device
gitlab_sign_in(current_user)
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
@@ -235,7 +219,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
unregistered_device = FakeU2fDevice.new(page, 'My device')
gitlab_sign_in(user)
unregistered_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_content('Authentication via U2F device failed')
end
@@ -260,7 +243,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
[first_device, second_device].each do |device|
gitlab_sign_in(user)
device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
@@ -283,7 +265,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
it "deletes u2f registrations" do
visit profile_account_path
- expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
+ expect do
+ accept_confirm { click_on "Disable" }
+ end.to change { U2fRegistration.count }.by(-1)
end
end
end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index 1261ffdc2ee..972c10aaf23 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -21,16 +21,12 @@ feature 'User uploads file to note' do
end
context 'uploading is in progress' do
- it 'shows "Cancel" button on uploading', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
-
- expect(page).to have_button('Cancel')
- end
-
it 'cancels uploading on clicking to "Cancel" button', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- click_button 'Cancel'
+ click_button 'Cancel'
+ end
expect(page).to have_button('Attach a file')
expect(page).not_to have_button('Cancel')
@@ -38,16 +34,20 @@ feature 'User uploads file to note' do
end
it 'shows "Attaching a file" message on uploading 1 file', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ end
end
it 'shows "Attaching 2 files" message on uploading 2 file', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
- Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
+ Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ end
end
it 'shows error message, "retry" and "attach a new file" link a if file is too big', :js do
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 0252c957c95..a9973cdf214 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -24,6 +24,7 @@ feature 'Users', :js do
user.reload
expect(user.reset_password_token).not_to be_nil
+ find('a[href="#login-pane"]').click
gitlab_sign_in(user)
expect(current_path).to eq root_path
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index 5d8e818f7bf..c78f7d0d9be 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -82,7 +82,7 @@ describe 'Project variables', :js do
it 'deletes variable' do
page.within('.variables-table') do
- click_on 'Remove'
+ accept_confirm { click_on 'Remove' }
end
expect(page).not_to have_selector('variables-table')
diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb
index 684af74d750..dcf9111776e 100644
--- a/spec/finders/autocomplete_users_finder_spec.rb
+++ b/spec/finders/autocomplete_users_finder_spec.rb
@@ -42,6 +42,21 @@ describe AutocompleteUsersFinder do
it { is_expected.to match_array([user1]) }
end
+ context 'when passed a subgroup', :nested_groups do
+ let(:grandparent) { create(:group, :public) }
+ let(:parent) { create(:group, :public, parent: grandparent) }
+ let(:child) { create(:group, :public, parent: parent) }
+ let(:group) { parent }
+
+ let!(:grandparent_user) { create(:group_member, :developer, group: grandparent).user }
+ let!(:parent_user) { create(:group_member, :developer, group: parent).user }
+ let!(:child_user) { create(:group_member, :developer, group: child).user }
+
+ it 'includes users from parent groups as well' do
+ expect(subject).to match_array([grandparent_user, parent_user])
+ end
+ end
+
it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
context 'when filtered by search' do
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 1f255a17881..489d563be2b 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -1,11 +1,38 @@
{
"type": "object",
"required" : [
- "status"
+ "status",
+ "applications"
],
"properties" : {
"status": { "type": "string" },
- "status_reason": { "type": ["string", "null"] }
+ "status_reason": { "type": ["string", "null"] },
+ "applications": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/application_status" }
+ }
},
- "additionalProperties": false
+ "additionalProperties": false,
+ "definitions": {
+ "application_status": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties" : {
+ "name": { "type": "string" },
+ "status": {
+ "type": {
+ "enum": [
+ "installable",
+ "scheduled",
+ "installing",
+ "installed",
+ "errored"
+ ]
+ }
+ },
+ "status_reason": { "type": ["string", "null"] }
+ },
+ "required" : [ "name", "status" ]
+ }
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
new file mode 100644
index 00000000000..3d3329a3406
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -0,0 +1,44 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "author_id": { "type": "integer" },
+ "description": { "type": ["string", "null"] },
+ "lock_version": { "type": ["string", "null"] },
+ "milestone_id": { "type": ["string", "null"] },
+ "title": { "type": "string" },
+ "moved_to_id": { "type": ["integer", "null"] },
+ "project_id": { "type": "integer" },
+ "web_url": { "type": "string" },
+ "state": { "type": "string" },
+ "create_note_path": { "type": "string" },
+ "preview_note_path": { "type": "string" },
+ "current_user": {
+ "type": "object",
+ "properties": {
+ "can_create_note": { "type": "boolean" },
+ "can_update": { "type": "boolean" }
+ }
+ },
+ "created_at": { "type": "date-time" },
+ "updated_at": { "type": "date-time" },
+ "branch_name": { "type": ["string", "null"] },
+ "due_date": { "type": "date" },
+ "confidential": { "type": "boolean" },
+ "discussion_locked": { "type": ["boolean", "null"] },
+ "updated_by_id": { "type": ["string", "null"] },
+ "deleted_at": { "type": ["string", "null"] },
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["integer", "null"] },
+ "human_total_time_spent": { "type": ["integer", "null"] },
+ "milestone": { "type": ["object", "null"] },
+ "labels": {
+ "type": "array",
+ "items": { "$ref": "label.json" }
+ },
+ "assignees": { "type": ["array", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/issue_sidebar.json b/spec/fixtures/api/schemas/entities/issue_sidebar.json
new file mode 100644
index 00000000000..682e345d5f5
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue_sidebar.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "subscribed": { "type": "boolean" },
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["integer", "null"] },
+ "human_total_time_spent": { "type": ["integer", "null"] },
+ "participants": {
+ "type": "array",
+ "items": { "$ref": "../public_api/v4/user/basic.json" }
+ },
+ "assignees": {
+ "type": "array",
+ "items": { "$ref": "../public_api/v4/user/basic.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/label.json b/spec/fixtures/api/schemas/entities/label.json
new file mode 100644
index 00000000000..40dff764c17
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/label.json
@@ -0,0 +1,26 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "type": { "type": "string" },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 6b14188582a..995f13381ad 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -9,7 +9,9 @@
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
- "assignee_id": { "type": ["integer", "null"] }
+ "assignee_id": { "type": ["integer", "null"] },
+ "subscribed": { "type": ["boolean", "null"] },
+ "participants": { "type": "array" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index e1f62508933..b579e32c9aa 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -13,38 +13,15 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
+ "issue_sidebar_endpoint": { "type": "string" },
+ "toggle_subscription_endpoint": { "type": "string" },
"project": {
"id": { "type": "integer" },
"path": { "type": "string" }
},
"labels": {
"type": "array",
- "items": {
- "type": "object",
- "required": [
- "id",
- "color",
- "description",
- "title",
- "priority"
- ],
- "properties": {
- "id": { "type": "integer" },
- "color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
- },
- "description": { "type": ["string", "null"] },
- "text_color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
- },
- "type": { "type": "string" },
- "title": { "type": "string" },
- "priority": { "type": ["integer", "null"] }
- },
- "additionalProperties": false
- }
+ "items": { "$ref": "entities/label.json" }
},
"assignee": {
"id": { "type": "integet" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index 5828be5255b..034509091a5 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -70,6 +70,7 @@
"sha": { "type": "string" },
"merge_commit_sha": { "type": ["string", "null"] },
"user_notes_count": { "type": "integer" },
+ "changes_count": { "type": "string" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
"discussion_locked": { "type": ["boolean", "null"] },
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
new file mode 100644
index 00000000000..4ba6422406c
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
@@ -0,0 +1,18 @@
+{
+ "type": "object",
+ "properties": {
+ "domain": { "type": "string" },
+ "url": { "type": "uri" },
+ "certificate_expiration": {
+ "type": "object",
+ "properties": {
+ "expired": { "type": "boolean" },
+ "expiration": { "type": "string" }
+ },
+ "required": ["expired", "expiration"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["domain", "url"],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
new file mode 100644
index 00000000000..08db8d47050
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "properties": {
+ "domain": { "type": "string" },
+ "url": { "type": "uri" },
+ "certificate": {
+ "type": "object",
+ "properties": {
+ "subject": { "type": "string" },
+ "expired": { "type": "boolean" },
+ "certificate": { "type": "string" },
+ "certificate_text": { "type": "string" }
+ },
+ "required": ["subject", "expired"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["domain", "url"],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json
new file mode 100644
index 00000000000..c7d86de7d8e
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "pages_domain/basic.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
index 0de1d0f1228..7c27218dc5a 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
@@ -1,23 +1,4 @@
{
"type": "array",
- "items": {
- "type": "object",
- "properties": {
- "domain": { "type": "string" },
- "url": { "type": "uri" },
- "certificate": {
- "type": "object",
- "properties": {
- "subject": { "type": "string" },
- "expired": { "type": "boolean" },
- "certificate": { "type": "string" },
- "certificate_text": { "type": "string" }
- },
- "required": ["subject", "expired"],
- "additionalProperties": false
- }
- },
- "required": ["domain", "url"],
- "additionalProperties": false
- }
+ "items": { "$ref": "pages_domain/detail.json" }
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
index e6c1d9c9d84..aa066883c47 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/login.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
@@ -27,11 +27,9 @@
"can_create_group",
"can_create_project",
"two_factor_enabled",
- "external",
- "private_token"
+ "external"
],
"properties": {
- "$ref": "full.json",
- "private_token": { "type": "string" }
+ "$ref": "full.json"
}
}
diff --git a/spec/fixtures/clusters/sample_cert.pem b/spec/fixtures/clusters/sample_cert.pem
new file mode 100644
index 00000000000..e39a2b34416
--- /dev/null
+++ b/spec/fixtures/clusters/sample_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAOutg3Kf2y5dMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTcxMDI5MTgxOTU3WhcNMTgxMDI5MTgxOTU3WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAvQysroM3TLxaavadSPnFIltrYnxCnU4PvCR8971HMWXsq7Z4ShU4BbbE
+8yp7oUFjulSwW6DhdIvnQb8ihLKictLmrA0isQqrD/iNpKZ6/lI4DGWw4QzrvMnW
+V4yy2QZNpg9tzQHd4+xkeeIoG23RijDU/sPd5dqxF+rPHBfCVInmYvSzLvMhneNj
+Bt6gV02gU9e9hsnMatsDvEbvWKp7wcbPot0nWrfZulx2QAWyXy+zG9mJQUds6yc0
+4agAeT9JEb/xtRgR/kS0aUHSGnfSnhZiEn17s0PhTmbu7qSHgzgB+7oJrC9jPoUh
+S2Wo3n0xykAjHrA8wC/Ddw3L38S41VQ58GEfNchistPswyMmXo/Oenv9P3s/kCOI
+fndiksFNdqVo51y9Vjngj589hpOseFDyKmWPIEQZ9kxW/crjP6RZWWLHgz26KtxZ
+uJaoYL8VBbYfrk/bucw0Ma2GEOp8rTsBE7SvgejXZa78q+381Kzc/utW6VwSXqzY
+xeIitft0rXi17SZ+XoiTkIXtHn0ZwMtOXNDBADTpFmKa6wVACQilvcpOYD8gUHyH
+pB+EDRdST3M4Fiq1MBAVhk8Lj3tHSJ/1ymeF1PWSu57AnJlzerzq2fcfPotNNd37
+ZPNkPh0kxPLwxbAyrHflzx9qVVdI1irY9055mNSnhzlec4qJ9cECAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUnVa5dYPoIG/3+qXml0bX8+N16GwwdQYDVR0jBG4wbIAUnVa5
+dYPoIG/3+qXml0bX8+N16GyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDr
+rYNyn9suXTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAUg4cyxXi1
+VR8ejTpaAruRyJ1pEG9Kc3kiIRXODy60z3hJXnx9LkScPkWGiuL5XacfZ2rMd4bw
+oVXIyi8U1UHWfAH8EZdrFKkU92jCiL5soHUONxLAvQEJ/FTR/qijrpzLCxXBdVQE
+xFEDWUu6rxLFyjEwzwnRTLgpjR606fdb7qXHkuAMvZ/ezJj8j97hok3Odpn4lr2H
+6hMTpK7HmDBX+kmdJJ+yBrm9hG1Pzpl7QU0dkxZ+qJNFjYMLnziiTwkv0c5ZaA9E
+NykZUcOv3Sjb6spu1A/E2BSq4WTjkIjrogFlfimE1vmUmObTRJOqUB0Vky1kHEwN
+pg7QqIJQmof1EAIaSM/YpUWXyumBwGLDUEud1JUz05In9Q4IZjEwZSJwbQW4fUia
+A93m9rk3Lw3xsFcaUdPMFIXk0rPoF1IgmV/oqb0gK95lOWRLbN+AV8qpKPpcKXOc
+TkIdFE47ZisEDhIdF6wC1izEMLeMEsPAO7/Y6MY4nRxsinSe95lRaw+yQpzx+mvJ
+Q7n1kiHI9Pd5M3+CiQda0d/GO1o5ORJnUGJRvr9HKuNmE7Lif0As/N0AlywjzE7A
+6Z8AEiWyRV1ffshu1k2UKmzvZuZeGGKRtrIjbJIRAtpRVtVZZGzhq5/sojCLoJ+u
+texqFBUo/4mFRZa4pDItUdyOlDy2/LO/ag==
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 4f46e40ce7a..638cd8b07c8 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -268,3 +268,37 @@ However the wrapping tags can not be mixed as such -
### Videos
![My Video](/assets/videos/gitlab-demo.mp4)
+
+### Mermaid
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid
+
+It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid].
+
+In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block.
+
+Example:
+
+ ```mermaid
+ graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+ ```
+
+Becomes:
+
+```mermaid
+graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+```
+
+For details see the [Mermaid official page][mermaid].
+
+[mermaid]: https://mermaidjs.github.io/ "Mermaid website"
+
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 7a241b02d28..5c5d53877a6 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -4,8 +4,6 @@ require 'spec_helper'
describe ApplicationHelper do
include UploadHelpers
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
-
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -57,30 +55,11 @@ describe ApplicationHelper do
end
describe 'project_icon' do
- let(:asset_host) { 'http://assets' }
-
it 'returns an url for the avatar' do
project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
- avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
-
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
-
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- avatar_url = "#{asset_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
-
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
- end
-
- it 'gives uploaded icon when present' do
- project = create(:project)
- allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
-
- avatar_url = "#{gitlab_host}#{project_avatar_path(project)}"
expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end
end
@@ -91,40 +70,7 @@ describe ApplicationHelper do
context 'when there is a matching user' do
it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon(user.email).to_s)
- .to eq("/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
-
- context 'when an asset_host is set in the config' do
- let(:asset_host) { 'http://assets' }
-
- before do
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- end
-
- it 'returns an absolute URL on that asset host' do
- expect(helper.avatar_icon(user.email, only_path: false).to_s)
- .to eq("#{asset_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when only_path is set to false' do
- it 'returns an absolute URL for the avatar' do
- expect(helper.avatar_icon(user.email, only_path: false).to_s)
- .to eq("#{gitlab_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when the GitLab instance is at a relative URL' do
- before do
- stub_config_setting(relative_url_root: '/gitlab')
- # Must be stubbed after the stub above, and separately
- stub_config_setting(url: Settings.send(:build_gitlab_url))
- end
-
- it 'returns a relative URL with the correct prefix' do
- expect(helper.avatar_icon(user.email).to_s)
- .to eq("/gitlab/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
+ .to eq(user.avatar.url)
end
end
@@ -138,18 +84,9 @@ describe ApplicationHelper do
end
describe 'using a user' do
- context 'when only_path is true' do
- it 'returns a relative URL for the avatar' do
- expect(helper.avatar_icon(user, only_path: true).to_s)
- .to eq("/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when only_path is false' do
- it 'returns an absolute URL for the avatar' do
- expect(helper.avatar_icon(user, only_path: false).to_s)
- .to eq("#{gitlab_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
+ it 'returns a relative URL for the avatar' do
+ expect(helper.avatar_icon(user).to_s)
+ .to eq(user.avatar.url)
end
end
end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 6a3945c0ebc..bc2422aba90 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -8,17 +8,13 @@ describe CiStatusHelper do
describe '#ci_icon_for_status' do
it 'renders to correct svg on success' do
- expect(helper).to receive(:render)
- .with('shared/icons/icon_status_success.svg', anything)
-
- helper.ci_icon_for_status(success_commit.status)
+ expect(helper.ci_icon_for_status('success').to_s)
+ .to include 'status_success'
end
it 'renders the correct svg on failure' do
- expect(helper).to receive(:render)
- .with('shared/icons/icon_status_failed.svg', anything)
-
- helper.ci_icon_for_status(failed_commit.status)
+ expect(helper.ci_icon_for_status('failed').to_s)
+ .to include 'status_failed'
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index d5536fcb22b..8a80b88da5d 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -1,96 +1,6 @@
require 'spec_helper'
describe EventsHelper do
- describe '#event_note' do
- let(:user) { build(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- it 'displays one line of plain text without alteration' do
- input = 'A short, plain note'
- expect(helper.event_note(input)).to match(input)
- expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
- end
-
- it 'displays inline code' do
- input = 'A note with `inline code`'
- expected = 'A note with <code>inline code</code>'
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'truncates a note with multiple paragraphs' do
- input = "Paragraph 1\n\nParagraph 2"
- expected = 'Paragraph 1...'
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'displays the first line of a code block' do
- input = "```\nCode block\nwith two lines\n```"
- expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'truncates a single long line of text' do
- text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
- input = text * 4
- expected = (text * 2).sub(/.{3}/, '...')
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'preserves a link href when link text is truncated' do
- text = 'The quick brown fox jumped over the lazy dog' # 44 chars
- input = "#{text}#{text}#{text} " # 133 chars
- link_url = 'http://example.com/foo/bar/baz' # 30 chars
- input << link_url
- expected_link_text = 'http://example...</a>'
-
- expect(helper.event_note(input)).to match(link_url)
- expect(helper.event_note(input)).to match(expected_link_text)
- end
-
- it 'preserves code color scheme' do
- input = "```ruby\ndef test\n 'hello world'\nend\n```"
- expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
- "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
- "</code></pre>"
- expect(helper.event_note(input)).to eq(expected)
- end
-
- it 'preserves data-src for lazy images' do
- input = "![ImageTest](/uploads/test.png)"
- image_url = "data-src=\"/uploads/test.png\""
- expect(helper.event_note(input)).to match(image_url)
- end
-
- context 'labels formatting' do
- let(:input) { 'this should be ~label_1' }
-
- def format_event_note(project)
- create(:label, title: 'label_1', project: project)
-
- helper.event_note(input, { project: project })
- end
-
- it 'preserves style attribute for a label that can be accessed by current_user' do
- project = create(:project, :public)
-
- expect(format_event_note(project)).to match(/span class=.*style=.*/)
- end
-
- it 'does not style a label that can not be accessed by current_user' do
- project = create(:project, :private)
-
- expect(format_event_note(project)).to eq("<p>#{input}</p>")
- end
- end
- end
-
describe '#event_commit_title' do
let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
subject { helper.event_commit_title(message) }
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index a44b200c5da..6c4f7050ee0 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -63,4 +63,30 @@ describe GitlabRoutingHelper do
it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end
end
+
+ describe '#preview_markdown_path' do
+ let(:project) { create(:project) }
+
+ it 'returns group preview markdown path for a group parent' do
+ group = create(:group)
+
+ expect(preview_markdown_path(group)).to eq("/groups/#{group.path}/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project parent' do
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+
+ it 'returns snippet preview markdown path for a personal snippet' do
+ @snippet = create(:personal_snippet)
+
+ expect(preview_markdown_path(nil)).to eq("/snippets/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project snippet' do
+ @snippet = create(:project_snippet, project: project)
+
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+ end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 97f0ed4904e..32432ee1e81 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -3,8 +3,6 @@ require 'spec_helper'
describe GroupsHelper do
include ApplicationHelper
- let(:asset_host) { 'http://assets' }
-
describe 'group_icon' do
avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
@@ -13,16 +11,8 @@ describe GroupsHelper do
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
- avatar_url = "/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
-
- expect(helper.group_icon(group).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
-
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- avatar_url = "#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
-
expect(helper.group_icon(group).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ .to eq "<img data-src=\"#{group.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end
end
@@ -34,25 +24,7 @@ describe GroupsHelper do
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
expect(group_icon_url(group.path).to_s)
- .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
- end
-
- it 'returns an CDN url for the avatar' do
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- group = create(:group)
- group.avatar = fixture_file_upload(avatar_file_path)
- group.save!
- expect(group_icon_url(group.path).to_s)
- .to match("#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
- end
-
- it 'returns an based url for the avatar if private' do
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- group = create(:group, :private)
- group.avatar = fixture_file_upload(avatar_file_path)
- group.save!
- expect(group_icon_url(group.path).to_s)
- .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
+ .to match(group.avatar.url)
end
it 'gives default avatar_icon when no avatar is present' do
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 3d79dac284f..2f23ed55d99 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe IconsHelper do
+ let(:icons_path) { ActionController::Base.helpers.image_path("icons.svg") }
+
describe 'icon' do
it 'returns aria-hidden by default' do
star = icon('star')
@@ -16,22 +18,42 @@ describe IconsHelper do
end
end
+ describe 'sprite_icon_path' do
+ it 'returns relative path' do
+ expect(sprite_icon_path)
+ .to eq icons_path
+ end
+
+ context 'when an asset_host is set in the config it will return an absolute local URL' do
+ let(:asset_host) { 'http://assets' }
+
+ before do
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ end
+
+ it 'returns an absolute URL on that asset host' do
+ expect(sprite_icon_path)
+ .to eq ActionController::Base.helpers.image_path("icons.svg", host: Gitlab.config.gitlab.url)
+ end
+ end
+ end
+
describe 'sprite_icon' do
icon_name = 'clock'
it 'returns svg icon html' do
expect(sprite_icon(icon_name).to_s)
- .to eq "<svg><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes' do
expect(sprite_icon(icon_name, size: 72).to_s)
- .to eq "<svg class=\"s72\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes + additional class' do
expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
- .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index ead3e28438e..cb851d828f2 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -159,4 +159,36 @@ describe IssuablesHelper do
end
end
end
+
+ describe '#issuable_initial_data' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ it 'returns the correct json for an issue' do
+ issue = create(:issue, author: user, description: 'issue text')
+ @project = issue.project
+
+ expected_data = {
+ 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}",
+ 'canUpdate' => true,
+ 'canDestroy' => true,
+ 'issuableRef' => "##{issue.iid}",
+ 'markdownPreviewPath' => "/#{@project.full_path}/preview_markdown",
+ 'markdownDocsPath' => '/help/user/markdown',
+ 'issuableTemplates' => [],
+ 'projectPath' => @project.path,
+ 'projectNamespace' => @project.namespace.path,
+ 'initialTitleHtml' => issue.title,
+ 'initialTitleText' => issue.title,
+ 'initialDescriptionHtml' => '<p dir="auto">issue text</p>',
+ 'initialDescriptionText' => 'issue text',
+ 'initialTaskStatus' => '0 of 0 tasks completed'
+ }
+ expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data)
+ end
+ end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 36d6e495ed0..4ac4302adfd 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -24,7 +24,7 @@ describe LabelsHelper do
let(:group) { build(:group, name: 'bar') }
it 'links to group issues page' do
- expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 03d706062b7..62ea6d48542 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -67,7 +67,7 @@ describe MarkupHelper do
describe 'without redacted attribute' do
it 'renders the markdown value' do
- expect(Banzai).to receive(:render_field).with(commit, attribute).and_call_original
+ expect(Banzai).to receive(:render_field).with(commit, attribute, {}).and_call_original
helper.markdown_field(commit, attribute)
end
@@ -252,38 +252,141 @@ describe MarkupHelper do
end
describe '#first_line_in_markdown' do
- it 'truncates Markdown properly' do
- text = "@#{user.username}, can you look at this?\nHello world\n"
- actual = first_line_in_markdown(text, 100, project: project)
+ shared_examples_for 'common markdown examples' do
+ let(:project_base) { build(:project, :repository) }
- doc = Nokogiri::HTML.parse(actual)
+ it 'displays inline code' do
+ object = create_object('Text with `inline code`')
+ expected = 'Text with <code>inline code</code>'
- # Make sure we didn't create invalid markup
- expect(doc.errors).to be_empty
+ expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
+ end
- # Leading user link
- expect(doc.css('a').length).to eq(1)
- expect(doc.css('a')[0].attr('href')).to eq user_path(user)
- expect(doc.css('a')[0].text).to eq "@#{user.username}"
+ it 'truncates the text with multiple paragraphs' do
+ object = create_object("Paragraph 1\n\nParagraph 2")
+ expected = 'Paragraph 1...'
- expect(doc.content).to eq "@#{user.username}, can you look at this?..."
- end
+ expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
+ end
- it 'truncates Markdown with emoji properly' do
- text = "foo :wink:\nbar :grinning:"
- actual = first_line_in_markdown(text, 100, project: project)
+ it 'displays the first line of a code block' do
+ object = create_object("```\nCode block\nwith two lines\n```")
+ expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
- doc = Nokogiri::HTML.parse(actual)
+ expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
+ end
- # Make sure we didn't create invalid markup
- # But also account for the 2 errors caused by the unknown `gl-emoji` elements
- expect(doc.errors.length).to eq(2)
+ it 'truncates a single long line of text' do
+ text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
+ object = create_object(text * 4)
+ expected = (text * 2).sub(/.{3}/, '...')
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected)
+ end
+
+ it 'preserves a link href when link text is truncated' do
+ text = 'The quick brown fox jumped over the lazy dog' # 44 chars
+ input = "#{text}#{text}#{text} " # 133 chars
+ link_url = 'http://example.com/foo/bar/baz' # 30 chars
+ input << link_url
+ object = create_object(input)
+ expected_link_text = 'http://example...</a>'
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(link_url)
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected_link_text)
+ end
+
+ it 'preserves code color scheme' do
+ object = create_object("```ruby\ndef test\n 'hello world'\nend\n```")
+ expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
+ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
+ "</code></pre>"
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to eq(expected)
+ end
+
+ it 'preserves data-src for lazy images' do
+ object = create_object("![ImageTest](/uploads/test.png)")
+ image_url = "data-src=\".*/uploads/test.png\""
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(image_url)
+ end
+
+ context 'labels formatting' do
+ let(:label_title) { 'this should be ~label_1' }
+
+ def create_and_format_label(project)
+ create(:label, title: 'label_1', project: project)
+ object = create_object(label_title, project: project)
- expect(doc.css('gl-emoji').length).to eq(2)
- expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
- expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
+ first_line_in_markdown(object, attribute, 150, project: project)
+ end
- expect(doc.content).to eq "foo 😉\nbar 😀"
+ it 'preserves style attribute for a label that can be accessed by current_user' do
+ project = create(:project, :public)
+
+ expect(create_and_format_label(project)).to match(/span class=.*style=.*/)
+ end
+
+ it 'does not style a label that can not be accessed by current_user' do
+ project = create(:project, :private)
+
+ expect(create_and_format_label(project)).to eq("<p>#{label_title}</p>")
+ end
+ end
+
+ it 'truncates Markdown properly' do
+ object = create_object("@#{user.username}, can you look at this?\nHello world\n")
+ actual = first_line_in_markdown(object, attribute, 100, project: project)
+
+ doc = Nokogiri::HTML.parse(actual)
+
+ # Make sure we didn't create invalid markup
+ expect(doc.errors).to be_empty
+
+ # Leading user link
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a')[0].attr('href')).to eq user_path(user)
+ expect(doc.css('a')[0].text).to eq "@#{user.username}"
+
+ expect(doc.content).to eq "@#{user.username}, can you look at this?..."
+ end
+
+ it 'truncates Markdown with emoji properly' do
+ object = create_object("foo :wink:\nbar :grinning:")
+ actual = first_line_in_markdown(object, attribute, 100, project: project)
+
+ doc = Nokogiri::HTML.parse(actual)
+
+ # Make sure we didn't create invalid markup
+ # But also account for the 2 errors caused by the unknown `gl-emoji` elements
+ expect(doc.errors.length).to eq(2)
+
+ expect(doc.css('gl-emoji').length).to eq(2)
+ expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
+ expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
+
+ expect(doc.content).to eq "foo 😉\nbar 😀"
+ end
+ end
+
+ context 'when the asked attribute can be redacted' do
+ include_examples 'common markdown examples' do
+ let(:attribute) { :note }
+ def create_object(title, project: project_base)
+ build(:note, note: title, project: project)
+ end
+ end
+ end
+
+ context 'when the asked attribute can not be redacted' do
+ include_examples 'common markdown examples' do
+ let(:attribute) { :body }
+ def create_object(title, project: project_base)
+ issue = build(:issue, title: title)
+ build(:todo, :done, project: project_base, author: user, target: issue)
+ end
+ end
end
end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 8365b3f5538..460d3b6a7e4 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -29,5 +29,30 @@ describe NamespacesHelper do
expect(options).not_to include(admin_group.name)
expect(options).to include(user_group.name)
end
+
+ context 'when nested groups are available', :nested_groups do
+ it 'includes groups nested in groups the user can administer' do
+ allow(helper).to receive(:current_user).and_return(user)
+ child_group = create(:group, :private, parent: user_group)
+
+ options = helper.namespaces_options
+
+ expect(options).to include(child_group.name)
+ end
+
+ it 'orders the groups correctly' do
+ allow(helper).to receive(:current_user).and_return(user)
+ child_group = create(:group, :private, parent: user_group)
+ other_child = create(:group, :private, parent: user_group)
+ sub_child = create(:group, :private, parent: child_group)
+
+ expect(helper).to receive(:options_for_group)
+ .with([user_group, child_group, sub_child, other_child], anything)
+ .and_call_original
+ allow(helper).to receive(:options_for_group).and_call_original
+
+ helper.namespaces_options
+ end
+ end
end
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index d7b66e6f078..c358ccae9c3 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -1,10 +1,36 @@
require 'spec_helper'
describe TreeHelper do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' }
+
+ describe '.render_tree' do
+ before do
+ @id = sha
+ @project = project
+ end
+
+ it 'displays all entries without a warning' do
+ tree = repository.tree(sha, 'files')
+
+ html = render_tree(tree)
+
+ expect(html).not_to have_selector('.tree-truncated-warning')
+ end
+
+ it 'truncates entries and adds a warning' do
+ stub_const('TreeHelper::FILE_LIMIT', 1)
+ tree = repository.tree(sha, 'files')
+
+ html = render_tree(tree)
+
+ expect(html).to have_selector('.tree-truncated-warning', count: 1)
+ expect(html).to have_selector('.tree-item-file-name', count: 1)
+ end
+ end
+
describe 'flatten_tree' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
- let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' }
let(:tree) { repository.tree(sha, 'files') }
let(:root_path) { 'files' }
let(:tree_item) { tree.entries.find { |entry| entry.path == path } }
diff --git a/spec/initializers/8_metrics_spec.rb b/spec/initializers/8_metrics_spec.rb
index 4e6052a9f80..80c77057065 100644
--- a/spec/initializers/8_metrics_spec.rb
+++ b/spec/initializers/8_metrics_spec.rb
@@ -3,7 +3,6 @@ require 'spec_helper'
describe 'instrument_classes' do
let(:config) { double(:config) }
- let(:unicorn_sampler) { double(:unicorn_sampler) }
let(:influx_sampler) { double(:influx_sampler) }
before do
@@ -11,9 +10,7 @@ describe 'instrument_classes' do
allow(config).to receive(:instrument_methods)
allow(config).to receive(:instrument_instance_method)
allow(config).to receive(:instrument_instance_methods)
- allow(Gitlab::Metrics::UnicornSampler).to receive(:initialize_instance).and_return(unicorn_sampler)
- allow(Gitlab::Metrics::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler)
- allow(unicorn_sampler).to receive(:start)
+ allow(Gitlab::Metrics::Samplers::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler)
allow(influx_sampler).to receive(:start)
allow(Gitlab::Application).to receive(:configure)
end
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
new file mode 100644
index 00000000000..b8155144e2a
--- /dev/null
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -0,0 +1,47 @@
+import { CopyAsGFM } from '~/behaviors/copy_as_gfm';
+
+describe('CopyAsGFM', () => {
+ describe('CopyAsGFM.pasteGFM', () => {
+ function callPasteGFM() {
+ const e = {
+ originalEvent: {
+ clipboardData: {
+ getData(mimeType) {
+ // When GFM code is copied, we put the regular plain text
+ // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
+ // This emulates the behavior of `getData` with that data.
+ if (mimeType === 'text/plain') {
+ return 'code';
+ }
+ if (mimeType === 'text/x-gfm') {
+ return '`code`';
+ }
+ return null;
+ },
+ },
+ },
+ preventDefault() {},
+ };
+
+ CopyAsGFM.pasteGFM(e);
+ }
+
+ it('wraps pasted code when not already in code tags', () => {
+ spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
+ const insertedText = textFunc('This is code: ', '');
+ expect(insertedText).toEqual('`code`');
+ });
+
+ callPasteGFM();
+ });
+
+ it('does not wrap pasted code when already in code tags', () => {
+ spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
+ const insertedText = textFunc('This is code: `', '`');
+ expect(insertedText).toEqual('code');
+ });
+
+ callPasteGFM();
+ });
+ });
+});
diff --git a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
index ec2c549e032..f96f20ed4a5 100644
--- a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
+++ b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
@@ -21,13 +21,18 @@ describe('Unicode Support Map', () => {
});
it('should call .getItem and .setItem', () => {
- const allArgs = window.localStorage.setItem.calls.allArgs();
-
- expect(window.localStorage.getItem).toHaveBeenCalledWith('gl-emoji-user-agent');
- expect(allArgs[0][0]).toBe('gl-emoji-user-agent');
- expect(allArgs[0][1]).toBe(navigator.userAgent);
- expect(allArgs[1][0]).toBe('gl-emoji-unicode-support-map');
- expect(allArgs[1][1]).toBe(stringSupportMap);
+ const getArgs = window.localStorage.getItem.calls.allArgs();
+ const setArgs = window.localStorage.setItem.calls.allArgs();
+
+ expect(getArgs[0][0]).toBe('gl-emoji-version');
+ expect(getArgs[1][0]).toBe('gl-emoji-user-agent');
+
+ expect(setArgs[0][0]).toBe('gl-emoji-version');
+ expect(setArgs[0][1]).toBe('0.2.0');
+ expect(setArgs[1][0]).toBe('gl-emoji-user-agent');
+ expect(setArgs[1][1]).toBe(navigator.userAgent);
+ expect(setArgs[2][0]).toBe('gl-emoji-unicode-support-map');
+ expect(setArgs[2][1]).toBe(stringSupportMap);
});
});
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 83b13b06dc1..8f607899b20 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -9,10 +9,11 @@
import Vue from 'vue';
import '~/boards/models/assignee';
+import eventHub from '~/boards/eventhub';
import '~/boards/models/list';
import '~/boards/models/label';
import '~/boards/stores/boards_store';
-import boardCard from '~/boards/components/board_card';
+import boardCard from '~/boards/components/board_card.vue';
import './mock_data';
describe('Board card', () => {
@@ -157,33 +158,35 @@ describe('Board card', () => {
});
it('sets detail issue to card issue on mouse up', () => {
+ spyOn(eventHub, '$emit');
+
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue);
expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
});
it('adds active class if detail issue is set', (done) => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
-
- setTimeout(() => {
- expect(vm.$el.classList.contains('is-active')).toBe(true);
- done();
- }, 0);
+ vm.detailIssue.issue = vm.issue;
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.classList.contains('is-active')).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('resets detail issue to empty if already set', () => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
+ spyOn(eventHub, '$emit');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ gl.issueBoards.BoardsStore.detail.issue = vm.issue;
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
});
});
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 022d286d5df..ccde657789a 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -133,6 +133,19 @@ describe('Issue model', () => {
expect(relativePositionIssue.position).toBe(1);
});
+ it('updates data', () => {
+ issue.updateData({ subscribed: true });
+ expect(issue.subscribed).toBe(true);
+ });
+
+ it('sets fetching state', () => {
+ expect(issue.isFetching.subscriptions).toBe(true);
+
+ issue.setFetchingState('subscriptions', false);
+
+ expect(issue.isFetching.subscriptions).toBe(false);
+ });
+
describe('update', () => {
it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
new file mode 100644
index 00000000000..027e8001053
--- /dev/null
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -0,0 +1,257 @@
+import Clusters from '~/clusters/clusters_bundle';
+import {
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_INSTALLED,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from '~/clusters/constants';
+import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+
+describe('Clusters', () => {
+ let cluster;
+ preloadFixtures('clusters/show_cluster.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('clusters/show_cluster.html.raw');
+ cluster = new Clusters();
+ });
+
+ afterEach(() => {
+ cluster.destroy();
+ });
+
+ describe('toggle', () => {
+ it('should update the button and the input field on click', () => {
+ cluster.toggleButton.click();
+
+ expect(
+ cluster.toggleButton.classList,
+ ).not.toContain('checked');
+
+ expect(
+ cluster.toggleInput.getAttribute('value'),
+ ).toEqual('false');
+ });
+ });
+
+ describe('checkForNewInstalls', () => {
+ const INITIAL_APP_MAP = {
+ helm: { status: null, title: 'Helm Tiller' },
+ ingress: { status: null, title: 'Ingress' },
+ runner: { status: null, title: 'GitLab Runner' },
+ };
+
+ it('does not show alert when things transition from initial null state to something', () => {
+ cluster.checkForNewInstalls(INITIAL_APP_MAP, {
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLABLE, title: 'Helm Tiller' },
+ });
+
+ expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeNull();
+ });
+
+ it('shows an alert when something gets newly installed', () => {
+ cluster.checkForNewInstalls({
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLING, title: 'Helm Tiller' },
+ }, {
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
+ });
+
+ expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
+ expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller was successfully installed on your cluster');
+ });
+
+ it('shows an alert when multiple things gets newly installed', () => {
+ cluster.checkForNewInstalls({
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLING, title: 'Helm Tiller' },
+ ingress: { status: APPLICATION_INSTALLABLE, title: 'Ingress' },
+ }, {
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
+ ingress: { status: APPLICATION_INSTALLED, title: 'Ingress' },
+ });
+
+ expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
+ expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your cluster');
+ });
+ });
+
+ describe('updateContainer', () => {
+ describe('when creating cluster', () => {
+ it('should show the creating container', () => {
+ cluster.updateContainer(null, 'creating');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+
+ it('should continue to show `creating` banner with subsequent updates of the same status', () => {
+ cluster.updateContainer('creating', 'creating');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster is created', () => {
+ it('should show the success container', () => {
+ cluster.updateContainer(null, 'created');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+
+ it('should not show a banner when status is already `created`', () => {
+ cluster.updateContainer('created', 'created');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster has error', () => {
+ it('should show the error container', () => {
+ cluster.updateContainer(null, 'errored', 'this is an error');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+
+ expect(
+ cluster.errorReasonContainer.textContent,
+ ).toContain('this is an error');
+ });
+
+ it('should show `error` banner when previously `creating`', () => {
+ cluster.updateContainer('creating', 'errored');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ });
+ });
+ });
+
+ describe('installApplication', () => {
+ it('tries to install helm', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+
+ cluster.installApplication('helm');
+
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('helm');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('tries to install ingress', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
+
+ cluster.installApplication('ingress');
+
+ expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('tries to install runner', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
+
+ cluster.installApplication('runner');
+
+ expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('runner');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets error request status when the request fails', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR')));
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+
+ cluster.installApplication('helm');
+
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalled();
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
+ expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js
new file mode 100644
index 00000000000..e671c18e1a5
--- /dev/null
+++ b/spec/javascripts/clusters/components/application_row_spec.js
@@ -0,0 +1,237 @@
+import Vue from 'vue';
+import eventHub from '~/clusters/event_hub';
+import {
+ APPLICATION_NOT_INSTALLABLE,
+ APPLICATION_SCHEDULED,
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_INSTALLED,
+ APPLICATION_ERROR,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from '~/clusters/constants';
+import applicationRow from '~/clusters/components/application_row.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
+
+describe('Application Row', () => {
+ let vm;
+ let ApplicationRow;
+
+ beforeEach(() => {
+ ApplicationRow = Vue.extend(applicationRow);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Title', () => {
+ it('shows title', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ titleLink: null,
+ });
+ const title = vm.$el.querySelector('.js-cluster-application-title');
+
+ expect(title.tagName).toEqual('SPAN');
+ expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title);
+ });
+
+ it('shows title link', () => {
+ expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined();
+
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ });
+ const title = vm.$el.querySelector('.js-cluster-application-title');
+
+ expect(title.tagName).toEqual('A');
+ expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title);
+ });
+ });
+
+ describe('Install button', () => {
+ it('has indeterminate state on page load', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: null,
+ });
+
+ expect(vm.installButtonLabel).toBeUndefined();
+ });
+
+ it('has disabled "Install" when APPLICATION_NOT_INSTALLABLE', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_NOT_INSTALLABLE,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has enabled "Install" when APPLICATION_INSTALLABLE', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(false);
+ });
+
+ it('has loading "Installing" when APPLICATION_SCHEDULED', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_SCHEDULED,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Installing');
+ expect(vm.installButtonLoading).toEqual(true);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has loading "Installing" when APPLICATION_INSTALLING', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLING,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Installing');
+ expect(vm.installButtonLoading).toEqual(true);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has disabled "Installed" when APPLICATION_INSTALLED', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLED,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Installed');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has enabled "Install" when APPLICATION_ERROR', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_ERROR,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(false);
+ });
+
+ it('has loading "Install" when REQUEST_LOADING', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_LOADING,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(true);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has disabled "Install" when REQUEST_SUCCESS', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_SUCCESS,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_FAILURE,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(false);
+ });
+
+ it('clicking install button emits event', () => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ });
+ const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+ installButton.click();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id);
+ });
+
+ it('clicking disabled install button emits nothing', () => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLING,
+ });
+ const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+ expect(vm.installButtonDisabled).toEqual(true);
+
+ installButton.click();
+
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Error block', () => {
+ it('does not show error block when there is no error', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: null,
+ requestStatus: null,
+ });
+ const generalErrorMessage = vm.$el.querySelector('.js-cluster-application-general-error-message');
+
+ expect(generalErrorMessage).toBeNull();
+ });
+
+ it('shows status reason when APPLICATION_ERROR', () => {
+ const statusReason = 'We broke it 0.0';
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_ERROR,
+ statusReason,
+ });
+ const generalErrorMessage = vm.$el.querySelector('.js-cluster-application-general-error-message');
+ const statusErrorMessage = vm.$el.querySelector('.js-cluster-application-status-error-message');
+
+ expect(generalErrorMessage.textContent.trim()).toEqual(`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`);
+ expect(statusErrorMessage.textContent.trim()).toEqual(statusReason);
+ });
+
+ it('shows request reason when REQUEST_FAILURE', () => {
+ const requestReason = 'We broke thre request 0.0';
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_FAILURE,
+ requestReason,
+ });
+ const generalErrorMessage = vm.$el.querySelector('.js-cluster-application-general-error-message');
+ const requestErrorMessage = vm.$el.querySelector('.js-cluster-application-request-error-message');
+
+ expect(generalErrorMessage.textContent.trim()).toEqual(`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`);
+ expect(requestErrorMessage.textContent.trim()).toEqual(requestReason);
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
new file mode 100644
index 00000000000..7460da031c4
--- /dev/null
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import applications from '~/clusters/components/applications.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Applications', () => {
+ let vm;
+ let Applications;
+
+ beforeEach(() => {
+ Applications = Vue.extend(applications);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ runner: { title: 'GitLab Runner' },
+ },
+ });
+ });
+
+ it('renders a row for Helm Tiller', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeDefined();
+ });
+
+ it('renders a row for Ingress', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined();
+ });
+
+ /* * /
+ it('renders a row for GitLab Runner', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
+ });
+ /* */
+ });
+});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
new file mode 100644
index 00000000000..af6b6a73819
--- /dev/null
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -0,0 +1,50 @@
+import {
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_ERROR,
+} from '~/clusters/constants';
+
+const CLUSTERS_MOCK_DATA = {
+ GET: {
+ '/gitlab-org/gitlab-shell/clusters/1/status.json': {
+ data: {
+ status: 'errored',
+ status_reason: 'Failed to request to CloudPlatform.',
+ applications: [{
+ name: 'helm',
+ status: APPLICATION_INSTALLABLE,
+ status_reason: null,
+ }, {
+ name: 'ingress',
+ status: APPLICATION_ERROR,
+ status_reason: 'Cannot connect',
+ }, {
+ name: 'runner',
+ status: APPLICATION_INSTALLING,
+ status_reason: null,
+ }],
+ },
+ },
+ },
+ POST: {
+ '/gitlab-org/gitlab-shell/clusters/1/applications/helm': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
+ },
+};
+
+const DEFAULT_APPLICATION_STATE = {
+ id: 'some-app',
+ title: 'My App',
+ titleLink: 'https://about.gitlab.com/',
+ description: 'Some description about this interesting application!',
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+};
+
+export {
+ CLUSTERS_MOCK_DATA,
+ DEFAULT_APPLICATION_STATE,
+};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
new file mode 100644
index 00000000000..cb8b3d38e2e
--- /dev/null
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -0,0 +1,89 @@
+import ClustersStore from '~/clusters/stores/clusters_store';
+import { APPLICATION_INSTALLING } from '~/clusters/constants';
+import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
+
+describe('Clusters Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new ClustersStore();
+ });
+
+ describe('updateStatus', () => {
+ it('should store new status', () => {
+ expect(store.state.status).toEqual(null);
+
+ const newStatus = 'errored';
+ store.updateStatus(newStatus);
+
+ expect(store.state.status).toEqual(newStatus);
+ });
+ });
+
+ describe('updateStatusReason', () => {
+ it('should store new reason', () => {
+ expect(store.state.statusReason).toEqual(null);
+
+ const newReason = 'Something went wrong!';
+ store.updateStatusReason(newReason);
+
+ expect(store.state.statusReason).toEqual(newReason);
+ });
+ });
+
+ describe('updateAppProperty', () => {
+ it('should store new request status', () => {
+ expect(store.state.applications.helm.requestStatus).toEqual(null);
+
+ const newStatus = APPLICATION_INSTALLING;
+ store.updateAppProperty('helm', 'requestStatus', newStatus);
+
+ expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
+ });
+
+ it('should store new request reason', () => {
+ expect(store.state.applications.helm.requestReason).toEqual(null);
+
+ const newReason = 'We broke it.';
+ store.updateAppProperty('helm', 'requestReason', newReason);
+
+ expect(store.state.applications.helm.requestReason).toEqual(newReason);
+ });
+ });
+
+ describe('updateStateFromServer', () => {
+ it('should store new polling data from server', () => {
+ const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/1/status.json'].data;
+ store.updateStateFromServer(mockResponseData);
+
+ expect(store.state).toEqual({
+ helpPath: null,
+ status: mockResponseData.status,
+ statusReason: mockResponseData.status_reason,
+ applications: {
+ helm: {
+ title: 'Helm Tiller',
+ status: mockResponseData.applications[0].status,
+ statusReason: mockResponseData.applications[0].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ },
+ ingress: {
+ title: 'Ingress',
+ status: mockResponseData.applications[1].status,
+ statusReason: mockResponseData.applications[1].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ },
+ runner: {
+ title: 'GitLab Runner',
+ status: mockResponseData.applications[2].status,
+ statusReason: mockResponseData.applications[2].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/clusters_spec.js b/spec/javascripts/clusters_spec.js
deleted file mode 100644
index eb1cd6eb804..00000000000
--- a/spec/javascripts/clusters_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Clusters from '~/clusters';
-
-describe('Clusters', () => {
- let cluster;
- preloadFixtures('clusters/show_cluster.html.raw');
-
- beforeEach(() => {
- loadFixtures('clusters/show_cluster.html.raw');
- cluster = new Clusters();
- });
-
- describe('toggle', () => {
- it('should update the button and the input field on click', () => {
- cluster.toggleButton.click();
-
- expect(
- cluster.toggleButton.classList,
- ).not.toContain('checked');
-
- expect(
- cluster.toggleInput.getAttribute('value'),
- ).toEqual('false');
- });
- });
-
- describe('updateContainer', () => {
- describe('when creating cluster', () => {
- it('should show the creating container', () => {
- cluster.updateContainer('creating');
-
- expect(
- cluster.creatingContainer.classList.contains('hidden'),
- ).toBeFalsy();
- expect(
- cluster.successContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.errorContainer.classList.contains('hidden'),
- ).toBeTruthy();
- });
- });
-
- describe('when cluster is created', () => {
- it('should show the success container', () => {
- cluster.updateContainer('created');
-
- expect(
- cluster.creatingContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.successContainer.classList.contains('hidden'),
- ).toBeFalsy();
- expect(
- cluster.errorContainer.classList.contains('hidden'),
- ).toBeTruthy();
- });
- });
-
- describe('when cluster has error', () => {
- it('should show the error container', () => {
- cluster.updateContainer('errored', 'this is an error');
-
- expect(
- cluster.creatingContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.successContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.errorContainer.classList.contains('hidden'),
- ).toBeFalsy();
-
- expect(
- cluster.errorReasonContainer.textContent,
- ).toContain('this is an error');
- });
- });
- });
-});
diff --git a/spec/javascripts/copy_as_gfm_spec.js b/spec/javascripts/copy_as_gfm_spec.js
deleted file mode 100644
index ded450749d3..00000000000
--- a/spec/javascripts/copy_as_gfm_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import '~/copy_as_gfm';
-
-(() => {
- describe('gl.CopyAsGFM', () => {
- describe('gl.CopyAsGFM.pasteGFM', () => {
- function callPasteGFM() {
- const e = {
- originalEvent: {
- clipboardData: {
- getData(mimeType) {
- // When GFM code is copied, we put the regular plain text
- // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
- // This emulates the behavior of `getData` with that data.
- if (mimeType === 'text/plain') {
- return 'code';
- }
- if (mimeType === 'text/x-gfm') {
- return '`code`';
- }
- return null;
- },
- },
- },
- preventDefault() {},
- };
-
- window.gl.CopyAsGFM.pasteGFM(e);
- }
-
- it('wraps pasted code when not already in code tags', () => {
- spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
- const insertedText = textFunc('This is code: ', '');
- expect(insertedText).toEqual('`code`');
- });
-
- callPasteGFM();
- });
-
- it('does not wrap pasted code when already in code tags', () => {
- spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
- const insertedText = textFunc('This is code: `', '`');
- expect(insertedText).toEqual('code');
- });
-
- callPasteGFM();
- });
- });
- });
-})();
diff --git a/spec/javascripts/emoji_spec.js b/spec/javascripts/emoji_spec.js
index fa11c602ec3..124d91f4477 100644
--- a/spec/javascripts/emoji_spec.js
+++ b/spec/javascripts/emoji_spec.js
@@ -1,6 +1,7 @@
import { glEmojiTag } from '~/emoji';
import isEmojiUnicodeSupported, {
isFlagEmoji,
+ isRainbowFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
@@ -217,6 +218,24 @@ describe('gl_emoji', () => {
});
});
+ describe('isRainbowFlagEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isRainbowFlagEmoji('')).toBeFalsy();
+ });
+ it('should detect rainbow_flag', () => {
+ expect(isRainbowFlagEmoji('ðŸ³ðŸŒˆ')).toBeTruthy();
+ });
+ it('should not detect flag_white on its\' own', () => {
+ expect(isRainbowFlagEmoji('ðŸ³')).toBeFalsy();
+ });
+ it('should not detect rainbow on its\' own', () => {
+ expect(isRainbowFlagEmoji('🌈')).toBeFalsy();
+ });
+ it('should not detect flag_white with something else', () => {
+ expect(isRainbowFlagEmoji('ðŸ³ðŸ”µ')).toBeFalsy();
+ });
+ });
+
describe('isKeycapEmoji', () => {
it('should gracefully handle empty string', () => {
expect(isKeycapEmoji('')).toBeFalsy();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index f209328dee1..230c15e5de6 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -396,6 +396,25 @@ describe('Filtered Search Manager', () => {
});
});
+ describe('Clearing search', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('Clicking the "x" clear button, clears the input', () => {
+ const inputValue = 'label:~bug ';
+ manager.filteredSearchInput.value = inputValue;
+ manager.filteredSearchInput.dispatchEvent(new Event('input'));
+
+ expect(gl.DropdownUtils.getSearchQuery()).toEqual(inputValue);
+
+ manager.clearSearchButton.click();
+
+ expect(manager.filteredSearchInput.value).toEqual('');
+ expect(gl.DropdownUtils.getSearchQuery()).toEqual('');
+ });
+ });
+
describe('toggleInputContainerFocus', () => {
beforeEach(() => {
initializeManager();
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 5774f36f026..8e74c4f859c 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -6,7 +6,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace) }
- let(:cluster) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')}
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
render_views
diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml
index 97b0c25c923..85ee61f0b54 100644
--- a/spec/javascripts/fixtures/pipelines.html.haml
+++ b/spec/javascripts/fixtures/pipelines.html.haml
@@ -1,16 +1,10 @@
%div
#pipelines-list-vue{ data: { endpoint: 'foo',
- "css-class" => 'foo',
"help-page-path" => 'foo',
+ "help-auto-devops-path" => 'foo',
"empty-state-svg-path" => 'foo',
"error-state-svg-path" => 'foo',
"new-pipeline-path" => 'foo',
"can-create-pipeline" => 'true',
- "all-path" => 'foo',
- "pending-path" => 'foo',
- "running-path" => 'foo',
- "finished-path" => 'foo',
- "branches-path" => 'foo',
- "tags-path" => 'foo',
"has-ci" => 'foo',
"ci-lint-path" => 'foo' } }
diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml
index 7785120da5b..0421ed2182f 100644
--- a/spec/javascripts/fixtures/search_autocomplete.html.haml
+++ b/spec/javascripts/fixtures/search_autocomplete.html.haml
@@ -8,3 +8,4 @@
%input#search.search-input.dropdown-menu-toggle
.dropdown-menu.dropdown-select
.dropdown-content
+ %input{ type: "hidden", class: "js-search-project-options" }
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index ad0c7264616..6f357306ec7 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () {
});
});
+ describe('DefaultOptions.beforeInsert', () => {
+ const beforeInsert = (context, value) => (
+ gfmAutoCompleteCallbacks.beforeInsert.call(context, value)
+ );
+
+ const atwhoInstance = { setting: { skipSpecialCharacterTest: false } };
+
+ it('should not quote if value only contains alphanumeric charecters', () => {
+ expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1');
+ expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1');
+ });
+
+ it('should quote if value contains any non-alphanumeric characters', () => {
+ expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
+ expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
+ });
+
+ it('should quote integer labels', () => {
+ expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
+ });
+ });
+
describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => (
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js
index 124fc030774..5a8009e57fd 100644
--- a/spec/javascripts/gl_form_spec.js
+++ b/spec/javascripts/gl_form_spec.js
@@ -1,9 +1,9 @@
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
import GLForm from '~/gl_form';
import '~/lib/utils/text_utility';
import '~/lib/utils/common_utils';
-window.autosize = autosize;
+window.autosize = Autosize;
describe('GLForm', () => {
describe('when instantiated', function () {
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index cd19a0fae1e..59d4f7c45c6 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -431,9 +431,9 @@ describe('AppComponent', () => {
});
it('should render groups tree', (done) => {
- vm.groups = [mockParentGroupItem];
+ vm.store.state.groups = [mockParentGroupItem];
vm.isLoading = false;
- vm.pageInfo = mockPageInfo;
+ vm.store.state.pageInfo = mockPageInfo;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
done();
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 4751eb868a4..2443ffd48f3 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,4 +1,4 @@
-import '~/header';
+import initTodoToggle from '~/header';
describe('Header', function () {
const todosPendingCount = '.todos-count';
@@ -14,6 +14,7 @@ describe('Header', function () {
preloadFixtures(fixtureTemplate);
beforeEach(() => {
+ initTodoToggle();
loadFixtures(fixtureTemplate);
});
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
index b71136c4114..34acdfbfba9 100644
--- a/spec/javascripts/helpers/vue_mount_component_helper.js
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -1,3 +1,8 @@
+export const createComponentWithStore = (Component, store, propsData = {}) => new Component({
+ store,
+ propsData,
+});
+
export default (Component, props = {}, el = null) => new Component({
propsData: props,
}).$mount(el);
diff --git a/spec/javascripts/issuable_context_spec.js b/spec/javascripts/issuable_context_spec.js
deleted file mode 100644
index f266209027a..00000000000
--- a/spec/javascripts/issuable_context_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'jquery';
-import IssuableContext from '~/issuable_context';
-
-describe('IssuableContext', () => {
- describe('toggleHiddenParticipants', () => {
- const event = jasmine.createSpyObj('event', ['preventDefault']);
-
- beforeEach(() => {
- spyOn($.fn, 'data').and.returnValue('data');
- spyOn($.fn, 'text').and.returnValue('data');
- });
-
- afterEach(() => {
- gl.lazyLoader = undefined;
- });
-
- it('calls loadCheck if lazyLoader is set', () => {
- gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
-
- IssuableContext.prototype.toggleHiddenParticipants(event);
-
- expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
- });
-
- it('does not throw if lazyLoader is not defined', () => {
- gl.lazyLoader = undefined;
-
- const toggle = IssuableContext.prototype.toggleHiddenParticipants.bind(null, event);
-
- expect(toggle).not.toThrow();
- });
- });
-});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 2ea290108a4..5662c7387fb 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -223,23 +223,46 @@ describe('Issuable output', () => {
});
});
- it('closes form on error', (done) => {
- spyOn(window, 'Flash').and.callThrough();
- spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
- reject();
- }));
+ describe('error when updating', () => {
+ beforeEach(() => {
+ spyOn(window, 'Flash').and.callThrough();
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
+ reject();
+ }));
+ });
- vm.updateIssuable();
+ it('closes form on error', (done) => {
+ vm.updateIssuable();
- setTimeout(() => {
- expect(
- eventHub.$emit,
- ).toHaveBeenCalledWith('close.form');
- expect(
- window.Flash,
- ).toHaveBeenCalledWith('Error updating issue');
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error updating issue');
- done();
+ done();
+ });
+ });
+
+ it('returns the correct error message for issuableType', (done) => {
+ vm.issuableType = 'merge request';
+
+ Vue.nextTick(() => {
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error updating merge request');
+
+ done();
+ });
+ });
});
});
});
diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js
index f6625b748b6..d779ab7bb31 100644
--- a/spec/javascripts/issue_show/components/edit_actions_spec.js
+++ b/spec/javascripts/issue_show/components/edit_actions_spec.js
@@ -61,6 +61,15 @@ describe('Edit Actions components', () => {
});
});
+ it('should not show delete button if showDeleteButton is false', (done) => {
+ vm.showDeleteButton = false;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn-danger')).toBeNull();
+ done();
+ });
+ });
+
describe('updateIssuable', () => {
it('sends update.issauble event when clicking save button', () => {
vm.$el.querySelector('.btn-save').click();
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 60a452f2223..3636aac79a0 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,6 +1,5 @@
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import Issue from '~/issue';
-import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import '~/lib/utils/text_utility';
describe('Issue', function() {
@@ -189,37 +188,4 @@ describe('Issue', function() {
});
});
});
-
- describe('units', () => {
- describe('class constructor', () => {
- it('calls .initCloseReopenReport', () => {
- spyOn(Issue.prototype, 'initCloseReopenReport');
-
- new Issue(); // eslint-disable-line no-new
-
- expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled();
- });
- });
-
- describe('initCloseReopenReport', () => {
- it('calls .initDroplab', () => {
- const container = jasmine.createSpyObj('container', ['querySelector']);
- const dropdownTrigger = {};
- const dropdownList = {};
- const button = {};
-
- spyOn(document, 'querySelector').and.returnValue(container);
- spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
- container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
-
- Issue.prototype.initCloseReopenReport();
-
- expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
- expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
- });
- });
- });
});
diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js
index 1d7fa7e12fc..3069a0cd60e 100644
--- a/spec/javascripts/jobs/job_details_mediator_spec.js
+++ b/spec/javascripts/jobs/job_details_mediator_spec.js
@@ -1,39 +1,35 @@
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import JobMediator from '~/jobs/job_details_mediator';
import job from './mock_data';
describe('JobMediator', () => {
let mediator;
+ let mock;
beforeEach(() => {
- mediator = new JobMediator({ endpoint: 'foo' });
+ mediator = new JobMediator({ endpoint: 'jobs/40291672.json' });
+ mock = new MockAdapter(axios);
});
it('should set defaults', () => {
expect(mediator.store).toBeDefined();
expect(mediator.service).toBeDefined();
- expect(mediator.options).toEqual({ endpoint: 'foo' });
+ expect(mediator.options).toEqual({ endpoint: 'jobs/40291672.json' });
expect(mediator.state.isLoading).toEqual(false);
});
describe('request and store data', () => {
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(job), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
+ mock.onGet().reply(200, job, {});
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
+ mock.restore();
});
it('should store received data', (done) => {
mediator.fetchJob();
-
setTimeout(() => {
expect(mediator.store.state.job).toEqual(job);
done();
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 17e4ef26b2c..43532275121 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -22,7 +22,7 @@ export default {
details_path: '/root/ci-mock/-/jobs/4757',
favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/-/jobs/4757/retry',
method: 'post',
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index 4e66304e0ad..a197b35f6fb 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -1,13 +1,12 @@
/* eslint-disable no-new */
import IssuableContext from '~/issuable_context';
-/* global LabelsSelect */
+import LabelsSelect from '~/labels_select';
import '~/gl_dropdown';
import 'select2';
import '~/api';
import '~/create_label';
import '~/users_select';
-import '~/labels_select';
(() => {
let saveLabelCount = 0;
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index a5298be5669..6dad5d6b6bd 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -183,6 +183,36 @@ describe('common_utils', () => {
});
});
+ describe('historyPushState', () => {
+ afterEach(() => {
+ window.history.replaceState({}, null, null);
+ });
+
+ it('should call pushState with the correct path', () => {
+ spyOn(window.history, 'pushState');
+
+ commonUtils.historyPushState('newpath?page=2');
+
+ expect(window.history.pushState).toHaveBeenCalled();
+ expect(window.history.pushState.calls.allArgs()[0][2]).toContain('newpath?page=2');
+ });
+ });
+
+ describe('parseQueryStringIntoObject', () => {
+ it('should return object with query parameters', () => {
+ expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' });
+ expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' });
+ expect(commonUtils.parseQueryStringIntoObject()).toEqual({});
+ });
+ });
+
+ describe('buildUrlWithCurrentLocation', () => {
+ it('should build an url with current location and given parameters', () => {
+ expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
+ expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`);
+ });
+ });
+
describe('getParameterByName', () => {
beforeEach(() => {
window.history.pushState({}, null, '?scope=all&p=2');
diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js
index 0b9fde2be67..e58ac4300ba 100644
--- a/spec/javascripts/lib/utils/datefix_spec.js
+++ b/spec/javascripts/lib/utils/datefix_spec.js
@@ -1,4 +1,4 @@
-import { pad, parsePikadayDate, pikadayToString } from '~/lib/utils/datefix';
+import { pad, pikadayToString } from '~/lib/utils/datefix';
describe('datefix', () => {
describe('pad', () => {
@@ -16,9 +16,7 @@ describe('datefix', () => {
});
describe('parsePikadayDate', () => {
- it('should return a UTC date', () => {
- expect(parsePikadayDate('2020-01-29')).toEqual(new Date('2020-01-29'));
- });
+ // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834
});
describe('pikadayToString', () => {
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index 83c92deccdc..fcf27f6805f 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -1,4 +1,4 @@
-import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils';
+import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -52,4 +52,29 @@ describe('Number Utils', () => {
expect(bytesToMiB(1000000)).toEqual(0.95367431640625);
});
});
+
+ describe('bytesToGiB', () => {
+ it('calculates GiB for the given bytes', () => {
+ expect(bytesToGiB(1073741824)).toEqual(1);
+ expect(bytesToGiB(10737418240)).toEqual(10);
+ });
+ });
+
+ describe('numberToHumanSize', () => {
+ it('should return bytes', () => {
+ expect(numberToHumanSize(654)).toEqual('654 bytes');
+ });
+
+ it('should return KiB', () => {
+ expect(numberToHumanSize(1079)).toEqual('1.05 KiB');
+ });
+
+ it('should return MiB', () => {
+ expect(numberToHumanSize(10485764)).toEqual('10.00 MiB');
+ });
+
+ it('should return GiB', () => {
+ expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB');
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index 2aa7011ca51..9b8f68f1676 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -155,7 +155,7 @@ describe('Poll', () => {
successCallback: () => {
Polling.stop();
setTimeout(() => {
- Polling.restart();
+ Polling.restart({ data: { page: 4 } });
}, 0);
},
errorCallback: callbacks.error,
@@ -170,10 +170,10 @@ describe('Poll', () => {
Polling.stop();
expect(service.fetch.calls.count()).toEqual(2);
- expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
+ expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
expect(Polling.stop).toHaveBeenCalled();
expect(Polling.restart).toHaveBeenCalled();
-
+ expect(Polling.options.data).toEqual({ page: 4 });
done();
});
});
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
new file mode 100644
index 00000000000..a95a7e2a5be
--- /dev/null
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -0,0 +1,62 @@
+import textUtils from '~/lib/utils/text_markdown';
+
+describe('init markdown', () => {
+ let textArea;
+
+ beforeAll(() => {
+ textArea = document.createElement('textarea');
+ document.querySelector('body').appendChild(textArea);
+ textArea.focus();
+ });
+
+ afterAll(() => {
+ textArea.parentNode.removeChild(textArea);
+ });
+
+ describe('without selection', () => {
+ it('inserts the tag on an empty line', () => {
+ const initialValue = '';
+
+ textArea.value = initialValue;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = 0;
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on a new line if the current one is not empty', () => {
+ const initialValue = 'some text';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}\n* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains spaces', () => {
+ const initialValue = ' ';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains tabs', () => {
+ const initialValue = '\t\t\t';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index 829b3ef5735..b21bd958f90 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -1,109 +1,57 @@
-import { highCountTrim } from '~/lib/utils/text_utility';
+import * as textUtils from '~/lib/utils/text_utility';
describe('text_utility', () => {
- describe('gl.text.getTextWidth', () => {
- it('returns zero width when no text is passed', () => {
- expect(gl.text.getTextWidth('')).toBe(0);
+ describe('addDelimiter', () => {
+ it('should add a delimiter to the given string', () => {
+ expect(textUtils.addDelimiter('1234')).toEqual('1,234');
+ expect(textUtils.addDelimiter('222222')).toEqual('222,222');
});
- it('returns zero width when no text is passed and font is passed', () => {
- expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
- });
-
- it('returns width when text is passed', () => {
- expect(gl.text.getTextWidth('foo') > 0).toBe(true);
- });
-
- it('returns bigger width when font is larger', () => {
- const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
- const regular = gl.text.getTextWidth('foo', '10px sans-serif');
- expect(largeFont > regular).toBe(true);
- });
- });
-
- describe('gl.text.pluralize', () => {
- it('returns pluralized', () => {
- expect(gl.text.pluralize('test', 2)).toBe('tests');
- });
-
- it('returns pluralized when count is 0', () => {
- expect(gl.text.pluralize('test', 0)).toBe('tests');
- });
-
- it('does not return pluralized', () => {
- expect(gl.text.pluralize('test', 1)).toBe('test');
+ it('should not add a delimiter if string contains no numbers', () => {
+ expect(textUtils.addDelimiter('aaaa')).toEqual('aaaa');
});
});
describe('highCountTrim', () => {
it('returns 99+ for count >= 100', () => {
- expect(highCountTrim(105)).toBe('99+');
- expect(highCountTrim(100)).toBe('99+');
+ expect(textUtils.highCountTrim(105)).toBe('99+');
+ expect(textUtils.highCountTrim(100)).toBe('99+');
});
it('returns exact number for count < 100', () => {
- expect(highCountTrim(45)).toBe(45);
+ expect(textUtils.highCountTrim(45)).toBe(45);
});
});
- describe('gl.text.insertText', () => {
- let textArea;
-
- beforeAll(() => {
- textArea = document.createElement('textarea');
- document.querySelector('body').appendChild(textArea);
- textArea.focus();
+ describe('humanize', () => {
+ it('should remove underscores and uppercase the first letter', () => {
+ expect(textUtils.humanize('foo_bar')).toEqual('Foo bar');
});
+ });
- afterAll(() => {
- textArea.parentNode.removeChild(textArea);
+ describe('pluralize', () => {
+ it('should pluralize given string', () => {
+ expect(textUtils.pluralize('test', 2)).toBe('tests');
});
- describe('without selection', () => {
- it('inserts the tag on an empty line', () => {
- const initialValue = '';
-
- textArea.value = initialValue;
- textArea.selectionStart = 0;
- textArea.selectionEnd = 0;
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
-
- it('inserts the tag on a new line if the current one is not empty', () => {
- const initialValue = 'some text';
-
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}\n* `);
- });
-
- it('inserts the tag on the same line if the current line only contains spaces', () => {
- const initialValue = ' ';
-
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
-
- it('inserts the tag on the same line if the current line only contains tabs', () => {
- const initialValue = '\t\t\t';
+ it('should pluralize when count is 0', () => {
+ expect(textUtils.pluralize('test', 0)).toBe('tests');
+ });
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
+ it('should not pluralize when count is 1', () => {
+ expect(textUtils.pluralize('test', 1)).toBe('test');
+ });
+ });
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+ describe('dasherize', () => {
+ it('should replace underscores with dashes', () => {
+ expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo');
+ });
+ });
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
+ describe('slugify', () => {
+ it('should remove accents and convert to lower case', () => {
+ expect(textUtils.slugify('João')).toEqual('joão');
});
});
});
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
index ac6ace48108..6054b75d0b8 100644
--- a/spec/javascripts/merge_request_notes_spec.js
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -1,6 +1,6 @@
/* global Notes */
-import 'vendor/autosize';
+import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js
index 2571b7ef869..145c8db28d5 100644
--- a/spec/javascripts/monitoring/graph/legend_spec.js
+++ b/spec/javascripts/monitoring/graph/legend_spec.js
@@ -28,7 +28,7 @@ const defaultValuesComponent = {
currentDataIndex: 0,
};
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0],
+const timeSeries = createTimeSeries(convertedMetrics[0].queries,
defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
defaultValuesComponent.graphHeightOffset);
diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
index 81825a3ae87..c83bd19345f 100644
--- a/spec/javascripts/monitoring/graph_path_spec.js
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -13,7 +13,7 @@ const createComponent = (propsData) => {
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120);
+const timeSeries = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120);
const firstTimeSeries = timeSeries[0];
describe('Monitoring Paths', () => {
@@ -32,4 +32,21 @@ describe('Monitoring Paths', () => {
expect(metricLine.getAttribute('stroke')).toBe('#1f78d1');
expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath);
});
+
+ describe('Computed properties', () => {
+ it('strokeDashArray', () => {
+ const component = createComponent({
+ generatedLinePath: firstTimeSeries.linePath,
+ generatedAreaPath: firstTimeSeries.areaPath,
+ lineColor: firstTimeSeries.lineColor,
+ areaColor: firstTimeSeries.areaColor,
+ });
+
+ component.lineStyle = 'dashed';
+ expect(component.strokeDashArray).toBe('3, 1');
+
+ component.lineStyle = 'dotted';
+ expect(component.strokeDashArray).toBe('1, 1');
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
index 7e44a9ade9e..99584c75287 100644
--- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
+++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
@@ -2,7 +2,7 @@ import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data';
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120);
+const timeSeries = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120);
const firstTimeSeries = timeSeries[0];
describe('Multiple time series', () => {
diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js
new file mode 100644
index 00000000000..9d7625ca269
--- /dev/null
+++ b/spec/javascripts/namespace_select_spec.js
@@ -0,0 +1,65 @@
+import NamespaceSelect from '~/namespace_select';
+
+describe('NamespaceSelect', () => {
+ beforeEach(() => {
+ spyOn($.fn, 'glDropdown');
+ });
+
+ it('initializes glDropdown', () => {
+ const dropdown = document.createElement('div');
+
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+
+ expect($.fn.glDropdown).toHaveBeenCalled();
+ });
+
+ describe('as input', () => {
+ let glDropdownOptions;
+
+ beforeEach(() => {
+ const dropdown = document.createElement('div');
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+ glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ });
+
+ it('prevents click events', () => {
+ const dummyEvent = new Event('dummy');
+ spyOn(dummyEvent, 'preventDefault');
+
+ glDropdownOptions.clicked({ e: dummyEvent });
+
+ expect(dummyEvent.preventDefault).toHaveBeenCalled();
+ });
+ });
+
+ describe('as filter', () => {
+ let glDropdownOptions;
+
+ beforeEach(() => {
+ const dropdown = document.createElement('div');
+ dropdown.dataset.isFilter = 'true';
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+ glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ });
+
+ it('does not prevent click events', () => {
+ const dummyEvent = new Event('dummy');
+ spyOn(dummyEvent, 'preventDefault');
+
+ glDropdownOptions.clicked({ e: dummyEvent });
+
+ expect(dummyEvent.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('sets URL of dropdown items', () => {
+ const dummyNamespace = { id: 'eal' };
+
+ const itemUrl = glDropdownOptions.url(dummyNamespace);
+
+ expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`);
+ });
+ });
+});
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index c57f44dae17..50a5e4ff056 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
-/* global NewBranchForm */
-import '~/new_branch_form';
+import NewBranchForm from '~/new_branch_form';
(function() {
describe('Branch', function() {
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
index 3f659af5c3b..db75262b562 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
import store from '~/notes/stores';
import issueCommentForm from '~/notes/components/issue_comment_form.vue';
import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data';
@@ -55,6 +55,25 @@ describe('issue_comment_form component', () => {
expect(vm.toggleIssueState).toHaveBeenCalled();
});
+
+ it('should disable action button whilst submitting', (done) => {
+ const saveNotePromise = Promise.resolve();
+ vm.note = 'hello world';
+ spyOn(vm, 'saveNote').and.returnValue(saveNotePromise);
+ spyOn(vm, 'stopPolling');
+
+ const actionButton = vm.$el.querySelector('.js-action-button');
+
+ vm.handleSave();
+
+ Vue.nextTick()
+ .then(() => expect(actionButton.disabled).toBeTruthy())
+ .then(saveNotePromise)
+ .then(Vue.nextTick)
+ .then(() => expect(actionButton.disabled).toBeFalsy())
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('textarea', () => {
@@ -97,14 +116,14 @@ describe('issue_comment_form component', () => {
});
it('should resize textarea after note discarded', (done) => {
- spyOn(autosize, 'update');
+ spyOn(Autosize, 'update');
spyOn(vm, 'discard').and.callThrough();
vm.note = 'foo';
vm.discard();
Vue.nextTick(() => {
- expect(autosize.update).toHaveBeenCalled();
+ expect(Autosize.update).toHaveBeenCalled();
done();
});
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 4546b88e44d..63abac222c4 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,11 +1,12 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-import 'vendor/autosize';
+import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
-import '~/render_gfm';
import '~/render_math';
+import '~/render_mermaid';
+import '~/render_gfm';
import '~/notes';
(function() {
@@ -343,6 +344,7 @@ import '~/notes';
diff_discussion_html: false,
};
$form = jasmine.createSpyObj('$form', ['closest', 'find']);
+ $form.length = 1;
row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
notes = jasmine.createSpyObj('notes', [
@@ -371,13 +373,29 @@ import '~/notes';
$form.closest.and.returnValues(row, $form);
$form.find.and.returnValues(discussionContainer);
body.attr.and.returnValue('');
-
- Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
it('should call Notes.animateAppendNote', () => {
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
});
+
+ it('should append to row selected with line_code', () => {
+ $form.length = 0;
+ note.discussion_line_code = 'line_code';
+ note.diff_discussion_html = '<tr></tr>';
+
+ const line = document.createElement('div');
+ line.id = note.discussion_line_code;
+ document.body.appendChild(line);
+
+ $form.closest.and.returnValues($form);
+
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
+ expect(line.nextSibling.outerHTML).toEqual(note.diff_discussion_html);
+ });
});
describe('Discussion sub note', () => {
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 85bd87318db..e8fcd4b1a36 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -11,7 +11,7 @@ describe('pipeline graph action component', () => {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
- actionIcon: 'icon_action_cancel',
+ actionIcon: 'cancel',
},
}).$mount();
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
index 25fd18b197e..ba721bc53c6 100644
--- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
@@ -11,7 +11,7 @@ describe('action component', () => {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
- actionIcon: 'icon_action_cancel',
+ actionIcon: 'cancel',
},
}).$mount();
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index e90593e0f40..342ee6c1242 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -14,7 +14,7 @@ describe('pipeline graph job component', () => {
group: 'success',
details_path: '/root/ci-mock/builds/4256',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4256/retry',
method: 'post',
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
index 56c522b7f77..b9494f86d74 100644
--- a/spec/javascripts/pipelines/graph/mock_data.js
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -39,7 +39,7 @@ export default {
"details_path": "/root/ci-mock/builds/4153",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4153/retry",
"method": "post"
@@ -62,7 +62,7 @@ export default {
"details_path": "/root/ci-mock/builds/4153",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4153/retry",
"method": "post"
@@ -96,7 +96,7 @@ export default {
"details_path": "/root/ci-mock/builds/4166",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4166/retry",
"method": "post"
@@ -119,7 +119,7 @@ export default {
"details_path": "/root/ci-mock/builds/4166",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4166/retry",
"method": "post"
@@ -138,7 +138,7 @@ export default {
"details_path": "/root/ci-mock/builds/4159",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4159/retry",
"method": "post"
@@ -161,7 +161,7 @@ export default {
"details_path": "/root/ci-mock/builds/4159",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4159/retry",
"method": "post"
diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
index aa4d6eedaf4..063ab53681b 100644
--- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js
+++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
@@ -13,7 +13,7 @@ describe('stage column component', () => {
group: 'success',
details_path: '/root/ci-mock/builds/4256',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4256/retry',
method: 'post',
diff --git a/spec/javascripts/pipelines/navigation_tabs_spec.js b/spec/javascripts/pipelines/navigation_tabs_spec.js
index 53a88e6322f..f125a2fa189 100644
--- a/spec/javascripts/pipelines/navigation_tabs_spec.js
+++ b/spec/javascripts/pipelines/navigation_tabs_spec.js
@@ -8,120 +8,48 @@ describe('navigation tabs pipeline component', () => {
let data;
beforeEach(() => {
- data = {
- scope: 'all',
- count: {
- all: 16,
- running: 1,
- pending: 10,
- finished: 0,
+ data = [
+ {
+ name: 'All',
+ scope: 'all',
+ count: 1,
+ isActive: true,
+ },
+ {
+ name: 'Pending',
+ scope: 'pending',
+ count: 0,
+ isActive: false,
},
- paths: {
- allPath: '/gitlab-org/gitlab-ce/pipelines',
- pendingPath: '/gitlab-org/gitlab-ce/pipelines?scope=pending',
- finishedPath: '/gitlab-org/gitlab-ce/pipelines?scope=finished',
- runningPath: '/gitlab-org/gitlab-ce/pipelines?scope=running',
- branchesPath: '/gitlab-org/gitlab-ce/pipelines?scope=branches',
- tagsPath: '/gitlab-org/gitlab-ce/pipelines?scope=tags',
+ {
+ name: 'Running',
+ scope: 'running',
+ isActive: false,
},
- };
+ ];
Component = Vue.extend(navigationTabs);
+ vm = mountComponent(Component, { tabs: data });
});
afterEach(() => {
vm.$destroy();
});
- it('should render tabs with correct paths', () => {
- vm = mountComponent(Component, data);
-
- // All
- const allTab = vm.$el.querySelector('.js-pipelines-tab-all a');
- expect(allTab.textContent.trim()).toContain('All');
- expect(allTab.getAttribute('href')).toEqual(data.paths.allPath);
-
- // Pending
- const pendingTab = vm.$el.querySelector('.js-pipelines-tab-pending a');
- expect(pendingTab.textContent.trim()).toContain('Pending');
- expect(pendingTab.getAttribute('href')).toEqual(data.paths.pendingPath);
-
- // Running
- const runningTab = vm.$el.querySelector('.js-pipelines-tab-running a');
- expect(runningTab.textContent.trim()).toContain('Running');
- expect(runningTab.getAttribute('href')).toEqual(data.paths.runningPath);
-
- // Finished
- const finishedTab = vm.$el.querySelector('.js-pipelines-tab-finished a');
- expect(finishedTab.textContent.trim()).toContain('Finished');
- expect(finishedTab.getAttribute('href')).toEqual(data.paths.finishedPath);
-
- // Branches
- const branchesTab = vm.$el.querySelector('.js-pipelines-tab-branches a');
- expect(branchesTab.textContent.trim()).toContain('Branches');
-
- // Tags
- const tagsTab = vm.$el.querySelector('.js-pipelines-tab-tags a');
- expect(tagsTab.textContent.trim()).toContain('Tags');
+ it('should render tabs', () => {
+ expect(vm.$el.querySelectorAll('li').length).toEqual(data.length);
});
- describe('scope', () => {
- it('should render scope provided as active tab', () => {
- vm = mountComponent(Component, data);
- expect(vm.$el.querySelector('.js-pipelines-tab-all').className).toContain('active');
- });
+ it('should render active tab', () => {
+ expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined();
});
- describe('badges', () => {
- it('should render provided number', () => {
- vm = mountComponent(Component, data);
- // All
- expect(
- vm.$el.querySelector('.js-totalbuilds-count').textContent.trim(),
- ).toContain(data.count.all);
-
- // Pending
- expect(
- vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim(),
- ).toContain(data.count.pending);
-
- // Running
- expect(
- vm.$el.querySelector('.js-pipelines-tab-running .badge').textContent.trim(),
- ).toContain(data.count.running);
-
- // Finished
- expect(
- vm.$el.querySelector('.js-pipelines-tab-finished .badge').textContent.trim(),
- ).toContain(data.count.finished);
- });
-
- it('should not render badge when number is undefined', () => {
- vm = mountComponent(Component, {
- scope: 'all',
- paths: {},
- count: {},
- });
-
- // All
- expect(
- vm.$el.querySelector('.js-totalbuilds-count'),
- ).toEqual(null);
-
- // Pending
- expect(
- vm.$el.querySelector('.js-pipelines-tab-pending .badge'),
- ).toEqual(null);
-
- // Running
- expect(
- vm.$el.querySelector('.js-pipelines-tab-running .badge'),
- ).toEqual(null);
+ it('should render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1');
+ expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual('0');
+ });
- // Finished
- expect(
- vm.$el.querySelector('.js-pipelines-tab-finished .badge'),
- ).toEqual(null);
- });
+ it('should not render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null);
});
});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index c30abb2edb0..ff38bc1974d 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
@@ -9,26 +10,33 @@ describe('Pipelines', () => {
preloadFixtures(jsonFixtureName);
let PipelinesComponent;
- let pipeline;
+ let pipelines;
+ let component;
beforeEach(() => {
loadFixtures('static/pipelines.html.raw');
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
- pipeline = pipelines.find(p => p.id === 1);
+ pipelines = getJSONFixture(jsonFixtureName);
PipelinesComponent = Vue.extend(pipelinesComp);
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
describe('successfull request', () => {
describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(pipeline), {
+ next(request.respondWith(JSON.stringify(pipelines), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor);
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
});
afterEach(() => {
@@ -38,18 +46,71 @@ describe('Pipelines', () => {
});
it('should render table', (done) => {
- const component = new PipelinesComponent({
- propsData: {
- store: new Store(),
- },
- }).$mount();
-
setTimeout(() => {
expect(component.$el.querySelector('.table-holder')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(
+ component.$el.querySelectorAll('.gl-responsive-table-row').length,
+ ).toEqual(pipelines.pipelines.length + 1);
done();
});
});
+
+ it('should render navigation tabs', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
+ ).toContain('Pending');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
+ ).toContain('All');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
+ ).toContain('Running');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
+ ).toContain('Finished');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
+ ).toContain('Branches');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
+ ).toContain('Tags');
+ done();
+ });
+ });
+
+ it('should make an API request when using tabs', (done) => {
+ setTimeout(() => {
+ spyOn(component, 'updateContent');
+ component.$el.querySelector('.js-pipelines-tab-finished').click();
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
+ done();
+ });
+ });
+
+ describe('with pagination', () => {
+ it('should make an API request when using pagination', (done) => {
+ setTimeout(() => {
+ spyOn(component, 'updateContent');
+ // Mock pagination
+ component.store.state.pageInfo = {
+ page: 1,
+ total: 10,
+ perPage: 2,
+ nextPage: 2,
+ totalPages: 5,
+ };
+
+ Vue.nextTick(() => {
+ component.$el.querySelector('.js-next-button a').click();
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
+
+ done();
+ });
+ });
+ });
+ });
});
describe('without pipelines', () => {
@@ -70,15 +131,14 @@ describe('Pipelines', () => {
});
it('should render empty state', (done) => {
- const component = new PipelinesComponent({
+ component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
setTimeout(() => {
- expect(component.$el.querySelector('.empty-state')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(component.$el.querySelector('.empty-state')).not.toBe(null);
done();
});
});
@@ -103,7 +163,7 @@ describe('Pipelines', () => {
});
it('should render error state', (done) => {
- const component = new PipelinesComponent({
+ component = new PipelinesComponent({
propsData: {
store: new Store(),
},
@@ -111,9 +171,50 @@ describe('Pipelines', () => {
setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
});
});
});
+
+ describe('updateContent', () => {
+ it('should set given parameters', () => {
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
+ component.updateContent({ scope: 'finished', page: '4' });
+
+ expect(component.page).toEqual('4');
+ expect(component.scope).toEqual('finished');
+ expect(component.requestData.scope).toEqual('finished');
+ expect(component.requestData.page).toEqual('4');
+ });
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
+
+ spyOn(component, 'updateContent');
+
+ component.onChangeTab('running');
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
+
+ spyOn(component, 'updateContent');
+
+ component.onChangePage(4);
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
+ });
+ });
});
diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js
index c9c5ce096fc..9a705a1f0ed 100644
--- a/spec/javascripts/repo/components/new_branch_form_spec.js
+++ b/spec/javascripts/repo/components/new_branch_form_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import newBranchForm from '~/repo/components/new_branch_form.vue';
-import eventHub from '~/repo/event_hub';
-import RepoStore from '~/repo/stores/repo_store';
-import createComponent from '../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
describe('Multi-file editor new branch form', () => {
let vm;
@@ -10,17 +10,17 @@ describe('Multi-file editor new branch form', () => {
beforeEach(() => {
const Component = Vue.extend(newBranchForm);
- RepoStore.currentBranch = 'master';
+ vm = createComponentWithStore(Component, store);
- vm = createComponent(Component, {
- currentBranch: RepoStore.currentBranch,
- });
+ vm.$store.state.currentBranch = 'master';
+
+ vm.$mount();
});
afterEach(() => {
vm.$destroy();
- RepoStore.currentBranch = '';
+ resetStore(vm.$store);
});
describe('template', () => {
@@ -48,6 +48,10 @@ describe('Multi-file editor new branch form', () => {
});
describe('submitNewBranch', () => {
+ beforeEach(() => {
+ spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve());
+ });
+
it('sets to loading', () => {
vm.submitNewBranch();
@@ -66,57 +70,45 @@ describe('Multi-file editor new branch form', () => {
});
});
- it('emits an event with branchName', () => {
- spyOn(eventHub, '$emit');
-
+ it('calls createdNewBranch with branchName', () => {
vm.branchName = 'testing';
vm.submitNewBranch();
- expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranch', 'testing');
+ expect(vm.createNewBranch).toHaveBeenCalledWith('testing');
});
});
- describe('showErrorMessage', () => {
- it('sets loading to false', () => {
- vm.loading = true;
-
- vm.showErrorMessage();
-
- expect(vm.loading).toBeFalsy();
- });
-
- it('creates flash element', () => {
- vm.showErrorMessage('error message');
-
- expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
- expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
+ describe('submitNewBranch with error', () => {
+ beforeEach(() => {
+ spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({
+ json: () => Promise.resolve({
+ message: 'error message',
+ }),
+ }));
});
- });
- describe('createdNewBranch', () => {
- it('set loading to false', () => {
+ it('sets loading to false', (done) => {
vm.loading = true;
- vm.createdNewBranch();
-
- expect(vm.loading).toBeFalsy();
- });
-
- it('resets branch name', () => {
- vm.branchName = 'testing';
+ vm.submitNewBranch();
- vm.createdNewBranch();
+ setTimeout(() => {
+ expect(vm.loading).toBeFalsy();
- expect(vm.branchName).toBe('');
+ done();
+ });
});
- it('sets the dropdown toggle text', () => {
- vm.dropdownText = document.createElement('span');
+ it('creates flash element', (done) => {
+ vm.submitNewBranch();
- vm.createdNewBranch('branch name');
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
+ expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
- expect(vm.dropdownText.textContent).toBe('branch name');
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js
index ddbfdab582d..93b10fc1fee 100644
--- a/spec/javascripts/repo/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import newDropdown from '~/repo/components/new_dropdown/index.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import RepoHelper from '~/repo/helpers/repo_helper';
-import eventHub from '~/repo/event_hub';
-import createComponent from '../../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
describe('new dropdown component', () => {
let vm;
@@ -11,15 +10,17 @@ describe('new dropdown component', () => {
beforeEach(() => {
const component = Vue.extend(newDropdown);
- vm = createComponent(component);
+ vm = createComponentWithStore(component, store);
+
+ vm.$store.state.path = '';
+
+ vm.$mount();
});
afterEach(() => {
vm.$destroy();
- RepoStore.files = [];
- RepoStore.openedFiles = [];
- RepoStore.setViewToPreview();
+ resetStore(vm.$store);
});
it('renders new file and new directory links', () => {
@@ -67,125 +68,4 @@ describe('new dropdown component', () => {
.catch(done.fail);
});
});
-
- describe('createEntryInStore', () => {
- ['tree', 'blob'].forEach((type) => {
- describe(type, () => {
- it('closes modal after creating file', () => {
- vm.openModal = true;
-
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(vm.openModal).toBeFalsy();
- });
-
- it('sets editMode to true', () => {
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(RepoStore.editMode).toBeTruthy();
- });
-
- it('toggles blob view', () => {
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(RepoStore.isPreviewView()).toBeFalsy();
- });
-
- it('adds file into activeFiles', () => {
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(RepoStore.openedFiles.length).toBe(1);
- });
-
- it(`creates ${type} in the current stores path`, () => {
- RepoStore.path = 'testing';
-
- eventHub.$emit('createNewEntry', 'testing/app', type);
-
- expect(RepoStore.files[0].path).toBe('testing/app');
- expect(RepoStore.files[0].name).toBe('app');
-
- if (type === 'tree') {
- expect(RepoStore.files[0].files.length).toBe(1);
- }
-
- RepoStore.path = '';
- });
- });
- });
-
- describe('file', () => {
- it('creates new file', () => {
- eventHub.$emit('createNewEntry', 'testing', 'blob');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('testing');
- expect(RepoStore.files[0].type).toBe('blob');
- expect(RepoStore.files[0].tempFile).toBeTruthy();
- });
-
- it('does not create temp file when file already exists', () => {
- RepoStore.files.push(RepoHelper.serializeRepoEntity('blob', {
- name: 'testing',
- }));
-
- eventHub.$emit('createNewEntry', 'testing', 'blob');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('testing');
- expect(RepoStore.files[0].type).toBe('blob');
- expect(RepoStore.files[0].tempFile).toBeUndefined();
- });
- });
-
- describe('tree', () => {
- it('creates new tree', () => {
- eventHub.$emit('createNewEntry', 'testing', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('testing');
- expect(RepoStore.files[0].type).toBe('tree');
- expect(RepoStore.files[0].tempFile).toBeTruthy();
- expect(RepoStore.files[0].files.length).toBe(1);
- expect(RepoStore.files[0].files[0].name).toBe('.gitkeep');
- });
-
- it('creates multiple trees when entryName has slashes', () => {
- eventHub.$emit('createNewEntry', 'app/test', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('app');
- expect(RepoStore.files[0].files[0].name).toBe('test');
- expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
- });
-
- it('creates tree in existing tree', () => {
- RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
- name: 'app',
- }));
-
- eventHub.$emit('createNewEntry', 'app/test', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('app');
- expect(RepoStore.files[0].tempFile).toBeUndefined();
- expect(RepoStore.files[0].files[0].tempFile).toBeTruthy();
- expect(RepoStore.files[0].files[0].name).toBe('test');
- expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
- });
-
- it('does not create new tree when already exists', () => {
- RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
- name: 'app',
- }));
-
- eventHub.$emit('createNewEntry', 'app', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('app');
- expect(RepoStore.files[0].tempFile).toBeUndefined();
- expect(RepoStore.files[0].files.length).toBe(0);
- });
- });
- });
});
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
index 4c5cdc47c6e..1ff7590ec79 100644
--- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import modal from '~/repo/components/new_dropdown/modal.vue';
-import eventHub from '~/repo/event_hub';
-import createComponent from '../../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
@@ -11,18 +11,18 @@ describe('new file modal component', () => {
afterEach(() => {
vm.$destroy();
- RepoStore.files = [];
- RepoStore.openedFiles = [];
- RepoStore.setViewToPreview();
+ resetStore(vm.$store);
});
['tree', 'blob'].forEach((type) => {
describe(type, () => {
beforeEach(() => {
- vm = createComponent(Component, {
+ vm = createComponentWithStore(Component, store, {
type,
- currentPath: RepoStore.path,
- });
+ path: '',
+ }).$mount();
+
+ vm.entryName = 'testing';
});
it(`sets modal title as ${type}`, () => {
@@ -42,35 +42,157 @@ describe('new file modal component', () => {
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`);
});
+
+ describe('createEntryInStore', () => {
+ it('calls createTempEntry', () => {
+ spyOn(vm, 'createTempEntry');
+
+ vm.createEntryInStore();
+
+ expect(vm.createTempEntry).toHaveBeenCalledWith({
+ name: 'testing',
+ type,
+ });
+ });
+
+ it('sets editMode to true', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.editMode).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('toggles blob view', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.currentBlobView).toBe('repo-editor');
+
+ done();
+ });
+ });
+
+ it('opens newly created file', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.openFiles.length).toBe(1);
+ expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
+
+ done();
+ });
+ });
+
+ it(`creates ${type} in the current stores path`, (done) => {
+ vm.$store.state.path = 'app';
+
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree[0].path).toBe('app/testing');
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+
+ if (type === 'tree') {
+ expect(vm.$store.state.tree[0].tree.length).toBe(1);
+ }
+
+ done();
+ });
+ });
+
+ if (type === 'blob') {
+ it('creates new file', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('blob');
+ expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('does not create temp file when file already exists', (done) => {
+ vm.$store.state.tree.push(file('testing', '1', type));
+
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('blob');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+
+ done();
+ });
+ });
+ } else {
+ it('creates new tree', () => {
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('tree');
+ expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+ expect(vm.$store.state.tree[0].tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('creates multiple trees when entryName has slashes', () => {
+ vm.entryName = 'app/test';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
+ expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('creates tree in existing tree', () => {
+ vm.$store.state.tree.push(file('app', '1', 'tree'));
+
+ vm.entryName = 'app/test';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(vm.$store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
+ expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('does not create new tree when already exists', () => {
+ vm.$store.state.tree.push(file('app', '1', 'tree'));
+
+ vm.entryName = 'app';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(vm.$store.state.tree[0].tree.length).toBe(0);
+ });
+ }
+ });
});
});
it('focuses field on mount', () => {
document.body.innerHTML += '<div class="js-test"></div>';
- vm = createComponent(Component, {
+ vm = createComponentWithStore(Component, store, {
type: 'tree',
- currentPath: RepoStore.path,
- }, '.js-test');
+ path: '',
+ }).$mount('.js-test');
expect(document.activeElement).toBe(vm.$refs.fieldName);
vm.$el.remove();
});
-
- describe('createEntryInStore', () => {
- it('emits createNewEntry event', () => {
- spyOn(eventHub, '$emit');
-
- vm = createComponent(Component, {
- type: 'tree',
- currentPath: RepoStore.path,
- });
- vm.entryName = 'testing';
-
- vm.createEntryInStore();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', 'testing', 'tree');
- });
- });
});
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..bf7893029b1
--- /dev/null
+++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import upload from '~/repo/components/new_dropdown/upload.vue';
+import store from '~/repo/stores';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown upload', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(upload);
+
+ vm = createComponentWithStore(Component, store, {
+ path: '',
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('readFile', () => {
+ beforeEach(() => {
+ spyOn(FileReader.prototype, 'readAsText');
+ spyOn(FileReader.prototype, 'readAsDataURL');
+ });
+
+ it('calls readAsText for text files', () => {
+ const file = {
+ type: 'text/html',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
+ });
+
+ it('calls readAsDataURL for non-text files', () => {
+ const file = {
+ type: 'images/png',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
+ });
+ });
+
+ describe('createFile', () => {
+ const target = {
+ result: 'content',
+ };
+ const binaryTarget = {
+ result: 'base64,base64content',
+ };
+ const file = {
+ name: 'file',
+ };
+
+ it('creates new file', (done) => {
+ vm.createFile(target, file, true);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(target.result);
+
+ done();
+ });
+ });
+
+ it('creates new file in path', (done) => {
+ vm.$store.state.path = 'testing';
+ vm.createFile(target, file, true);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(target.result);
+ expect(vm.$store.state.tree[0].path).toBe(`testing/${file.name}`);
+
+ done();
+ });
+ });
+
+ it('splits content on base64 if binary', (done) => {
+ vm.createFile(binaryTarget, file, false);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
+ expect(vm.$store.state.tree[0].base64).toBe(true);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index e09d593f04c..0f991e1b727 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -1,56 +1,43 @@
import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
import repoCommitSection from '~/repo/components/repo_commit_section.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import RepoService from '~/repo/services/repo_service';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
+import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
- const branch = 'master';
- const projectUrl = 'projectUrl';
- let changedFiles;
- let openedFiles;
+ let vm;
- RepoStore.projectUrl = projectUrl;
-
- function createComponent(el) {
+ function createComponent() {
const RepoCommitSection = Vue.extend(repoCommitSection);
- return new RepoCommitSection().$mount(el);
+ const comp = new RepoCommitSection({
+ store,
+ }).$mount();
+
+ comp.$store.state.currentBranch = 'master';
+ comp.$store.state.openFiles = [file(), file()];
+ comp.$store.state.openFiles.forEach(f => Object.assign(f, {
+ changed: true,
+ content: 'testing',
+ }));
+
+ return comp.$mount();
}
beforeEach(() => {
- // Create a copy for each test because these can get modified directly
- changedFiles = [{
- id: 0,
- changed: true,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
- path: 'dir/file0.ext',
- newContent: 'a',
- }, {
- id: 1,
- changed: true,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
- path: 'dir/file1.ext',
- newContent: 'b',
- }];
- openedFiles = changedFiles.concat([{
- id: 2,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
- path: 'dir/file2.ext',
- changed: false,
- }]);
+ vm = createComponent();
});
- it('renders a commit section', () => {
- RepoStore.isCommitable = true;
- RepoStore.currentBranch = branch;
- RepoStore.targetBranch = branch;
- RepoStore.openedFiles = openedFiles;
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
- const vm = createComponent();
+ it('renders a commit section', () => {
const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')];
- const commitMessage = vm.$el.querySelector('#commit-message');
- const submitCommit = vm.$refs.submitCommit;
+ const submitCommit = vm.$el.querySelector('.btn');
const targetBranch = vm.$el.querySelector('.target-branch');
expect(vm.$el.querySelector(':scope > form')).toBeTruthy();
@@ -58,160 +45,70 @@ describe('RepoCommitSection', () => {
expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => {
- expect(changedFile.textContent.trim()).toEqual(changedFiles[i].path);
+ expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path);
});
- expect(commitMessage.tagName).toEqual('TEXTAREA');
- expect(commitMessage.name).toEqual('commit-message');
- expect(submitCommit.type).toEqual('submit');
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy();
expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files');
expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch');
- expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual(branch);
- });
-
- it('does not render if not isCommitable', () => {
- RepoStore.isCommitable = false;
- RepoStore.openedFiles = [{
- id: 0,
- changed: true,
- }];
-
- const vm = createComponent();
-
- expect(vm.$el.innerHTML).toBeFalsy();
- });
-
- it('does not render if no changedFiles', () => {
- RepoStore.isCommitable = true;
- RepoStore.openedFiles = [];
-
- const vm = createComponent();
-
- expect(vm.$el.innerHTML).toBeFalsy();
+ expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual('master');
});
describe('when submitting', () => {
- let el;
- let vm;
- const projectId = 'projectId';
- const commitMessage = 'commitMessage';
-
- beforeEach((done) => {
- RepoStore.isCommitable = true;
- RepoStore.currentBranch = branch;
- RepoStore.targetBranch = branch;
- RepoStore.openedFiles = openedFiles;
- RepoStore.projectId = projectId;
-
- // We need to append to body to get form `submit` events working
- // Otherwise we run into, "Form submission canceled because the form is not connected"
- // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
- el = document.createElement('div');
- document.body.appendChild(el);
-
- vm = createComponent(el);
- vm.commitMessage = commitMessage;
-
- spyOn(vm, 'tryCommit').and.callThrough();
- spyOn(vm, 'redirectToNewMr').and.stub();
- spyOn(vm, 'redirectToBranch').and.stub();
- spyOn(RepoService, 'commitFiles').and.returnValue(Promise.resolve());
- spyOn(RepoService, 'getBranch').and.returnValue(Promise.resolve({
- commit: {
- id: 1,
- short_id: 1,
- },
- }));
-
- // Wait for the vm data to be in place
- Vue.nextTick(() => {
- done();
- });
- });
+ let changedFiles;
- afterEach(() => {
- vm.$destroy();
- el.remove();
- RepoStore.openedFiles = [];
- });
+ beforeEach(() => {
+ vm.commitMessage = 'testing';
+ changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles));
- it('shows commit message', () => {
- const commitMessageEl = vm.$el.querySelector('#commit-message');
- expect(commitMessageEl.value).toBe(commitMessage);
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ short_id: '1',
+ stats: {},
+ }));
});
it('allows you to submit', () => {
- const submitCommit = vm.$refs.submitCommit;
- expect(submitCommit.disabled).toBeFalsy();
+ expect(vm.$el.querySelector('.btn').disabled).toBeTruthy();
});
- it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => {
- const submitCommit = vm.$refs.submitCommit;
- submitCommit.click();
+ it('submits commit', (done) => {
+ vm.makeCommit();
// Wait for the branch check to finish
getSetTimeoutPromise()
.then(() => Vue.nextTick())
.then(() => {
- expect(vm.tryCommit).toHaveBeenCalled();
- expect(submitCommit.querySelector('.js-commit-loading-icon')).toBeTruthy();
- expect(vm.redirectToBranch).toHaveBeenCalled();
-
- const args = RepoService.commitFiles.calls.allArgs()[0];
- const { commit_message, actions, branch: payloadBranch } = args[0];
+ const args = service.commit.calls.allArgs()[0];
+ const { commit_message, actions, branch: payloadBranch } = args[1];
- expect(commit_message).toBe(commitMessage);
+ expect(commit_message).toBe('testing');
expect(actions.length).toEqual(2);
- expect(payloadBranch).toEqual(branch);
+ expect(payloadBranch).toEqual('master');
expect(actions[0].action).toEqual('update');
expect(actions[1].action).toEqual('update');
- expect(actions[0].content).toEqual(openedFiles[0].newContent);
- expect(actions[1].content).toEqual(openedFiles[1].newContent);
- expect(actions[0].file_path).toEqual(openedFiles[0].path);
- expect(actions[1].file_path).toEqual(openedFiles[1].path);
+ expect(actions[0].content).toEqual(changedFiles[0].content);
+ expect(actions[1].content).toEqual(changedFiles[1].content);
+ expect(actions[0].file_path).toEqual(changedFiles[0].path);
+ expect(actions[1].file_path).toEqual(changedFiles[1].path);
})
.then(done)
.catch(done.fail);
});
it('redirects to MR creation page if start new MR checkbox checked', (done) => {
+ spyOn(gl.utils, 'visitUrl');
vm.startNewMR = true;
- Vue.nextTick()
- .then(() => {
- const submitCommit = vm.$refs.submitCommit;
- submitCommit.click();
- })
- // Wait for the branch check to finish
- .then(() => getSetTimeoutPromise())
+ vm.makeCommit();
+
+ getSetTimeoutPromise()
+ .then(() => Vue.nextTick())
.then(() => {
- expect(vm.redirectToNewMr).toHaveBeenCalled();
+ expect(gl.utils.visitUrl).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
-
- describe('methods', () => {
- describe('resetCommitState', () => {
- it('should reset store vars and scroll to top', () => {
- const vm = {
- submitCommitsLoading: true,
- changedFiles: new Array(10),
- openedFiles: new Array(3),
- commitMessage: 'commitMessage',
- editMode: true,
- };
-
- repoCommitSection.methods.resetCommitState.call(vm);
-
- expect(vm.submitCommitsLoading).toEqual(false);
- expect(vm.changedFiles).toEqual([]);
- expect(vm.commitMessage).toEqual('');
- expect(vm.editMode).toEqual(false);
- });
- });
- });
});
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
index dff2fac191d..44018464b35 100644
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -1,45 +1,83 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoEditButton from '~/repo/components/repo_edit_button.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoEditButton', () => {
- function createComponent() {
+ let vm;
+
+ beforeEach(() => {
+ const f = file();
const RepoEditButton = Vue.extend(repoEditButton);
- return new RepoEditButton().$mount();
- }
+ vm = new RepoEditButton({
+ store,
+ });
+
+ f.active = true;
+ vm.$store.dispatch('setInitialData', {
+ canCommit: true,
+ onTopOfBranch: true,
+ });
+ vm.$store.state.openFiles.push(f);
+ });
afterEach(() => {
- RepoStore.openedFiles = [];
+ vm.$destroy();
+
+ resetStore(vm.$store);
});
- it('renders an edit button that toggles the view state', (done) => {
- RepoStore.isCommitable = true;
- RepoStore.changedFiles = [];
- RepoStore.binary = false;
- RepoStore.openedFiles = [{}, {}];
+ it('renders an edit button', () => {
+ vm.$mount();
+
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
+ });
- const vm = createComponent();
+ it('renders edit button with cancel text', () => {
+ vm.$store.state.editMode = true;
- expect(vm.$el.tagName).toEqual('BUTTON');
- expect(vm.$el.textContent).toMatch('Edit');
+ vm.$mount();
- spyOn(vm, 'editCancelClicked').and.callThrough();
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
+ });
- vm.$el.click();
+ it('toggles edit mode on click', (done) => {
+ vm.$mount();
+
+ vm.$el.querySelector('.btn').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
- Vue.nextTick(() => {
- expect(vm.editCancelClicked).toHaveBeenCalled();
- expect(vm.$el.textContent).toMatch('Cancel edit');
done();
});
});
- it('does not render if not isCommitable', () => {
- RepoStore.isCommitable = false;
+ describe('discardPopupOpen', () => {
+ beforeEach(() => {
+ vm.$store.state.discardPopupOpen = true;
+ vm.$store.state.editMode = true;
+ vm.$store.state.openFiles[0].changed = true;
+
+ vm.$mount();
+ });
+
+ it('renders popup', () => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+ });
+
+ it('removes all changed files', (done) => {
+ vm.$el.querySelector('.btn-warning').click();
- const vm = createComponent();
+ vm.$nextTick(() => {
+ expect(vm.$store.getters.changedFiles.length).toBe(0);
+ expect(vm.$el.querySelector('.modal')).toBeNull();
- expect(vm.$el.innerHTML).toBeUndefined();
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index a25a600b3be..979d2185076 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,54 +1,56 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoEditor from '~/repo/components/repo_editor.vue';
+import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
+ let vm;
+
beforeEach(() => {
+ const f = file();
const RepoEditor = Vue.extend(repoEditor);
- this.vm = new RepoEditor().$mount();
+ vm = new RepoEditor({
+ store,
+ });
+
+ f.active = true;
+ f.tempFile = true;
+ vm.$store.state.openFiles.push(f);
+ vm.$store.getters.activeFile.html = 'testing';
+ vm.monaco = true;
+
+ vm.$mount();
});
afterEach(() => {
- RepoStore.openedFiles = [];
+ vm.$destroy();
+
+ resetStore(vm.$store);
});
it('renders an ide container', (done) => {
- this.vm.openedFiles = ['idiidid'];
- this.vm.binary = false;
-
Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(false);
- expect(this.vm.$el.id).toEqual('ide');
- expect(this.vm.$el.tagName).toBe('DIV');
+ expect(vm.shouldHideEditor).toBeFalsy();
+ expect(vm.$el.textContent.trim()).toBe('');
+
done();
});
});
- describe('when there are no open files', () => {
- it('does not render the ide', (done) => {
- this.vm.openedFiles = [];
+ describe('when open file is binary and not raw', () => {
+ beforeEach((done) => {
+ vm.$store.getters.activeFile.binary = true;
- Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(true);
- expect(this.vm.$el.tagName).not.toBeDefined();
- done();
- });
+ Vue.nextTick(done);
});
- });
- describe('when open file is binary and not raw', () => {
- it('does not render the IDE', (done) => {
- this.vm.binary = true;
- this.vm.activeFile = {
- raw: false,
- };
-
- Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(true);
- expect(this.vm.$el.tagName).not.toBeDefined();
- done();
- });
+ it('does not render the IDE', () => {
+ expect(vm.shouldHideEditor).toBeTruthy();
+ });
+
+ it('shows activeFile html', () => {
+ expect(vm.$el.textContent.trim()).toBe('testing');
});
});
});
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
index 111c83ee50d..d6e255e4810 100644
--- a/spec/javascripts/repo/components/repo_file_buttons_spec.js
+++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js
@@ -1,72 +1,49 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoFileButtons', () => {
- const activeFile = {
- extension: 'md',
- url: 'url',
- raw_path: 'raw_path',
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
- };
+ const activeFile = file();
+ let vm;
function createComponent() {
const RepoFileButtons = Vue.extend(repoFileButtons);
- return new RepoFileButtons().$mount();
+ activeFile.rawPath = 'test';
+ activeFile.blamePath = 'test';
+ activeFile.commitsPath = 'test';
+ activeFile.active = true;
+ store.state.openFiles.push(activeFile);
+
+ return new RepoFileButtons({
+ store,
+ }).$mount();
}
afterEach(() => {
- RepoStore.openedFiles = [];
- });
-
- it('renders Raw, Blame, History, Permalink and Preview toggle', () => {
- const activeFileLabel = 'activeFileLabel';
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
- RepoStore.activeFileLabel = activeFileLabel;
- RepoStore.editMode = true;
- RepoStore.binary = false;
+ vm.$destroy();
- const vm = createComponent();
- const raw = vm.$el.querySelector('.raw');
- const blame = vm.$el.querySelector('.blame');
- const history = vm.$el.querySelector('.history');
-
- expect(raw.href).toMatch(`/${activeFile.raw_path}`);
- expect(raw.textContent.trim()).toEqual('Raw');
- expect(blame.href).toMatch(`/${activeFile.blame_path}`);
- expect(blame.textContent.trim()).toEqual('Blame');
- expect(history.href).toMatch(`/${activeFile.commits_path}`);
- expect(history.textContent.trim()).toEqual('History');
- expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
- expect(vm.$el.querySelector('.preview').textContent.trim()).toEqual(activeFileLabel);
+ resetStore(vm.$store);
});
- it('triggers rawPreviewToggle on preview click', () => {
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
- RepoStore.editMode = true;
-
- const vm = createComponent();
- const preview = vm.$el.querySelector('.preview');
-
- spyOn(vm, 'rawPreviewToggle');
-
- preview.click();
-
- expect(vm.rawPreviewToggle).toHaveBeenCalled();
- });
+ it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => {
+ vm = createComponent();
- it('does not render preview toggle if not canPreview', () => {
- activeFile.extension = 'js';
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
+ vm.$nextTick(() => {
+ const raw = vm.$el.querySelector('.raw');
+ const blame = vm.$el.querySelector('.blame');
+ const history = vm.$el.querySelector('.history');
- const vm = createComponent();
+ expect(raw.href).toMatch(`/${activeFile.rawPath}`);
+ expect(raw.textContent.trim()).toEqual('Raw');
+ expect(blame.href).toMatch(`/${activeFile.blamePath}`);
+ expect(blame.textContent.trim()).toEqual('Blame');
+ expect(history.href).toMatch(`/${activeFile.commitsPath}`);
+ expect(history.textContent.trim()).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
- expect(vm.$el.querySelector('.preview')).toBeFalsy();
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index 8403df9be64..bf9181fb09c 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -1,32 +1,29 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoFile from '~/repo/components/repo_file.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import eventHub from '~/repo/event_hub';
-import { file } from '../mock_data';
+import { file, resetStore } from '../helpers';
describe('RepoFile', () => {
const updated = 'updated';
- const otherFile = {
- id: 'test',
- html: '<p class="file-content">html</p>',
- pageTitle: 'otherpageTitle',
- };
+ let vm;
function createComponent(propsData) {
const RepoFile = Vue.extend(repoFile);
return new RepoFile({
+ store,
propsData,
}).$mount();
}
- beforeEach(() => {
- RepoStore.openedFiles = [];
+ afterEach(() => {
+ resetStore(vm.$store);
});
- it('renders link, icon, name and last commit details', () => {
+ it('renders link, icon and name', () => {
const RepoFile = Vue.extend(repoFile);
- const vm = new RepoFile({
+ vm = new RepoFile({
+ store,
propsData: {
file: file(),
},
@@ -40,30 +37,23 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px');
expect(name.href).toMatch(`/${vm.file.url}`);
expect(name.textContent.trim()).toEqual(vm.file.name);
- expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(vm.file.lastCommit.message);
- expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated);
expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy();
expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`);
+ expect(vm.$el.querySelectorAll('.animation-container').length).toBe(2);
});
it('does render if hasFiles is true and is loading tree', () => {
- const vm = createComponent({
+ vm = createComponent({
file: file(),
});
expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
});
- it('sets the document title correctly', () => {
- RepoStore.setActiveFiles(otherFile);
-
- expect(document.title.trim()).toEqual(otherFile.pageTitle);
- });
-
it('renders a spinner if the file is loading', () => {
const f = file();
f.loading = true;
- const vm = createComponent({
+ vm = createComponent({
file: f,
});
@@ -71,32 +61,34 @@ describe('RepoFile', () => {
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', () => {
- RepoStore.openedFiles.push(file());
-
- const vm = createComponent({
+ it('does not render commit message and datetime if mini', (done) => {
+ vm = createComponent({
file: file(),
});
+ vm.$store.state.openFiles.push(vm.file);
- expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
- expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
+ expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
+
+ done();
+ });
});
- it('fires linkClicked when the link is clicked', () => {
- const vm = createComponent({
+ it('fires clickedTreeRow when the link is clicked', () => {
+ vm = createComponent({
file: file(),
});
- spyOn(vm, 'linkClicked');
+ spyOn(vm, 'clickedTreeRow');
vm.$el.click();
- expect(vm.linkClicked).toHaveBeenCalledWith(vm.file);
+ expect(vm.clickedTreeRow).toHaveBeenCalledWith(vm.file);
});
describe('submodule', () => {
let f;
- let vm;
beforeEach(() => {
f = file('submodule name', '123456789');
@@ -119,20 +111,4 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678');
});
});
-
- describe('methods', () => {
- describe('linkClicked', () => {
- it('$emits fileNameClicked with file obj', () => {
- spyOn(eventHub, '$emit');
-
- const vm = createComponent({
- file: file(),
- });
-
- vm.linkClicked(vm.file);
-
- expect(eventHub.$emit).toHaveBeenCalledWith('fileNameClicked', vm.file);
- });
- });
- });
});
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
index e9f95a02028..031f2a9c0b2 100644
--- a/spec/javascripts/repo/components/repo_loading_file_spec.js
+++ b/spec/javascripts/repo/components/repo_loading_file_spec.js
@@ -1,13 +1,16 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoLoadingFile from '~/repo/components/repo_loading_file.vue';
+import { resetStore } from '../helpers';
describe('RepoLoadingFile', () => {
- function createComponent(propsData) {
+ let vm;
+
+ function createComponent() {
const RepoLoadingFile = Vue.extend(repoLoadingFile);
return new RepoLoadingFile({
- propsData,
+ store,
}).$mount();
}
@@ -30,33 +33,30 @@ describe('RepoLoadingFile', () => {
}
afterEach(() => {
- RepoStore.openedFiles = [];
+ vm.$destroy();
+
+ resetStore(vm.$store);
});
it('renders 3 columns of animated LoC', () => {
- const vm = createComponent({
- loading: {
- tree: true,
- },
- hasFiles: false,
- });
+ vm = createComponent();
const columns = [...vm.$el.querySelectorAll('td')];
expect(columns.length).toEqual(3);
assertColumns(columns);
});
- it('renders 1 column of animated LoC if isMini', () => {
- RepoStore.openedFiles = new Array(1);
- const vm = createComponent({
- loading: {
- tree: true,
- },
- hasFiles: false,
- });
- const columns = [...vm.$el.querySelectorAll('td')];
+ it('renders 1 column of animated LoC if isMini', (done) => {
+ vm = createComponent();
+ vm.$store.state.openFiles.push('test');
- expect(columns.length).toEqual(1);
- assertColumns(columns);
+ vm.$nextTick(() => {
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(1);
+ assertColumns(columns);
+
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js
index 4c064f21084..7f82ae36a64 100644
--- a/spec/javascripts/repo/components/repo_prev_directory_spec.js
+++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js
@@ -1,47 +1,45 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue';
-import eventHub from '~/repo/event_hub';
+import { resetStore } from '../helpers';
describe('RepoPrevDirectory', () => {
- function createComponent(propsData) {
+ let vm;
+ const parentLink = 'parent';
+ function createComponent() {
const RepoPrevDirectory = Vue.extend(repoPrevDirectory);
- return new RepoPrevDirectory({
- propsData,
- }).$mount();
- }
-
- it('renders a prev dir link', () => {
- const prevUrl = 'prevUrl';
- const vm = createComponent({
- prevUrl,
+ const comp = new RepoPrevDirectory({
+ store,
});
- const link = vm.$el.querySelector('a');
- spyOn(vm, 'linkClicked');
+ comp.$store.state.parentTreeUrl = parentLink;
- expect(link.href).toMatch(`/${prevUrl}`);
- expect(link.textContent).toEqual('...');
+ return comp.$mount();
+ }
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
- link.click();
+ afterEach(() => {
+ vm.$destroy();
- expect(vm.linkClicked).toHaveBeenCalledWith(prevUrl);
+ resetStore(vm.$store);
});
- describe('methods', () => {
- describe('linkClicked', () => {
- it('$emits linkclicked with prevUrl', () => {
- const prevUrl = 'prevUrl';
- const vm = createComponent({
- prevUrl,
- });
+ it('renders a prev dir link', () => {
+ const link = vm.$el.querySelector('a');
- spyOn(eventHub, '$emit');
+ expect(link.href).toMatch(`/${parentLink}`);
+ expect(link.textContent).toEqual('...');
+ });
- vm.linkClicked(prevUrl);
+ it('clicking row triggers getTreeData', () => {
+ spyOn(vm, 'getTreeData');
- expect(eventHub.$emit).toHaveBeenCalledWith('goToPreviousDirectoryClicked', prevUrl);
- });
- });
+ vm.$el.querySelector('td').click();
+
+ expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink });
});
});
diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js
index 4920cf02083..8d1a87494cf 100644
--- a/spec/javascripts/repo/components/repo_preview_spec.js
+++ b/spec/javascripts/repo/components/repo_preview_spec.js
@@ -1,23 +1,37 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoPreview from '~/repo/components/repo_preview.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoPreview', () => {
+ let vm;
+
function createComponent() {
+ const f = file();
const RepoPreview = Vue.extend(repoPreview);
- return new RepoPreview().$mount();
+ const comp = new RepoPreview({
+ store,
+ });
+
+ f.active = true;
+ f.html = 'test';
+
+ comp.$store.state.openFiles.push(f);
+
+ return comp.$mount();
}
- it('renders a div with the activeFile html', () => {
- const activeFile = {
- html: '<p class="file-content">html</p>',
- };
- RepoStore.activeFile = activeFile;
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
- const vm = createComponent();
+ it('renders a div with the activeFile html', () => {
+ vm = createComponent();
expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.innerHTML).toContain(activeFile.html);
+ expect(vm.$el.innerHTML).toContain('test');
});
});
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index 148f275e03d..7cb4dace491 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -1,32 +1,31 @@
import Vue from 'vue';
-import Helper from '~/repo/helpers/repo_helper';
-import RepoService from '~/repo/services/repo_service';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoSidebar from '~/repo/components/repo_sidebar.vue';
-import { file } from '../mock_data';
+import { file, resetStore } from '../helpers';
describe('RepoSidebar', () => {
let vm;
- function createComponent() {
+ beforeEach(() => {
const RepoSidebar = Vue.extend(repoSidebar);
- return new RepoSidebar().$mount();
- }
+ vm = new RepoSidebar({
+ store,
+ });
+
+ vm.$store.state.isRoot = true;
+ vm.$store.state.tree.push(file());
+
+ vm.$mount();
+ });
afterEach(() => {
vm.$destroy();
- RepoStore.files = [];
- RepoStore.openedFiles = [];
+ resetStore(vm.$store);
});
it('renders a sidebar', () => {
- RepoStore.files = [file()];
- RepoStore.openedFiles = [];
- RepoStore.isRoot = true;
-
- vm = createComponent();
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
@@ -41,139 +40,36 @@ describe('RepoSidebar', () => {
expect(tbody.querySelector('.file')).toBeTruthy();
});
- it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', () => {
- RepoStore.openedFiles = [{
- id: 0,
- }];
- vm = createComponent();
-
- expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
- expect(vm.$el.querySelector('thead')).toBeTruthy();
- expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy();
- });
-
- it('renders 5 loading files if tree is loading and not hasFiles', () => {
- RepoStore.loading.tree = true;
- RepoStore.files = [];
- vm = createComponent();
+ it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', (done) => {
+ vm.$store.state.openFiles.push(vm.$store.state.tree[0]);
- expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
- });
-
- it('renders a prev directory if is not root', () => {
- RepoStore.files = [file()];
- RepoStore.isRoot = false;
- RepoStore.loading.tree = false;
- vm = createComponent();
-
- expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
- });
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
+ expect(vm.$el.querySelector('thead')).toBeTruthy();
+ expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy();
- describe('flattendFiles', () => {
- it('returns a flattend array of files', () => {
- const f = file();
- f.files.push(file('testing 123'));
- const files = [f, file()];
- vm = createComponent();
- vm.files = files;
-
- expect(vm.flattendFiles.length).toBe(3);
- expect(vm.flattendFiles[1].name).toBe('testing 123');
+ done();
});
});
- describe('methods', () => {
- describe('fileClicked', () => {
- it('should fetch data for new file', () => {
- spyOn(Helper, 'getContent').and.callThrough();
- RepoStore.files = [file()];
- RepoStore.isRoot = true;
- vm = createComponent();
-
- vm.fileClicked(RepoStore.files[0]);
-
- expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[0]);
- });
-
- it('should not fetch data for already opened files', () => {
- const f = file();
- spyOn(Helper, 'getFileFromPath').and.returnValue(f);
- spyOn(RepoStore, 'setActiveFiles');
- vm = createComponent();
- vm.fileClicked(f);
+ it('renders 5 loading files if tree is loading', (done) => {
+ vm.$store.state.tree = [];
+ vm.$store.state.loading = true;
- expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(f);
- });
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
- it('should hide files in directory if already open', () => {
- spyOn(Helper, 'setDirectoryToClosed').and.callThrough();
- const f = file();
- f.opened = true;
- f.type = 'tree';
- RepoStore.files = [f];
- vm = createComponent();
-
- vm.fileClicked(RepoStore.files[0]);
-
- expect(Helper.setDirectoryToClosed).toHaveBeenCalledWith(RepoStore.files[0]);
- });
-
- describe('submodule', () => {
- it('opens submodule project URL', () => {
- spyOn(gl.utils, 'visitUrl');
-
- const f = file();
- f.type = 'submodule';
-
- vm = createComponent();
-
- vm.fileClicked(f);
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('url');
- });
- });
- });
-
- describe('goToPreviousDirectoryClicked', () => {
- it('should hide files in directory if already open', () => {
- const prevUrl = 'foo/bar';
- vm = createComponent();
-
- vm.goToPreviousDirectoryClicked(prevUrl);
-
- expect(RepoService.url).toEqual(prevUrl);
- });
+ done();
});
+ });
- describe('back button', () => {
- beforeEach(() => {
- const f = file();
- const file2 = Object.assign({}, file());
- file2.url = 'test';
- RepoStore.files = [f, file2];
- RepoStore.openedFiles = [];
- RepoStore.isRoot = true;
-
- vm = createComponent();
- });
-
- it('render previous file when using back button', () => {
- spyOn(Helper, 'getContent').and.callThrough();
-
- vm.fileClicked(RepoStore.files[1]);
- expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[1]);
-
- history.pushState({
- key: Math.random(),
- }, '', RepoStore.files[1].url);
- const popEvent = document.createEvent('Event');
- popEvent.initEvent('popstate', true, true);
- window.dispatchEvent(popEvent);
+ it('renders a prev directory if is not root', (done) => {
+ vm.$store.state.isRoot = false;
- expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(RepoStore.files[1].url);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
- window.history.pushState({}, null, '/');
- });
+ done();
});
});
});
diff --git a/spec/javascripts/repo/components/repo_spec.js b/spec/javascripts/repo/components/repo_spec.js
index 3558a155728..b32d2c13af8 100644
--- a/spec/javascripts/repo/components/repo_spec.js
+++ b/spec/javascripts/repo/components/repo_spec.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repo from '~/repo/components/repo.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import Service from '~/repo/services/repo_service';
-import eventHub from '~/repo/event_hub';
-import createComponent from '../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
describe('repo component', () => {
let vm;
@@ -11,86 +10,26 @@ describe('repo component', () => {
beforeEach(() => {
const Component = Vue.extend(repo);
- RepoStore.currentBranch = 'master';
-
- vm = createComponent(Component);
+ vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
- RepoStore.currentBranch = '';
+ resetStore(vm.$store);
});
- describe('createNewBranch', () => {
- beforeEach(() => {
- spyOn(history, 'pushState');
- });
-
- describe('success', () => {
- beforeEach(() => {
- spyOn(Service, 'createBranch').and.returnValue(Promise.resolve({
- data: {
- name: 'test',
- },
- }));
- });
-
- it('calls createBranch with branchName', () => {
- eventHub.$emit('createNewBranch', 'test');
-
- expect(Service.createBranch).toHaveBeenCalledWith({
- branch: 'test',
- ref: RepoStore.currentBranch,
- });
- });
-
- it('pushes new history state', (done) => {
- RepoStore.currentBranch = 'master';
-
- spyOn(vm, 'getCurrentLocation').and.returnValue('http://test.com/master');
-
- eventHub.$emit('createNewBranch', 'test');
-
- setTimeout(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'http://test.com/test');
- done();
- });
- });
-
- it('updates stores currentBranch', (done) => {
- eventHub.$emit('createNewBranch', 'test');
-
- setTimeout(() => {
- expect(RepoStore.currentBranch).toBe('test');
-
- done();
- });
- });
- });
-
- describe('failure', () => {
- beforeEach(() => {
- spyOn(Service, 'createBranch').and.returnValue(Promise.reject({
- response: {
- data: {
- message: 'test',
- },
- },
- }));
- });
-
- it('emits createNewBranchError event', (done) => {
- spyOn(eventHub, '$emit').and.callThrough();
+ it('does not render panel right when no files open', () => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ });
- eventHub.$emit('createNewBranch', 'test');
+ it('renders panel right when files are open', (done) => {
+ vm.$store.state.tree.push(file());
- setTimeout(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranchError', 'test');
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
- done();
- });
- });
+ done();
});
});
});
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
index 37e297437f0..df0ca55aafc 100644
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -1,47 +1,64 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoTab from '~/repo/components/repo_tab.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoTab', () => {
+ let vm;
+
function createComponent(propsData) {
const RepoTab = Vue.extend(repoTab);
return new RepoTab({
+ store,
propsData,
}).$mount();
}
+ afterEach(() => {
+ resetStore(vm.$store);
+ });
+
it('renders a close link and a name link', () => {
- const tab = {
- url: 'url',
- name: 'name',
- };
- const vm = createComponent({
- tab,
+ vm = createComponent({
+ tab: file(),
});
+ vm.$store.state.openFiles.push(vm.tab);
const close = vm.$el.querySelector('.close-btn');
- const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
-
- spyOn(vm, 'closeTab');
- spyOn(vm, 'tabClicked');
+ const name = vm.$el.querySelector(`a[title="${vm.tab.url}"]`);
expect(close.querySelector('.fa-times')).toBeTruthy();
- expect(name.textContent.trim()).toEqual(tab.name);
+ expect(name.textContent.trim()).toEqual(vm.tab.name);
+ });
- close.click();
- name.click();
+ it('calls setFileActive when clicking tab', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'setFileActive');
+
+ vm.$el.click();
- expect(vm.closeTab).toHaveBeenCalledWith(tab);
- expect(vm.tabClicked).toHaveBeenCalledWith(tab);
+ expect(vm.setFileActive).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('calls closeFile when clicking close button', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'closeFile');
+
+ vm.$el.querySelector('.close-btn').click();
+
+ expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab });
});
it('renders an fa-circle icon if tab is changed', () => {
- const tab = {
- url: 'url',
- name: 'name',
- changed: true,
- };
- const vm = createComponent({
+ const tab = file();
+ tab.changed = true;
+ vm = createComponent({
tab,
});
@@ -50,38 +67,41 @@ describe('RepoTab', () => {
describe('methods', () => {
describe('closeTab', () => {
- it('returns undefined and does not $emit if file is changed', () => {
- const tab = {
- url: 'url',
- name: 'name',
- changed: true,
- };
- const vm = createComponent({
+ it('does not close tab if is changed', (done) => {
+ const tab = file();
+ tab.changed = true;
+ tab.opened = true;
+ vm = createComponent({
tab,
});
-
- spyOn(RepoStore, 'removeFromOpenedFiles');
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.dispatch('setFileActive', tab);
vm.$el.querySelector('.close-btn').click();
- expect(RepoStore.removeFromOpenedFiles).not.toHaveBeenCalled();
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeTruthy();
+
+ done();
+ });
});
- it('$emits tabclosed event with file obj', () => {
- const tab = {
- url: 'url',
- name: 'name',
- changed: false,
- };
- const vm = createComponent({
+ it('closes tab when clicking close btn', (done) => {
+ const tab = file('lose');
+ tab.opened = true;
+ vm = createComponent({
tab,
});
-
- spyOn(RepoStore, 'removeFromOpenedFiles');
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.dispatch('setFileActive', tab);
vm.$el.querySelector('.close-btn').click();
- expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(tab);
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
index 431129bc866..d0246cc72e6 100644
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -1,35 +1,38 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoTabs from '~/repo/components/repo_tabs.vue';
+import { file, resetStore } from '../helpers';
describe('RepoTabs', () => {
- const openedFiles = [{
- id: 0,
- active: true,
- }, {
- id: 1,
- }];
+ const openedFiles = [file(), file()];
+ let vm;
function createComponent() {
const RepoTabs = Vue.extend(repoTabs);
- return new RepoTabs().$mount();
+ return new RepoTabs({
+ store,
+ }).$mount();
}
afterEach(() => {
- RepoStore.openedFiles = [];
+ resetStore(vm.$store);
});
- it('renders a list of tabs', () => {
- RepoStore.openedFiles = openedFiles;
+ it('renders a list of tabs', (done) => {
+ vm = createComponent();
+ openedFiles[0].active = true;
+ vm.$store.state.openFiles = openedFiles;
- const vm = createComponent();
- const tabs = [...vm.$el.querySelectorAll(':scope > li')];
+ vm.$nextTick(() => {
+ const tabs = [...vm.$el.querySelectorAll(':scope > li')];
- expect(vm.$el.id).toEqual('tabs');
- expect(tabs.length).toEqual(3);
- expect(tabs[0].classList.contains('active')).toBeTruthy();
- expect(tabs[1].classList.contains('active')).toBeFalsy();
- expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
+ expect(tabs.length).toEqual(3);
+ expect(tabs[0].classList.contains('active')).toBeTruthy();
+ expect(tabs[1].classList.contains('active')).toBeFalsy();
+ expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
+
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js
new file mode 100644
index 00000000000..820a44992b4
--- /dev/null
+++ b/spec/javascripts/repo/helpers.js
@@ -0,0 +1,15 @@
+import { decorateData } from '~/repo/stores/utils';
+import state from '~/repo/stores/state';
+
+export const resetStore = (store) => {
+ store.replaceState(state());
+};
+
+export const file = (name = 'name', id = name, type = '') => decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: name,
+});
diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js
deleted file mode 100644
index 71e275caf09..00000000000
--- a/spec/javascripts/repo/mock_data.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import RepoHelper from '~/repo/helpers/repo_helper';
-
-// eslint-disable-next-line import/prefer-default-export
-export const file = (name = 'name', id = name) => RepoHelper.serializeRepoEntity('blob', {
- id,
- icon: 'icon',
- url: 'url',
- name,
- last_commit: {
- id: '123',
- message: 'test',
- committed_date: new Date().toISOString(),
- },
-});
diff --git a/spec/javascripts/repo/services/repo_service_spec.js b/spec/javascripts/repo/services/repo_service_spec.js
deleted file mode 100644
index 6f530770525..00000000000
--- a/spec/javascripts/repo/services/repo_service_spec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import axios from 'axios';
-import RepoService from '~/repo/services/repo_service';
-import RepoStore from '~/repo/stores/repo_store';
-import Api from '~/api';
-
-describe('RepoService', () => {
- it('has default json format param', () => {
- expect(RepoService.options.params.format).toBe('json');
- });
-
- describe('buildParams', () => {
- let newParams;
- const url = 'url';
-
- beforeEach(() => {
- newParams = {};
-
- spyOn(Object, 'assign').and.returnValue(newParams);
- });
-
- it('clones params', () => {
- const params = RepoService.buildParams(url);
-
- expect(Object.assign).toHaveBeenCalledWith({}, RepoService.options.params);
-
- expect(params).toBe(newParams);
- });
-
- it('sets and returns viewer params to richif urlIsRichBlob is true', () => {
- spyOn(RepoService, 'urlIsRichBlob').and.returnValue(true);
-
- const params = RepoService.buildParams(url);
-
- expect(params.viewer).toEqual('rich');
- });
-
- it('returns params urlIsRichBlob is false', () => {
- spyOn(RepoService, 'urlIsRichBlob').and.returnValue(false);
-
- const params = RepoService.buildParams(url);
-
- expect(params.viewer).toBeUndefined();
- });
-
- it('calls urlIsRichBlob with the objects url prop if no url arg is provided', () => {
- spyOn(RepoService, 'urlIsRichBlob');
- RepoService.url = url;
-
- RepoService.buildParams();
-
- expect(RepoService.urlIsRichBlob).toHaveBeenCalledWith(url);
- });
- });
-
- describe('urlIsRichBlob', () => {
- it('returns true for md extension', () => {
- const isRichBlob = RepoService.urlIsRichBlob('url.md');
-
- expect(isRichBlob).toBeTruthy();
- });
-
- it('returns false for js extension', () => {
- const isRichBlob = RepoService.urlIsRichBlob('url.js');
-
- expect(isRichBlob).toBeFalsy();
- });
- });
-
- describe('getContent', () => {
- const params = {};
- const url = 'url';
- const requestPromise = Promise.resolve();
-
- beforeEach(() => {
- spyOn(RepoService, 'buildParams').and.returnValue(params);
- spyOn(axios, 'get').and.returnValue(requestPromise);
- });
-
- it('calls buildParams and axios.get', () => {
- const request = RepoService.getContent(url);
-
- expect(RepoService.buildParams).toHaveBeenCalledWith(url);
- expect(axios.get).toHaveBeenCalledWith(url, {
- params,
- });
- expect(request).toBe(requestPromise);
- });
-
- it('uses object url prop if no url arg is provided', () => {
- RepoService.url = url;
-
- RepoService.getContent();
-
- expect(axios.get).toHaveBeenCalledWith(url, {
- params,
- });
- });
- });
-
- describe('getBase64Content', () => {
- const url = 'url';
- const response = { data: 'data' };
-
- beforeEach(() => {
- spyOn(RepoService, 'bufferToBase64');
- spyOn(axios, 'get').and.returnValue(Promise.resolve(response));
- });
-
- it('calls axios.get and bufferToBase64 on completion', (done) => {
- const request = RepoService.getBase64Content(url);
-
- expect(axios.get).toHaveBeenCalledWith(url, {
- responseType: 'arraybuffer',
- });
- expect(request).toEqual(jasmine.any(Promise));
-
- request.then(() => {
- expect(RepoService.bufferToBase64).toHaveBeenCalledWith(response.data);
- done();
- }).catch(done.fail);
- });
- });
-
- describe('commitFiles', () => {
- it('calls commitMultiple and .then commitFlash', (done) => {
- const projectId = 'projectId';
- const payload = {};
- RepoStore.projectId = projectId;
-
- spyOn(Api, 'commitMultiple').and.returnValue(Promise.resolve());
- spyOn(RepoService, 'commitFlash');
-
- const apiPromise = RepoService.commitFiles(payload);
-
- expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, payload);
-
- apiPromise.then(() => {
- expect(RepoService.commitFlash).toHaveBeenCalled();
- done();
- }).catch(done.fail);
- });
- });
-
- describe('commitFlash', () => {
- it('calls Flash with data.message', () => {
- const data = {
- message: 'message',
- };
- spyOn(window, 'Flash');
-
- RepoService.commitFlash(data);
-
- expect(window.Flash).toHaveBeenCalledWith(data.message);
- });
-
- it('calls Flash with success string if short_id and stats', () => {
- const data = {
- short_id: 'short_id',
- stats: {
- additions: '4',
- deletions: '5',
- },
- };
- spyOn(window, 'Flash');
-
- RepoService.commitFlash(data);
-
- expect(window.Flash).toHaveBeenCalledWith(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js
new file mode 100644
index 00000000000..af9d6835a67
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/branch_spec.js
@@ -0,0 +1,38 @@
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { resetStore } from '../../helpers';
+
+describe('Multi-file store branch actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('createNewBranch', () => {
+ beforeEach(() => {
+ spyOn(service, 'createBranch').and.returnValue(Promise.resolve({
+ json: () => ({
+ name: 'testing',
+ }),
+ }));
+ spyOn(history, 'pushState');
+
+ store.state.project.id = 2;
+ store.state.currentBranch = 'testing';
+ });
+
+ it('creates new branch', (done) => {
+ store.dispatch('createNewBranch', 'master')
+ .then(() => {
+ expect(store.state.currentBranch).toBe('testing');
+ expect(service.createBranch).toHaveBeenCalledWith(2, {
+ branch: 'master',
+ ref: 'testing',
+ });
+ expect(history.pushState).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js
new file mode 100644
index 00000000000..099c0556e71
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/file_spec.js
@@ -0,0 +1,417 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store file actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('closeFile', () => {
+ let localFile;
+ let getLastCommitDataSpy;
+ let oldGetLastCommitData;
+
+ beforeEach(() => {
+ getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
+ oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
+ store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
+
+ localFile = file();
+ localFile.active = true;
+ localFile.opened = true;
+ localFile.parentTreeUrl = 'parentTreeUrl';
+
+ store.state.openFiles.push(localFile);
+
+ spyOn(history, 'pushState');
+ });
+
+ afterEach(() => {
+ store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
+ });
+
+ it('closes open files', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not close file if has changed', (done) => {
+ localFile.changed = true;
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localFile.active).toBeTruthy();
+ expect(store.state.openFiles.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not close file if temp file', (done) => {
+ localFile.tempFile = true;
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localFile.active).toBeTruthy();
+ expect(store.state.openFiles.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('force closes a changed file', (done) => {
+ localFile.changed = true;
+
+ store.dispatch('closeFile', { file: localFile, force: true })
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls pushState when no open files are left', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets next file as active', (done) => {
+ const f = file();
+ store.state.openFiles.push(f);
+
+ expect(f.active).toBeFalsy();
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getLastCommitData', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(getLastCommitDataSpy).toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('setFileActive', () => {
+ let scrollToTabSpy;
+ let oldScrollToTab;
+
+ beforeEach(() => {
+ scrollToTabSpy = jasmine.createSpy('scrollToTab');
+ oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+ store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+ });
+
+ afterEach(() => {
+ store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+ });
+
+ it('calls scrollToTab', (done) => {
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(scrollToTabSpy).toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file active', (done) => {
+ const localFile = file();
+
+ store.dispatch('setFileActive', localFile)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('returns early if file is already active', (done) => {
+ const localFile = file();
+ localFile.active = true;
+
+ store.dispatch('setFileActive', localFile)
+ .then(() => {
+ expect(scrollToTabSpy).not.toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets current active file to not active', (done) => {
+ const localFile = file();
+ localFile.active = true;
+ store.state.openFiles.push(localFile);
+
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('resets location.hash for line highlighting', (done) => {
+ location.hash = 'test';
+
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(location.hash).not.toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getFileData', () => {
+ let localFile = file();
+
+ beforeEach(() => {
+ spyOn(service, 'getFileData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'testing getFileData',
+ },
+ json: () => Promise.resolve({
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ }),
+ }));
+
+ localFile = file();
+ localFile.url = 'getFileDataURL';
+ });
+
+ it('calls the service', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file data', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets document title', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file as active', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds the file to open files', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles the file loading', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.loading).toBeTruthy();
+
+ return Vue.nextTick();
+ })
+ .then(() => {
+ expect(localFile.loading).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getRawFileData', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+
+ tmpFile = file();
+ });
+
+ it('calls getRawFileData service method', (done) => {
+ store.dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates file raw data', (done) => {
+ store.dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('changeFileContent', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ tmpFile = file();
+ });
+
+ it('updates file content', (done) => {
+ store.dispatch('changeFileContent', {
+ file: tmpFile,
+ content: 'content',
+ })
+ .then(() => {
+ expect(tmpFile.content).toBe('content');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('createTempFile', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"></div>';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ it('creates temp file', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.tempFile).toBeTruthy();
+ expect(store.state.tree.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds tmp file to open files', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(f.name);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets tmp file as active', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('enters edit mode if file is not base64', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then(() => {
+ expect(store.state.editMode).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not enter edit mode if file is base64', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ base64: true,
+ }).then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates flash message is file already exists', (done) => {
+ store.state.tree.push(file('test', '1', 'blob'));
+
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then(() => {
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('increases level of file', (done) => {
+ store.state.level = 1;
+
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.level).toBe(2);
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js
new file mode 100644
index 00000000000..393a797c6a3
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/tree_spec.js
@@ -0,0 +1,469 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store tree actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('getTreeData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () => Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }));
+ spyOn(history, 'pushState');
+
+ Object.assign(store.state.endpoints, {
+ rootEndpoint: 'rootEndpoint',
+ });
+ });
+
+ it('calls service getTreeData', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds data into tree', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree.length).toBe(3);
+ expect(store.state.tree[0].type).toBe('tree');
+ expect(store.state.tree[1].type).toBe('submodule');
+ expect(store.state.tree[2].type).toBe('blob');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets parent tree URL', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.parentTreeUrl).toBe('parent_tree_url');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets last commit path', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.lastCommitPath).toBe('last_commit_path');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets root if not currently at root', (done) => {
+ store.state.isInitialRoot = false;
+
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.isInitialRoot).toBeTruthy();
+ expect(store.state.isRoot).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets page title', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(document.title).toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles loading', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(store.state.loading).toBeTruthy();
+
+ return Vue.nextTick();
+ })
+ .then(() => {
+ expect(store.state.loading).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls pushState with endpoint', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
+ const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
+ const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
+ store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
+ store.state.prevLastCommitPath = 'test';
+
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state);
+
+ store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('toggleTreeOpen', () => {
+ let oldGetTreeData;
+ let getTreeDataSpy;
+ let tree;
+
+ beforeEach(() => {
+ getTreeDataSpy = jasmine.createSpy('getTreeData');
+
+ oldGetTreeData = store._actions.getTreeData; // eslint-disable-line
+ store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
+
+ tree = {
+ opened: false,
+ tree: [],
+ };
+ });
+
+ afterEach(() => {
+ store._actions.getTreeData = oldGetTreeData; // eslint-disable-line
+ });
+
+ it('toggles the tree open', (done) => {
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(tree.opened).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getTreeData if tree is closed', (done) => {
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(getTreeDataSpy).toHaveBeenCalledWith({
+ endpoint: 'test',
+ tree,
+ });
+ expect(store.state.previousUrl).toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('resets entries tree', (done) => {
+ Object.assign(tree, {
+ opened: true,
+ tree: ['a'],
+ });
+
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(tree.tree.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('pushes new state', (done) => {
+ spyOn(history, 'pushState');
+ Object.assign(tree, {
+ opened: true,
+ parentTreeUrl: 'testing',
+ });
+
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('clickedTreeRow', () => {
+ describe('tree', () => {
+ let toggleTreeOpenSpy;
+ let oldToggleTreeOpen;
+
+ beforeEach(() => {
+ toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen');
+
+ oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line
+ store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line
+ });
+
+ afterEach(() => {
+ store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line
+ });
+
+ it('opens tree', (done) => {
+ const tree = {
+ url: 'a',
+ type: 'tree',
+ };
+
+ store.dispatch('clickedTreeRow', tree)
+ .then(() => {
+ expect(toggleTreeOpenSpy).toHaveBeenCalledWith({
+ endpoint: tree.url,
+ tree,
+ });
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('submodule', () => {
+ let row;
+
+ beforeEach(() => {
+ spyOn(gl.utils, 'visitUrl');
+
+ row = {
+ url: 'submoduleurl',
+ type: 'submodule',
+ loading: false,
+ };
+ });
+
+ it('toggles loading for row', (done) => {
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(row.loading).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('opens submodule URL', (done) => {
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('blob', () => {
+ let row;
+
+ beforeEach(() => {
+ row = {
+ type: 'blob',
+ opened: false,
+ };
+ });
+
+ it('calls getFileData', (done) => {
+ const getFileDataSpy = jasmine.createSpy('getFileData');
+ const oldGetFileData = store._actions.getFileData; // eslint-disable-line
+ store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line
+
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(getFileDataSpy).toHaveBeenCalledWith(row);
+
+ store._actions.getFileData = oldGetFileData; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls setFileActive when file is opened', (done) => {
+ const setFileActiveSpy = jasmine.createSpy('setFileActive');
+ const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line
+ store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line
+
+ row.opened = true;
+
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(setFileActiveSpy).toHaveBeenCalledWith(row);
+
+ store._actions.setFileActive = oldSetFileActive; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+ });
+
+ describe('createTempTree', () => {
+ it('creates temp tree', (done) => {
+ store.dispatch('createTempTree', 'test')
+ .then(() => {
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].name).toBe('test');
+ expect(store.state.tree[0].type).toBe('tree');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates .gitkeep file in temp tree', (done) => {
+ store.dispatch('createTempTree', 'test')
+ .then(() => {
+ expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].tree[0].name).toBe('.gitkeep');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates new folder inside another tree', (done) => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ tree: [],
+ };
+
+ store.state.tree.push(tree);
+
+ store.dispatch('createTempTree', 'testing/test')
+ .then(() => {
+ expect(store.state.tree[0].name).toBe('testing');
+ expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].tree[0].name).toBe('test');
+ expect(store.state.tree[0].tree[0].type).toBe('tree');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not create new tree if already exists', (done) => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ tree: [],
+ };
+
+ store.state.tree.push(tree);
+
+ store.dispatch('createTempTree', 'testing/test')
+ .then(() => {
+ expect(store.state.tree[0].name).toBe('testing');
+ expect(store.state.tree[0].tempFile).toBeUndefined();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getLastCommitData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({
+ headers: {
+ 'more-logs-url': null,
+ },
+ json: () => Promise.resolve([{
+ type: 'tree',
+ file_name: 'testing',
+ commit: {
+ message: 'commit message',
+ authored_date: '123',
+ },
+ }]),
+ }));
+
+ store.state.tree.push(file('testing', '1', 'tree'));
+ store.state.lastCommitPath = 'lastcommitpath';
+ });
+
+ it('calls service with lastCommitPath', (done) => {
+ store.dispatch('getLastCommitData')
+ .then(() => {
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates trees last commit data', (done) => {
+ store.dispatch('getLastCommitData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree[0].lastCommit.message).toBe('commit message');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not update entry if not found', (done) => {
+ store.state.tree[0].name = 'a';
+
+ store.dispatch('getLastCommitData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree[0].lastCommit.message).not.toBe('commit message');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('updateDirectoryData', () => {
+ it('adds data into tree', (done) => {
+ const tree = {
+ tree: [],
+ };
+ const data = {
+ trees: [{ name: 'tree' }],
+ submodules: [{ name: 'submodule' }],
+ blobs: [{ name: 'blob' }],
+ };
+
+ store.dispatch('updateDirectoryData', {
+ data,
+ tree,
+ }).then(() => {
+ expect(tree.tree[0].name).toBe('tree');
+ expect(tree.tree[0].type).toBe('tree');
+ expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[1].type).toBe('submodule');
+ expect(tree.tree[2].name).toBe('blob');
+ expect(tree.tree[2].type).toBe('blob');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
new file mode 100644
index 00000000000..f2a7a698912
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -0,0 +1,419 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { resetStore, file } from '../helpers';
+
+describe('Multi-file store actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('redirectToUrl', () => {
+ it('calls visitUrl', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+
+ store.dispatch('redirectToUrl', 'test')
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setInitialData', () => {
+ it('commits initial data', (done) => {
+ store.dispatch('setInitialData', { canCommit: true })
+ .then(() => {
+ expect(store.state.canCommit).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('closeDiscardPopup', () => {
+ it('closes the discard popup', (done) => {
+ store.dispatch('closeDiscardPopup', false)
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardAllChanges', () => {
+ beforeEach(() => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+ });
+ });
+
+ describe('closeAllFiles', () => {
+ beforeEach(() => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].opened = true;
+ });
+
+ it('closes all open files', (done) => {
+ store.dispatch('closeAllFiles')
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('toggleEditMode', () => {
+ it('toggles edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('toggleEditMode')
+ .then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets preview mode', (done) => {
+ store.state.currentBlobView = 'repo-editor';
+ store.state.editMode = true;
+
+ store.dispatch('toggleEditMode')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-preview');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('opens discard popup if there are changed files', (done) => {
+ store.state.editMode = true;
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+
+ store.dispatch('toggleEditMode')
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('can force closed if there are changed files', (done) => {
+ store.state.editMode = true;
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+
+ store.dispatch('toggleEditMode', true)
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeFalsy();
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('discards file changes', (done) => {
+ const f = file();
+ store.state.editMode = true;
+ store.state.tree.push(f);
+ store.state.openFiles.push(f);
+ f.changed = true;
+
+ store.dispatch('toggleEditMode', true)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(f.changed).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('toggleBlobView', () => {
+ it('sets edit mode view if in edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('toggleBlobView')
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-editor');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets preview mode view if not in edit mode', (done) => {
+ store.dispatch('toggleBlobView')
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-preview');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('checkCommitStatus', () => {
+ beforeEach(() => {
+ store.state.project.id = 2;
+ store.state.currentBranch = 'master';
+ store.state.currentRef = '1';
+ });
+
+ it('calls service', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '123' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith(2, 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns true if current ref does not equal returned ID', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '123' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then((val) => {
+ expect(val).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns false if current ref equals returned ID', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '1' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then((val) => {
+ expect(val).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('commitChanges', () => {
+ let payload;
+
+ beforeEach(() => {
+ spyOn(window, 'scrollTo');
+
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.project.id = 123;
+ payload = {
+ branch: 'master',
+ };
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ }));
+ });
+
+ it('calls service', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith(123, payload);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('shows flash notice', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.querySelector('.flash-notice')).not.toBeNull();
+ expect(alert.textContent.trim()).toBe(
+ 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.',
+ );
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds commit data to changed files', (done) => {
+ const changedFile = file();
+ const f = file();
+ changedFile.changed = true;
+
+ store.state.openFiles.push(changedFile, f);
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(changedFile.lastCommit.message).toBe('test message');
+ expect(f.lastCommit.message).not.toBe('test message');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('closes all files', (done) => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].opened = true;
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('scrolls to top of page', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates commit ref', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(store.state.currentRef).toBe('123456');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('redirects to new merge request page', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+
+ store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch=';
+
+ store.dispatch('commitChanges', { payload, newMr: true })
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ message: 'failed message',
+ }));
+ });
+
+ it('shows failed message', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.textContent.trim()).toBe(
+ 'failed message',
+ );
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+ });
+
+ describe('createTempEntry', () => {
+ it('creates a temp tree', (done) => {
+ store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(store.state.tree.length).toBe(1);
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates temp file', (done) => {
+ store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'blob',
+ })
+ .then(() => {
+ expect(store.state.tree.length).toBe(1);
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].type).toBe('blob');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('popHistoryState', () => {
+
+ });
+
+ describe('scrollToTab', () => {
+ it('focuses the current active element', (done) => {
+ document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
+ const el = document.querySelector('.repo-tab');
+ spyOn(el, 'focus');
+
+ store.dispatch('scrollToTab')
+ .then(() => {
+ setTimeout(() => {
+ expect(el.focus).toHaveBeenCalled();
+
+ document.getElementById('tabs').remove();
+
+ done();
+ });
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js
new file mode 100644
index 00000000000..a204b2386cd
--- /dev/null
+++ b/spec/javascripts/repo/stores/getters_spec.js
@@ -0,0 +1,119 @@
+import * as getters from '~/repo/stores/getters';
+import state from '~/repo/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('treeList', () => {
+ it('returns flat tree list', () => {
+ localState.tree.push(file('1'));
+ localState.tree[0].tree.push(file('2'));
+ localState.tree[0].tree[0].tree.push(file('3'));
+
+ const treeList = getters.treeList(localState);
+
+ expect(treeList.length).toBe(3);
+ expect(treeList[1].name).toBe(localState.tree[0].tree[0].name);
+ expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name);
+ });
+ });
+
+ describe('changedFiles', () => {
+ it('returns a list of changed opened files', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('changed'));
+ localState.openFiles[1].changed = true;
+
+ const changedFiles = getters.changedFiles(localState);
+
+ expect(changedFiles.length).toBe(1);
+ expect(changedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('activeFile', () => {
+ it('returns the current active file', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+ localState.openFiles[1].active = true;
+
+ expect(getters.activeFile(localState).name).toBe('active');
+ });
+
+ it('returns undefined if no active files are found', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+
+ expect(getters.activeFile(localState)).toBeUndefined();
+ });
+ });
+
+ describe('activeFileExtension', () => {
+ it('returns the file extension for the current active file', () => {
+ localState.openFiles.push(file('active'));
+ localState.openFiles[0].active = true;
+ localState.openFiles[0].path = 'test.js';
+
+ expect(getters.activeFileExtension(localState)).toBe('.js');
+
+ localState.openFiles[0].path = 'test.es6.js';
+
+ expect(getters.activeFileExtension(localState)).toBe('.js');
+ });
+ });
+
+ describe('isCollapsed', () => {
+ it('returns true if state has open files', () => {
+ localState.openFiles.push(file());
+
+ expect(getters.isCollapsed(localState)).toBeTruthy();
+ });
+
+ it('returns false if state has no open files', () => {
+ expect(getters.isCollapsed(localState)).toBeFalsy();
+ });
+ });
+
+ describe('canEditFile', () => {
+ beforeEach(() => {
+ localState.onTopOfBranch = true;
+ localState.canCommit = true;
+
+ localState.openFiles.push(file());
+ localState.openFiles[0].active = true;
+ });
+
+ it('returns true if user can commit and has open files', () => {
+ expect(getters.canEditFile(localState)).toBeTruthy();
+ });
+
+ it('returns false if user can commit and has no open files', () => {
+ localState.openFiles = [];
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user can commit and active file is binary', () => {
+ localState.openFiles[0].binary = true;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user cant commit', () => {
+ localState.canCommit = false;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user can commit but on a branch', () => {
+ localState.onTopOfBranch = false;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js
new file mode 100644
index 00000000000..3c06794d5e3
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/branch_spec.js
@@ -0,0 +1,18 @@
+import mutations from '~/repo/stores/mutations/branch';
+import state from '~/repo/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('SET_CURRENT_BRANCH', () => {
+ it('sets currentBranch', () => {
+ mutations.SET_CURRENT_BRANCH(localState, 'master');
+
+ expect(localState.currentBranch).toBe('master');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js
new file mode 100644
index 00000000000..2f2835dde1f
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/file_spec.js
@@ -0,0 +1,131 @@
+import mutations from '~/repo/stores/mutations/file';
+import state from '~/repo/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store file mutations', () => {
+ let localState;
+ let localFile;
+
+ beforeEach(() => {
+ localState = state();
+ localFile = file();
+ });
+
+ describe('SET_FILE_ACTIVE', () => {
+ it('sets the file active', () => {
+ mutations.SET_FILE_ACTIVE(localState, {
+ file: localFile,
+ active: true,
+ });
+
+ expect(localFile.active).toBeTruthy();
+ });
+ });
+
+ describe('TOGGLE_FILE_OPEN', () => {
+ beforeEach(() => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile);
+ });
+
+ it('adds into opened files', () => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localState.openFiles.length).toBe(1);
+ });
+
+ it('removes from opened files', () => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile);
+
+ expect(localFile.opened).toBeFalsy();
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
+
+ describe('SET_FILE_DATA', () => {
+ it('sets extra file data', () => {
+ mutations.SET_FILE_DATA(localState, {
+ data: {
+ blame_path: 'blame',
+ commits_path: 'commits',
+ permalink: 'permalink',
+ raw_path: 'raw',
+ binary: true,
+ html: 'html',
+ render_error: 'render_error',
+ },
+ file: localFile,
+ });
+
+ expect(localFile.blamePath).toBe('blame');
+ expect(localFile.commitsPath).toBe('commits');
+ expect(localFile.permalink).toBe('permalink');
+ expect(localFile.rawPath).toBe('raw');
+ expect(localFile.binary).toBeTruthy();
+ expect(localFile.html).toBe('html');
+ expect(localFile.renderError).toBe('render_error');
+ });
+ });
+
+ describe('SET_FILE_RAW_DATA', () => {
+ it('sets raw data', () => {
+ mutations.SET_FILE_RAW_DATA(localState, {
+ file: localFile,
+ raw: 'testing',
+ });
+
+ expect(localFile.raw).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_FILE_CONTENT', () => {
+ beforeEach(() => {
+ localFile.raw = 'test';
+ });
+
+ it('sets content', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ file: localFile,
+ content: 'test',
+ });
+
+ expect(localFile.content).toBe('test');
+ });
+
+ it('sets changed if content does not match raw', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ file: localFile,
+ content: 'testing',
+ });
+
+ expect(localFile.content).toBe('testing');
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+
+ describe('DISCARD_FILE_CHANGES', () => {
+ beforeEach(() => {
+ localFile.content = 'test';
+ localFile.changed = true;
+ });
+
+ it('resets content and changed', () => {
+ mutations.DISCARD_FILE_CHANGES(localState, localFile);
+
+ expect(localFile.content).toBe('');
+ expect(localFile.changed).toBeFalsy();
+ });
+ });
+
+ describe('CREATE_TMP_FILE', () => {
+ it('adds file into parent tree', () => {
+ const f = file();
+
+ mutations.CREATE_TMP_FILE(localState, {
+ file: f,
+ parent: localFile,
+ });
+
+ expect(localFile.tree.length).toBe(1);
+ expect(localFile.tree[0].name).toBe(f.name);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js
new file mode 100644
index 00000000000..1c76cfed9c8
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/tree_spec.js
@@ -0,0 +1,71 @@
+import mutations from '~/repo/stores/mutations/tree';
+import state from '~/repo/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store tree mutations', () => {
+ let localState;
+ let localTree;
+
+ beforeEach(() => {
+ localState = state();
+ localTree = file();
+ });
+
+ describe('TOGGLE_TREE_OPEN', () => {
+ it('toggles tree open', () => {
+ mutations.TOGGLE_TREE_OPEN(localState, localTree);
+
+ expect(localTree.opened).toBeTruthy();
+
+ mutations.TOGGLE_TREE_OPEN(localState, localTree);
+
+ expect(localTree.opened).toBeFalsy();
+ });
+ });
+
+ describe('SET_DIRECTORY_DATA', () => {
+ const data = [{
+ name: 'tree',
+ },
+ {
+ name: 'submodule',
+ },
+ {
+ name: 'blob',
+ }];
+
+ it('adds directory data', () => {
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ tree: localState,
+ });
+
+ expect(localState.tree.length).toBe(3);
+ expect(localState.tree[0].name).toBe('tree');
+ expect(localState.tree[1].name).toBe('submodule');
+ expect(localState.tree[2].name).toBe('blob');
+ });
+ });
+
+ describe('SET_PARENT_TREE_URL', () => {
+ it('sets the parent tree url', () => {
+ mutations.SET_PARENT_TREE_URL(localState, 'test');
+
+ expect(localState.parentTreeUrl).toBe('test');
+ });
+ });
+
+ describe('CREATE_TMP_TREE', () => {
+ it('adds tree into parent tree', () => {
+ const tmpEntry = file();
+
+ mutations.CREATE_TMP_TREE(localState, {
+ tmpEntry,
+ parent: localTree,
+ });
+
+ expect(localTree.tree.length).toBe(1);
+ expect(localTree.tree[0].name).toBe(tmpEntry.name);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js
new file mode 100644
index 00000000000..d1c9885e01d
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations_spec.js
@@ -0,0 +1,117 @@
+import mutations from '~/repo/stores/mutations';
+import state from '~/repo/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store mutations', () => {
+ let localState;
+ let entry;
+
+ beforeEach(() => {
+ localState = state();
+ entry = file();
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ it('sets all initial data', () => {
+ mutations.SET_INITIAL_DATA(localState, {
+ test: 'test',
+ });
+
+ expect(localState.test).toBe('test');
+ });
+ });
+
+ describe('SET_PREVIEW_MODE', () => {
+ it('sets currentBlobView to repo-preview', () => {
+ mutations.SET_PREVIEW_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-preview');
+
+ localState.currentBlobView = 'testing';
+
+ mutations.SET_PREVIEW_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-preview');
+ });
+ });
+
+ describe('SET_EDIT_MODE', () => {
+ it('sets currentBlobView to repo-editor', () => {
+ mutations.SET_EDIT_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-editor');
+
+ localState.currentBlobView = 'testing';
+
+ mutations.SET_EDIT_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-editor');
+ });
+ });
+
+ describe('TOGGLE_LOADING', () => {
+ it('toggles loading of entry', () => {
+ mutations.TOGGLE_LOADING(localState, entry);
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, entry);
+
+ expect(entry.loading).toBeFalsy();
+ });
+ });
+
+ describe('TOGGLE_EDIT_MODE', () => {
+ it('toggles editMode', () => {
+ mutations.TOGGLE_EDIT_MODE(localState);
+
+ expect(localState.editMode).toBeTruthy();
+
+ mutations.TOGGLE_EDIT_MODE(localState);
+
+ expect(localState.editMode).toBeFalsy();
+ });
+ });
+
+ describe('TOGGLE_DISCARD_POPUP', () => {
+ it('sets discardPopupOpen', () => {
+ mutations.TOGGLE_DISCARD_POPUP(localState, true);
+
+ expect(localState.discardPopupOpen).toBeTruthy();
+
+ mutations.TOGGLE_DISCARD_POPUP(localState, false);
+
+ expect(localState.discardPopupOpen).toBeFalsy();
+ });
+ });
+
+ describe('SET_COMMIT_REF', () => {
+ it('sets currentRef', () => {
+ mutations.SET_COMMIT_REF(localState, '123');
+
+ expect(localState.currentRef).toBe('123');
+ });
+ });
+
+ describe('SET_ROOT', () => {
+ it('sets isRoot & initialRoot', () => {
+ mutations.SET_ROOT(localState, true);
+
+ expect(localState.isRoot).toBeTruthy();
+ expect(localState.isInitialRoot).toBeTruthy();
+
+ mutations.SET_ROOT(localState, false);
+
+ expect(localState.isRoot).toBeFalsy();
+ expect(localState.isInitialRoot).toBeFalsy();
+ });
+ });
+
+ describe('SET_PREVIOUS_URL', () => {
+ it('sets previousUrl', () => {
+ mutations.SET_PREVIOUS_URL(localState, 'testing');
+
+ expect(localState.previousUrl).toBe('testing');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js
new file mode 100644
index 00000000000..37287c587d7
--- /dev/null
+++ b/spec/javascripts/repo/stores/utils_spec.js
@@ -0,0 +1,102 @@
+import * as utils from '~/repo/stores/utils';
+
+describe('Multi-file store utils', () => {
+ describe('setPageTitle', () => {
+ it('sets the document page title', () => {
+ utils.setPageTitle('test');
+
+ expect(document.title).toBe('test');
+ });
+ });
+
+ describe('pushState', () => {
+ it('calls history.pushState', () => {
+ spyOn(history, 'pushState');
+
+ utils.pushState('test');
+
+ expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test');
+ });
+ });
+
+ describe('createTemp', () => {
+ it('creates temp tree', () => {
+ const tmp = utils.createTemp({
+ name: 'test',
+ path: 'test',
+ type: 'tree',
+ level: 0,
+ changed: false,
+ content: '',
+ base64: '',
+ });
+
+ expect(tmp.tempFile).toBeTruthy();
+ expect(tmp.icon).toBe('fa-folder');
+ });
+
+ it('creates temp file', () => {
+ const tmp = utils.createTemp({
+ name: 'test',
+ path: 'test',
+ type: 'blob',
+ level: 0,
+ changed: false,
+ content: '',
+ base64: '',
+ });
+
+ expect(tmp.tempFile).toBeTruthy();
+ expect(tmp.icon).toBe('fa-file-text-o');
+ });
+ });
+
+ describe('findIndexOfFile', () => {
+ let state;
+
+ beforeEach(() => {
+ state = [{
+ path: '1',
+ }, {
+ path: '2',
+ }];
+ });
+
+ it('finds in the index of an entry by path', () => {
+ const index = utils.findIndexOfFile(state, {
+ path: '2',
+ });
+
+ expect(index).toBe(1);
+ });
+ });
+
+ describe('findEntry', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ tree: [{
+ type: 'tree',
+ name: 'test',
+ }, {
+ type: 'blob',
+ name: 'file',
+ }],
+ };
+ });
+
+ it('returns an entry found by name', () => {
+ const foundEntry = utils.findEntry(state, 'tree', 'test');
+
+ expect(foundEntry.type).toBe('tree');
+ expect(foundEntry.name).toBe('test');
+ });
+
+ it('returns undefined when no entry found', () => {
+ const foundEntry = utils.findEntry(state, 'blob', 'test');
+
+ expect(foundEntry).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index cf811af3d6c..a2394857b82 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -3,7 +3,6 @@
import '~/gl_dropdown';
import '~/search_autocomplete';
import '~/lib/utils/common_utils';
-import 'vendor/fuzzaldrin-plus';
(function() {
var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -58,6 +57,10 @@ import 'vendor/fuzzaldrin-plus';
}
};
+ const disableProjectIssues = function() {
+ document.querySelector('.js-search-project-options').setAttribute('data-issues-disabled', true);
+ };
+
// Mock `gl` object in window for dashboard specific page. App code will need it.
mockDashboardOptions = function() {
window.gl || (window.gl = {});
@@ -92,18 +95,20 @@ import 'vendor/fuzzaldrin-plus';
assertLinks = function(list, issuesPath, mrsPath) {
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
- issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
- issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
+ if (issuesPath) {
+ issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
+ issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
+ a1 = "a[href='" + issuesAssignedToMeLink + "']";
+ a2 = "a[href='" + issuesIHaveCreatedLink + "']";
+ expect(list.find(a1).length).toBe(1);
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
+ expect(list.find(a2).length).toBe(1);
+ expect(list.find(a2).text()).toBe("Issues I've created");
+ }
mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName;
mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName;
- a1 = "a[href='" + issuesAssignedToMeLink + "']";
- a2 = "a[href='" + issuesIHaveCreatedLink + "']";
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
- expect(list.find(a1).length).toBe(1);
- expect(list.find(a1).text()).toBe('Issues assigned to me');
- expect(list.find(a2).length).toBe(1);
- expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
@@ -154,6 +159,14 @@ import 'vendor/fuzzaldrin-plus';
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
+ it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() {
+ addBodyAttributes('project');
+ disableProjectIssues();
+ mockProjectOptions();
+ widget.searchInput.triggerHandler('focus');
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
+ assertLinks(list, null, projectMRsPath);
+ });
it('should not show category related menu if there is text in the input', function() {
var link, list;
addBodyAttributes('project');
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index f6320db8dc4..5d6a885d4cc 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,6 +1,8 @@
-import '~/copy_as_gfm';
+import initCopyAsGFM from '~/behaviors/copy_as_gfm';
import ShortcutsIssuable from '~/shortcuts_issuable';
+initCopyAsGFM();
+
describe('ShortcutsIssuable', () => {
const fixtureName = 'merge_requests/diff_comment.html.raw';
preloadFixtures(fixtureName);
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index e2b6bcabc98..0682b463043 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -109,12 +109,14 @@ const sidebarMockData = {
labels: [],
web_url: '/root/some-project/issues/5',
},
+ '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {},
},
};
export default {
mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
editable: true,
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
new file mode 100644
index 00000000000..30cc549c7c0
--- /dev/null
+++ b/spec/javascripts/sidebar/participants_spec.js
@@ -0,0 +1,174 @@
+import Vue from 'vue';
+import participants from '~/sidebar/components/participants/participants.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+ PARTICIPANT,
+ { ...PARTICIPANT, id: 2 },
+ { ...PARTICIPANT, id: 3 },
+];
+
+describe('Participants', function () {
+ let vm;
+ let Participants;
+
+ beforeEach(() => {
+ Participants = Vue.extend(participants);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Participants, {
+ loading: true,
+ });
+
+ expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
+ });
+
+ it('shows participant count when given', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ });
+ const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+ expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+
+ it('shows full participant count when there are hidden participants', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 1,
+ });
+ const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+ expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+ });
+
+ describe('expanded sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Participants, {
+ loading: true,
+ });
+
+ expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
+ });
+
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', (done) => {
+ const numberOfLessParticipants = 2;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ vm.isShowingMoreParticipants = false;
+
+ Vue.nextTick()
+ .then(() => {
+ const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+ expect(participantEls.length).toBe(numberOfLessParticipants);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when only showing all participants, each has an avatar', (done) => {
+ const numberOfLessParticipants = 2;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ vm.isShowingMoreParticipants = true;
+
+ Vue.nextTick()
+ .then(() => {
+ const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+ expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not have more participants link when they can all be shown', () => {
+ const numberOfLessParticipants = 100;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
+ expect(moreParticipantLink).toBeNull();
+ });
+
+ it('when too many participants, has more participants link to show more', (done) => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ vm.isShowingMoreParticipants = false;
+
+ Vue.nextTick()
+ .then(() => {
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when too many participants and already showing them, has more participants link to show less', (done) => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ vm.isShowingMoreParticipants = true;
+
+ Vue.nextTick()
+ .then(() => {
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(moreParticipantLink.textContent.trim()).toBe('- show less');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clicking more participants link emits event', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(vm.isShowingMoreParticipants).toBe(false);
+
+ moreParticipantLink.click();
+
+ expect(vm.isShowingMoreParticipants).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 3aa8ca5db0d..7deb1fd2118 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -57,8 +57,8 @@ describe('Sidebar mediator', () => {
.then(() => {
expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
- done();
})
+ .then(done)
.catch(done.fail);
});
@@ -72,8 +72,21 @@ describe('Sidebar mediator', () => {
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
- done();
})
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('toggle subscription', (done) => {
+ this.mediator.store.setSubscribedState(false);
+ spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
+
+ this.mediator.toggleSubscription()
+ .then(() => {
+ expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
+ expect(this.mediator.store.subscribed).toEqual(true);
+ })
+ .then(done)
.catch(done.fail);
});
});
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
deleted file mode 100644
index a4bd8ba8d88..00000000000
--- a/spec/javascripts/sidebar/sidebar_service_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import Vue from 'vue';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import Mock from './mock_data';
-
-describe('Sidebar service', () => {
- beforeEach(() => {
- Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- this.service = new SidebarService({
- endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
- moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
- projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
- });
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
- });
-
- it('gets the data', (done) => {
- this.service.get()
- .then((resp) => {
- expect(resp).toBeDefined();
- done();
- })
- .catch(done.fail);
- });
-
- it('updates the data', (done) => {
- this.service.update('issue[assignee_ids]', [1])
- .then((resp) => {
- expect(resp).toBeDefined();
- done();
- })
- .catch(done.fail);
- });
-
- it('gets projects for autocomplete', (done) => {
- this.service.getProjectsAutocomplete()
- .then((resp) => {
- expect(resp).toBeDefined();
- done();
- })
- .catch(done.fail);
- });
-
- it('moves the issue to another project', (done) => {
- this.service.moveIssue(123)
- .then((resp) => {
- expect(resp).toBeDefined();
- done();
- })
- .catch(done.fail);
- });
-});
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index 69eb3839d67..51dee64fb93 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -2,21 +2,36 @@ import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
-describe('Sidebar store', () => {
- const assignee = {
- id: 2,
- name: 'gitlab user 2',
- username: 'gitlab2',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- };
-
- const anotherAssignee = {
- id: 3,
- name: 'gitlab user 3',
- username: 'gitlab3',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- };
+const ASSIGNEE = {
+ id: 2,
+ name: 'gitlab user 2',
+ username: 'gitlab2',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const ANOTHER_ASSINEE = {
+ id: 3,
+ name: 'gitlab user 3',
+ username: 'gitlab3',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+ PARTICIPANT,
+ { ...PARTICIPANT, id: 2 },
+ { ...PARTICIPANT, id: 3 },
+];
+describe('Sidebar store', () => {
beforeEach(() => {
this.store = new SidebarStore({
currentUser: {
@@ -40,23 +55,23 @@ describe('Sidebar store', () => {
});
it('adds a new assignee', () => {
- this.store.addAssignee(assignee);
+ this.store.addAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(1);
});
it('removes an assignee', () => {
- this.store.removeAssignee(assignee);
+ this.store.removeAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(0);
});
it('finds an existent assignee', () => {
let foundAssignee;
- this.store.addAssignee(assignee);
- foundAssignee = this.store.findAssignee(assignee);
+ this.store.addAssignee(ASSIGNEE);
+ foundAssignee = this.store.findAssignee(ASSIGNEE);
expect(foundAssignee).toBeDefined();
- expect(foundAssignee).toEqual(assignee);
- foundAssignee = this.store.findAssignee(anotherAssignee);
+ expect(foundAssignee).toEqual(ASSIGNEE);
+ foundAssignee = this.store.findAssignee(ANOTHER_ASSINEE);
expect(foundAssignee).toBeUndefined();
});
@@ -65,6 +80,28 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(0);
});
+ it('sets participants data', () => {
+ expect(this.store.participants.length).toEqual(0);
+
+ this.store.setParticipantsData({
+ participants: PARTICIPANT_LIST,
+ });
+
+ expect(this.store.isFetching.participants).toEqual(false);
+ expect(this.store.participants.length).toEqual(PARTICIPANT_LIST.length);
+ });
+
+ it('sets subcriptions data', () => {
+ expect(this.store.subscribed).toEqual(null);
+
+ this.store.setSubscriptionsData({
+ subscribed: true,
+ });
+
+ expect(this.store.isFetching.subscriptions).toEqual(false);
+ expect(this.store.subscribed).toEqual(true);
+ });
+
it('set assigned data', () => {
const users = {
assignees: UsersMockHelper.createNumberRandomUsers(3),
@@ -75,6 +112,14 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(3);
});
+ it('sets fetching state', () => {
+ expect(this.store.isFetching.participants).toEqual(true);
+
+ this.store.setFetchingState('participants', false);
+
+ expect(this.store.isFetching.participants).toEqual(false);
+ });
+
it('set time tracking data', () => {
this.store.setTimeTrackingData(Mock.time);
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
@@ -90,6 +135,14 @@ describe('Sidebar store', () => {
expect(this.store.autocompleteProjects).toEqual(projects);
});
+ it('sets subscribed state', () => {
+ expect(this.store.subscribed).toEqual(null);
+
+ this.store.setSubscribedState(true);
+
+ expect(this.store.subscribed).toEqual(true);
+ });
+
it('set move to project ID', () => {
const projectId = 7;
this.store.setMoveToProjectId(projectId);
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
new file mode 100644
index 00000000000..7adf22b0f1f
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import eventHub from '~/sidebar/event_hub';
+import mountComponent from '../helpers/vue_mount_component_helper';
+import Mock from './mock_data';
+
+describe('Sidebar Subscriptions', function () {
+ let vm;
+ let SidebarSubscriptions;
+
+ beforeEach(() => {
+ SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
+ // Setup the stores, services, etc
+ // eslint-disable-next-line no-new
+ new SidebarMediator(Mock.mediator);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ });
+
+ it('calls the mediator toggleSubscription on event', () => {
+ spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve());
+ vm = mountComponent(SidebarSubscriptions, {});
+
+ eventHub.$emit('toggleSubscription');
+
+ expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
new file mode 100644
index 00000000000..9b33dd02fb9
--- /dev/null
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Subscriptions', function () {
+ let vm;
+ let Subscriptions;
+
+ beforeEach(() => {
+ Subscriptions = Vue.extend(subscriptions);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Subscriptions, {
+ loading: true,
+ subscribed: undefined,
+ });
+
+ expect(vm.$refs.loadingButton.loading).toBe(true);
+ expect(vm.$refs.loadingButton.label).toBeUndefined();
+ });
+
+ it('has "Subscribe" text when currently not subscribed', () => {
+ vm = mountComponent(Subscriptions, {
+ subscribed: false,
+ });
+
+ expect(vm.$refs.loadingButton.label).toBe('Subscribe');
+ });
+
+ it('has "Unsubscribe" text when currently not subscribed', () => {
+ vm = mountComponent(Subscriptions, {
+ subscribed: true,
+ });
+
+ expect(vm.$refs.loadingButton.label).toBe('Unsubscribe');
+ });
+});
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
index 7833bf3fb04..1c87fcec245 100644
--- a/spec/javascripts/smart_interval_spec.js
+++ b/spec/javascripts/smart_interval_spec.js
@@ -1,6 +1,6 @@
-import '~/smart_interval';
+import SmartInterval from '~/smart_interval';
-(() => {
+describe('SmartInterval', function () {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_SHORT_TIMEOUT = 75;
@@ -9,7 +9,7 @@ import '~/smart_interval';
function createDefaultSmartInterval(config) {
const defaultParams = {
- callback: () => {},
+ callback: () => Promise.resolve(),
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
@@ -22,158 +22,171 @@ import '~/smart_interval';
_.extend(defaultParams, config);
}
- return new gl.SmartInterval(defaultParams);
+ return new SmartInterval(defaultParams);
}
- describe('SmartInterval', function () {
- describe('Increment Interval', function () {
- beforeEach(function () {
- this.smartInterval = createDefaultSmartInterval();
- });
+ describe('Increment Interval', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
- it('should increment the interval delay', function (done) {
- const interval = this.smartInterval;
- setTimeout(() => {
- const intervalConfig = this.smartInterval.cfg;
- const iterationCount = 4;
- const maxIntervalAfterIterations = intervalConfig.startingInterval *
- (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
- const currentInterval = interval.getCurrentInterval();
-
- // Provide some flexibility for performance of testing environment
- expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
- expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
-
- done();
- }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
- });
+ it('should increment the interval delay', function (done) {
+ const interval = this.smartInterval;
+ setTimeout(() => {
+ const intervalConfig = this.smartInterval.cfg;
+ const iterationCount = 4;
+ const maxIntervalAfterIterations = intervalConfig.startingInterval *
+ (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
+ const currentInterval = interval.getCurrentInterval();
+
+ // Provide some flexibility for performance of testing environment
+ expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
+ expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
+ });
- it('should not increment past maxInterval', function (done) {
- const interval = this.smartInterval;
+ it('should not increment past maxInterval', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- const currentInterval = interval.getCurrentInterval();
- expect(currentInterval).toBe(interval.cfg.maxInterval);
+ setTimeout(() => {
+ const currentInterval = interval.getCurrentInterval();
+ expect(currentInterval).toBe(interval.cfg.maxInterval);
- done();
- }, DEFAULT_LONG_TIMEOUT);
- });
+ done();
+ }, DEFAULT_LONG_TIMEOUT);
});
- describe('Public methods', function () {
- beforeEach(function () {
- this.smartInterval = createDefaultSmartInterval();
+ it('does not increment while waiting for callback', function () {
+ jasmine.clock().install();
+
+ const smartInterval = createDefaultSmartInterval({
+ callback: () => new Promise($.noop),
});
- it('should cancel an interval', function (done) {
- const interval = this.smartInterval;
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
+
+ const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
+ expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
- setTimeout(() => {
- interval.cancel();
+ jasmine.clock().uninstall();
+ });
+ });
- const intervalId = interval.state.intervalId;
- const currentInterval = interval.getCurrentInterval();
- const intervalLowerLimit = interval.cfg.startingInterval;
+ describe('Public methods', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
- expect(intervalId).toBeUndefined();
- expect(currentInterval).toBe(intervalLowerLimit);
+ it('should cancel an interval', function (done) {
+ const interval = this.smartInterval;
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ setTimeout(() => {
+ interval.cancel();
- it('should resume an interval', function (done) {
- const interval = this.smartInterval;
+ const intervalId = interval.state.intervalId;
+ const currentInterval = interval.getCurrentInterval();
+ const intervalLowerLimit = interval.cfg.startingInterval;
- setTimeout(() => {
- interval.cancel();
+ expect(intervalId).toBeUndefined();
+ expect(currentInterval).toBe(intervalLowerLimit);
- interval.resume();
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- const intervalId = interval.state.intervalId;
+ it('should resume an interval', function (done) {
+ const interval = this.smartInterval;
- expect(intervalId).toBeTruthy();
+ setTimeout(() => {
+ interval.cancel();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ interval.resume();
+
+ const intervalId = interval.state.intervalId;
+
+ expect(intervalId).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
});
+ });
- describe('DOM Events', function () {
- beforeEach(function () {
- // This ensures DOM and DOM events are initialized for these specs.
- setFixtures('<div></div>');
+ describe('DOM Events', function () {
+ beforeEach(function () {
+ // This ensures DOM and DOM events are initialized for these specs.
+ setFixtures('<div></div>');
- this.smartInterval = createDefaultSmartInterval();
- });
+ this.smartInterval = createDefaultSmartInterval();
+ });
- it('should pause when page is not visible', function (done) {
- const interval = this.smartInterval;
+ it('should pause when page is not visible', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
- expect(interval.state.intervalId).toBeUndefined();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ expect(interval.state.intervalId).toBeUndefined();
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should change to the hidden interval when page is not visible', function (done) {
- const HIDDEN_INTERVAL = 1500;
- const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
+ it('should change to the hidden interval when page is not visible', function (done) {
+ const HIDDEN_INTERVAL = 1500;
+ const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
- interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
+ interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should resume when page is becomes visible at the previous interval', function (done) {
- const interval = this.smartInterval;
+ it('should resume when page is becomes visible at the previous interval', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
- expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.state.intervalId).toBeUndefined();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
- expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.state.intervalId).toBeTruthy();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should cancel on page unload', function (done) {
- const interval = this.smartInterval;
+ it('should cancel on page unload', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- $(document).triggerHandler('beforeunload');
- expect(interval.state.intervalId).toBeUndefined();
- expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ setTimeout(() => {
+ $(document).triggerHandler('beforeunload');
+ expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should execute callback before first interval', function () {
- const interval = createDefaultSmartInterval({ immediateExecution: true });
- expect(interval.cfg.immediateExecution).toBeFalsy();
- });
+ it('should execute callback before first interval', function () {
+ const interval = createDefaultSmartInterval({ immediateExecution: true });
+ expect(interval.cfg.immediateExecution).toBeFalsy();
});
});
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index d4e134583c7..fd7aa332d17 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -11,6 +11,12 @@ const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent);
Vue.config.devtools = !isHeadlessChrome;
Vue.config.productionTip = false;
+let hasVueWarnings = false;
+Vue.config.warnHandler = (msg, vm, trace) => {
+ hasVueWarnings = true;
+ fail(`${msg}${trace}`);
+};
+
Vue.use(VueResource);
// enable test fixtures
@@ -34,11 +40,6 @@ window.addEventListener('unhandledrejection', (event) => {
console.error(event.reason.stack || event.reason);
});
-const checkUnhandledPromiseRejections = (done) => {
- expect(hasUnhandledPromiseRejections).toBe(false);
- done();
-};
-
// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
// because it appears to lock up the thread that communicates to Karma's socket
// This async beforeEach gets called on every spec and releases the JS thread long
@@ -47,17 +48,6 @@ const checkUnhandledPromiseRejections = (done) => {
// to run our unit tests.
beforeEach(done => done());
-beforeAll(() => {
- const origError = console.error;
- spyOn(console, 'error').and.callFake((message) => {
- if (/^\[Vue warn\]/.test(message)) {
- fail(message);
- } else {
- origError(message);
- }
- });
-});
-
const builtinVueHttpInterceptors = Vue.http.interceptors.slice();
beforeEach(() => {
@@ -80,8 +70,22 @@ testsContext.keys().forEach(function (path) {
}
});
-it('has no unhandled Promise rejections', (done) => {
- setTimeout(checkUnhandledPromiseRejections(done), 1000);
+describe('test errors', () => {
+ beforeAll((done) => {
+ if (hasUnhandledPromiseRejections || hasVueWarnings) {
+ setTimeout(done, 1000);
+ } else {
+ done();
+ }
+ });
+
+ it('has no unhandled Promise rejections', () => {
+ expect(hasUnhandledPromiseRejections).toBe(false);
+ });
+
+ it('has no Vue warnings', () => {
+ expect(hasVueWarnings).toBe(false);
+ });
});
// if we're generating coverage reports, make sure to include all files so
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 690665ae12c..d7af956c9c1 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,148 +1,147 @@
import Vue from 'vue';
-import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
-import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
+import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
import mockData from '../mock_data';
-const createComponent = (mr) => {
- const Component = Vue.extend(pipelineComponent);
- return new Component({
- el: document.createElement('div'),
- propsData: { mr },
- });
-};
-
describe('MRWidgetPipeline', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = pipelineComponent.props;
+ let vm;
+ let Component;
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
+ beforeEach(() => {
+ Component = Vue.extend(pipelineComponent);
});
- describe('components', () => {
- it('should have components added', () => {
- expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
- expect(pipelineComponent.components.ciIcon).toBeDefined();
- });
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
- describe('svg', () => {
- it('should have the proper SVG icon', () => {
- const vm = createComponent({ pipeline: mockData.pipeline });
-
- expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed);
- });
- });
-
describe('hasPipeline', () => {
it('should return true when there is a pipeline', () => {
- expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0);
-
- const vm = createComponent({
+ vm = mountComponent(Component, {
pipeline: mockData.pipeline,
+ ciStatus: 'success',
+ hasCi: true,
});
- expect(vm.hasPipeline).toBeTruthy();
+ expect(vm.hasPipeline).toEqual(true);
});
it('should return false when there is no pipeline', () => {
- const vm = createComponent({
- pipeline: null,
+ vm = mountComponent(Component, {
+ pipeline: {},
});
- expect(vm.hasPipeline).toBeFalsy();
+ expect(vm.hasPipeline).toEqual(false);
});
});
describe('hasCIError', () => {
it('should return false when there is no CI error', () => {
- const vm = createComponent({
+ vm = mountComponent(Component, {
pipeline: mockData.pipeline,
- hasCI: true,
+ hasCi: true,
ciStatus: 'success',
});
- expect(vm.hasCIError).toBeFalsy();
+ expect(vm.hasCIError).toEqual(false);
});
it('should return true when there is a CI error', () => {
- const vm = createComponent({
+ vm = mountComponent(Component, {
pipeline: mockData.pipeline,
- hasCI: true,
+ hasCi: true,
ciStatus: null,
});
- expect(vm.hasCIError).toBeTruthy();
+ expect(vm.hasCIError).toEqual(true);
});
});
});
- describe('template', () => {
- let vm;
- let el;
- const { pipeline } = mockData;
- const mr = {
- hasCI: true,
- ciStatus: 'success',
- pipelineDetailedStatus: pipeline.details.status,
- pipeline,
- };
-
- beforeEach(() => {
- vm = createComponent(mr);
- el = vm.$el;
- });
+ describe('rendered output', () => {
+ it('should render CI error', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ ciStatus: null,
+ });
- it('should render template elements correctly', () => {
- expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
- expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1);
- expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`);
- expect(el.innerText).toContain('passed');
- expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path);
- expect(el.querySelectorAll('.stage-container').length).toEqual(2);
- expect(el.querySelector('.js-ci-error')).toEqual(null);
- expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path);
- expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id);
- expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%`);
+ expect(
+ vm.$el.querySelector('.media-body').textContent.trim(),
+ ).toEqual('Could not connect to the CI server. Please check your settings and try again');
});
- it('should list single stage', (done) => {
- pipeline.details.stages.splice(0, 1);
+ describe('with a pipeline', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ });
+ });
+
+ it('should render pipeline ID', () => {
+ expect(
+ vm.$el.querySelector('.pipeline-id').textContent.trim(),
+ ).toEqual(`#${mockData.pipeline.id}`);
+ });
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.stage-container button').length).toEqual(1);
- done();
+ it('should render pipeline status and commit id', () => {
+ expect(
+ vm.$el.querySelector('.media-body').textContent.trim(),
+ ).toContain(mockData.pipeline.details.status.label);
+
+ expect(
+ vm.$el.querySelector('.js-commit-link').textContent.trim(),
+ ).toEqual(mockData.pipeline.commit.short_id);
+
+ expect(
+ vm.$el.querySelector('.js-commit-link').getAttribute('href'),
+ ).toEqual(mockData.pipeline.commit.commit_path);
});
- });
- it('should not have stages when there is no stage', (done) => {
- vm.mr.pipeline.details.stages = [];
+ it('should render pipeline graph', () => {
+ expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length);
+ });
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.stage-container button').length).toEqual(0);
- done();
+ it('should render coverage information', () => {
+ expect(
+ vm.$el.querySelector('.media-body').textContent,
+ ).toContain(`Coverage ${mockData.pipeline.coverage}`);
});
});
- it('should not have coverage text when pipeline has no coverage info', (done) => {
- vm.mr.pipeline.coverage = null;
+ describe('without coverage', () => {
+ it('should not render a coverage', () => {
+ const mockCopy = Object.assign({}, mockData);
+ delete mockCopy.pipeline.coverage;
+
+ vm = mountComponent(Component, {
+ pipeline: mockCopy.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ });
- Vue.nextTick(() => {
- expect(el.querySelector('.js-mr-coverage')).toEqual(null);
- done();
+ expect(
+ vm.$el.querySelector('.media-body').textContent,
+ ).not.toContain('Coverage');
});
});
- it('should show CI error when there is a CI error', (done) => {
- vm.mr.ciStatus = null;
+ describe('without a pipeline graph', () => {
+ it('should not render a pipeline graph', () => {
+ const mockCopy = Object.assign({}, mockData);
+ delete mockCopy.pipeline.details.stages;
+
+ vm = mountComponent(Component, {
+ pipeline: mockCopy.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ });
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
- expect(el.innerText).toContain('Could not connect to the CI server');
- done();
+ expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 0795d0aaa82..1ad7c2d8efa 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -202,7 +202,6 @@ export default {
"revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
"email_patches_path": "/root/acets-app/merge_requests/22.patch",
"plain_diff_path": "/root/acets-app/merge_requests/22.diff",
- "ci_status_path": "/root/acets-app/merge_requests/22/ci_status",
"status_path": "/root/acets-app/merge_requests/22.json",
"merge_check_path": "/root/acets-app/merge_requests/22/merge_check",
"ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status",
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index e4324e91502..9e6d0aa472c 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -1,16 +1,9 @@
import Vue from 'vue';
-import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
import mockData from './mock_data';
-
-const createComponent = () => {
- delete mrWidgetOptions.el; // Prevent component mounting
- gl.mrWidgetData = mockData;
- const Component = Vue.extend(mrWidgetOptions);
- return new Component();
-};
+import mountComponent from '../helpers/vue_mount_component_helper';
const returnPromise = data => new Promise((resolve) => {
resolve({
@@ -23,9 +16,16 @@ const returnPromise = data => new Promise((resolve) => {
describe('mrWidgetOptions', () => {
let vm;
+ let MrWidgetOptions;
beforeEach(() => {
- vm = createComponent();
+ // Prevent component mounting
+ delete mrWidgetOptions.el;
+
+ MrWidgetOptions = Vue.extend(mrWidgetOptions);
+ vm = mountComponent(MrWidgetOptions, {
+ mrData: { ...mockData },
+ });
});
describe('data', () => {
@@ -78,7 +78,7 @@ describe('mrWidgetOptions', () => {
});
it('should return true if there is relatedLinks in MR', () => {
- vm.mr.relatedLinks = {};
+ Vue.set(vm.mr, 'relatedLinks', {});
expect(vm.shouldRenderRelatedLinks).toBeTruthy();
});
});
@@ -121,24 +121,28 @@ describe('mrWidgetOptions', () => {
describe('initPolling', () => {
it('should call SmartInterval', () => {
- spyOn(gl, 'SmartInterval').and.returnValue({
- resume() {},
- stopTimer() {},
- });
+ spyOn(vm, 'checkStatus').and.returnValue(Promise.resolve());
+ jasmine.clock().install();
vm.initPolling();
+ expect(vm.checkStatus).not.toHaveBeenCalled();
+
+ jasmine.clock().tick(10000);
+
expect(vm.pollingInterval).toBeDefined();
- expect(gl.SmartInterval).toHaveBeenCalled();
+ expect(vm.checkStatus).toHaveBeenCalled();
+
+ jasmine.clock().uninstall();
});
});
describe('initDeploymentsPolling', () => {
it('should call SmartInterval', () => {
- spyOn(gl, 'SmartInterval');
+ spyOn(vm, 'fetchDeployments').and.returnValue(Promise.resolve());
vm.initDeploymentsPolling();
expect(vm.deploymentsInterval).toBeDefined();
- expect(gl.SmartInterval).toHaveBeenCalled();
+ expect(vm.fetchDeployments).toHaveBeenCalled();
});
});
@@ -312,28 +316,6 @@ describe('mrWidgetOptions', () => {
expect(vm.pollingInterval.stopTimer).toHaveBeenCalled();
});
});
-
- describe('createService', () => {
- it('should instantiate a Service', () => {
- const endpoints = {
- mergePath: '/nice/path',
- mergeCheckPath: '/nice/path',
- cancelAutoMergePath: '/nice/path',
- removeWIPPath: '/nice/path',
- sourceBranchPath: '/nice/path',
- ciEnvironmentsStatusPath: '/nice/path',
- statusPath: '/nice/path',
- mergeActionsContentPath: '/nice/path',
- };
-
- const serviceInstance = vm.createService(endpoints);
- const isInstanceOfMRService = serviceInstance instanceof MRWidgetService;
- expect(isInstanceOfMRService).toBe(true);
- Object.keys(serviceInstance).forEach((key) => {
- expect(serviceInstance[key]).toBeDefined();
- });
- });
- });
});
describe('components', () => {
diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
deleted file mode 100644
index e667b4b3677..00000000000
--- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
-
-Vue.use(VueResource);
-
-describe('MRWidgetService', () => {
- const mr = {
- mergePath: './',
- mergeCheckPath: './',
- cancelAutoMergePath: './',
- removeWIPPath: './',
- sourceBranchPath: './',
- ciEnvironmentsStatusPath: './',
- statusPath: './',
- mergeActionsContentPath: './',
- isServiceStore: true,
- };
-
- it('should have store and resources created in constructor', () => {
- const service = new MRWidgetService(mr);
-
- expect(service.mergeResource).toBeDefined();
- expect(service.mergeCheckResource).toBeDefined();
- expect(service.cancelAutoMergeResource).toBeDefined();
- expect(service.removeWIPResource).toBeDefined();
- expect(service.removeSourceBranchResource).toBeDefined();
- expect(service.deploymentsResource).toBeDefined();
- expect(service.pollResource).toBeDefined();
- expect(service.mergeActionsContentResource).toBeDefined();
- });
-
- it('should have methods defined', () => {
- window.history.pushState({}, null, '/');
- const service = new MRWidgetService(mr);
-
- expect(service.merge()).toBeDefined();
- expect(service.cancelAutomaticMerge()).toBeDefined();
- expect(service.removeWIP()).toBeDefined();
- expect(service.removeSourceBranch()).toBeDefined();
- expect(service.fetchDeployments()).toBeDefined();
- expect(service.poll()).toBeDefined();
- expect(service.checkStatus()).toBeDefined();
- expect(service.fetchMergeActionsContent()).toBeDefined();
- expect(MRWidgetService.stopEnvironment()).toBeDefined();
- });
-});
diff --git a/spec/javascripts/vue_shared/ci_action_icons_spec.js b/spec/javascripts/vue_shared/ci_action_icons_spec.js
deleted file mode 100644
index 3d53a5ab24d..00000000000
--- a/spec/javascripts/vue_shared/ci_action_icons_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import getActionIcon from '~/vue_shared/ci_action_icons';
-import cancelSVG from 'icons/_icon_action_cancel.svg';
-import retrySVG from 'icons/_icon_action_retry.svg';
-import playSVG from 'icons/_icon_action_play.svg';
-import stopSVG from 'icons/_icon_action_stop.svg';
-
-describe('getActionIcon', () => {
- it('should return an empty string', () => {
- expect(getActionIcon()).toEqual('');
- });
-
- it('should return cancel svg', () => {
- expect(getActionIcon('icon_action_cancel')).toEqual(cancelSVG);
- });
-
- it('should return retry svg', () => {
- expect(getActionIcon('icon_action_retry')).toEqual(retrySVG);
- });
-
- it('should return play svg', () => {
- expect(getActionIcon('icon_action_play')).toEqual(playSVG);
- });
-
- it('should render stop svg', () => {
- expect(getActionIcon('icon_action_stop')).toEqual(stopSVG);
- });
-});
diff --git a/spec/javascripts/vue_shared/ci_status_icon_spec.js b/spec/javascripts/vue_shared/ci_status_icon_spec.js
deleted file mode 100644
index b6621d6054d..00000000000
--- a/spec/javascripts/vue_shared/ci_status_icon_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { borderlessStatusIconEntityMap, statusIconEntityMap } from '~/vue_shared/ci_status_icons';
-
-describe('CI status icons', () => {
- const statuses = [
- 'icon_status_canceled',
- 'icon_status_created',
- 'icon_status_failed',
- 'icon_status_manual',
- 'icon_status_pending',
- 'icon_status_running',
- 'icon_status_skipped',
- 'icon_status_success',
- 'icon_status_warning',
- ];
-
- it('should have a dictionary for borderless icons', () => {
- statuses.forEach((status) => {
- expect(borderlessStatusIconEntityMap[status]).toBeDefined();
- });
- });
-
- it('should have a dictionary for icons', () => {
- statuses.forEach((status) => {
- expect(statusIconEntityMap[status]).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
index ba303738f71..8762ce9903b 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -11,63 +11,63 @@ describe('CI Badge Link Component', () => {
text: 'canceled',
label: 'canceled',
group: 'canceled',
- icon: 'icon_status_canceled',
+ icon: 'status_canceled',
details_path: 'status/canceled',
},
created: {
text: 'created',
label: 'created',
group: 'created',
- icon: 'icon_status_created',
+ icon: 'status_created',
details_path: 'status/created',
},
failed: {
text: 'failed',
label: 'failed',
group: 'failed',
- icon: 'icon_status_failed',
+ icon: 'status_failed',
details_path: 'status/failed',
},
manual: {
text: 'manual',
label: 'manual action',
group: 'manual',
- icon: 'icon_status_manual',
+ icon: 'status_manual',
details_path: 'status/manual',
},
pending: {
text: 'pending',
label: 'pending',
group: 'pending',
- icon: 'icon_status_pending',
+ icon: 'status_pending',
details_path: 'status/pending',
},
running: {
text: 'running',
label: 'running',
group: 'running',
- icon: 'icon_status_running',
+ icon: 'status_running',
details_path: 'status/running',
},
skipped: {
text: 'skipped',
label: 'skipped',
group: 'skipped',
- icon: 'icon_status_skipped',
+ icon: 'status_skipped',
details_path: 'status/skipped',
},
success_warining: {
text: 'passed',
label: 'passed',
group: 'success_with_warnings',
- icon: 'icon_status_warning',
+ icon: 'status_warning',
details_path: 'status/warning',
},
success: {
text: 'passed',
label: 'passed',
group: 'passed',
- icon: 'icon_status_success',
+ icon: 'status_success',
details_path: 'status/passed',
},
};
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
new file mode 100644
index 00000000000..104da4473ce
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Sprite Icon Component', function () {
+ describe('Initialization', function () {
+ let icon;
+
+ beforeEach(function () {
+ const IconComponent = Vue.extend(Icon);
+
+ icon = mountComponent(IconComponent, {
+ name: 'test',
+ size: 99,
+ cssClasses: 'extraclasses',
+ });
+ });
+
+ afterEach(() => {
+ icon.$destroy();
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(icon).toBeDefined();
+ });
+
+ it('should have <svg> as a child element', function () {
+ expect(icon.$el.tagName).toBe('svg');
+ });
+
+ it('should have <use> as a child element with the correct href', function () {
+ expect(icon.$el.firstChild.tagName).toBe('use');
+ expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_icons}#test`);
+ });
+
+ it('should properly compute iconSizeClass', function () {
+ expect(icon.iconSizeClass).toBe('s99');
+ });
+
+ it('should properly render img css', function () {
+ const classList = icon.$el.classList;
+ const containsSizeClass = classList.contains('s99');
+ const containsCustomClass = classList.contains('extraclasses');
+ expect(containsSizeClass).toBe(true);
+ expect(containsCustomClass).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
index 2cf4d8e00ed..24484796bf1 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
@@ -16,7 +16,7 @@ describe('Issue Warning Component', () => {
isLocked: true,
});
- expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock');
+ expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.');
});
});
@@ -27,7 +27,7 @@ describe('Issue Warning Component', () => {
isConfidential: true,
});
- expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash');
+ expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
});
});
@@ -39,7 +39,7 @@ describe('Issue Warning Component', () => {
isConfidential: true,
});
- expect(vm.$el.querySelector('i')).toBeFalsy();
+ expect(vm.$el.querySelector('.icon')).toBeFalsy();
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.');
});
});
diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js
index 97c8a08fcdd..49bf8ee6f7c 100644
--- a/spec/javascripts/vue_shared/components/loading_button_spec.js
+++ b/spec/javascripts/vue_shared/components/loading_button_spec.js
@@ -66,6 +66,23 @@ describe('LoadingButton', function () {
});
});
+ describe('container class', () => {
+ it('should default to btn btn-align-content', () => {
+ vm = mountComponent(LoadingButton, {});
+ expect(vm.$el.classList.contains('btn')).toEqual(true);
+ expect(vm.$el.classList.contains('btn-align-content')).toEqual(true);
+ });
+
+ it('should be configurable through props', () => {
+ vm = mountComponent(LoadingButton, {
+ containerClass: 'test-class',
+ });
+ expect(vm.$el.classList.contains('btn')).toEqual(false);
+ expect(vm.$el.classList.contains('btn-align-content')).toEqual(false);
+ expect(vm.$el.classList.contains('test-class')).toEqual(true);
+ });
+ });
+
describe('click callback prop', () => {
it('calls given callback when normal', () => {
vm = mountComponent(LoadingButton, {
@@ -81,7 +98,6 @@ describe('LoadingButton', function () {
it('does not call given callback when disabled because of loading', () => {
vm = mountComponent(LoadingButton, {
loading: true,
- indeterminate: true,
});
spyOn(vm, '$emit');
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 60a5c2ae74e..24209be83fe 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -1,6 +1,12 @@
import Vue from 'vue';
import fieldComponent from '~/vue_shared/components/markdown/field.vue';
+function assertMarkdownTabs(isWrite, writeLink, previewLink, vm) {
+ expect(writeLink.parentNode.classList.contains('active')).toEqual(isWrite);
+ expect(previewLink.parentNode.classList.contains('active')).toEqual(!isWrite);
+ expect(vm.$el.querySelector('.md-preview').style.display).toEqual(isWrite ? 'none' : '');
+}
+
describe('Markdown field component', () => {
let vm;
@@ -39,19 +45,23 @@ describe('Markdown field component', () => {
describe('markdown preview', () => {
let previewLink;
+ let writeLink;
beforeEach(() => {
spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => {
- resolve({
- json() {
- return {
- body: '<p>markdown preview</p>',
- };
- },
+ setTimeout(() => {
+ resolve({
+ json() {
+ return {
+ body: '<p>markdown preview</p>',
+ };
+ },
+ });
});
}));
- previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a');
+ previewLink = vm.$el.querySelector('.nav-links .js-preview-link');
+ writeLink = vm.$el.querySelector('.nav-links .js-write-link');
});
it('sets preview link as active', (done) => {
@@ -103,6 +113,23 @@ describe('Markdown field component', () => {
done();
}, 0);
});
+
+ it('clicking already active write or preview link does nothing', (done) => {
+ writeLink.click();
+ Vue.nextTick()
+ .then(() => assertMarkdownTabs(true, writeLink, previewLink, vm))
+ .then(() => writeLink.click())
+ .then(() => Vue.nextTick())
+ .then(() => assertMarkdownTabs(true, writeLink, previewLink, vm))
+ .then(() => previewLink.click())
+ .then(() => Vue.nextTick())
+ .then(() => assertMarkdownTabs(false, writeLink, previewLink, vm))
+ .then(() => previewLink.click())
+ .then(() => Vue.nextTick())
+ .then(() => assertMarkdownTabs(false, writeLink, previewLink, vm))
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('markdown buttons', () => {
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
index 7110ff36937..edebd822295 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -43,11 +43,13 @@ describe('Markdown field header component', () => {
it('emits toggle markdown event when clicking preview', () => {
spyOn(vm, '$emit');
- vm.$el.querySelector('li:nth-child(2) a').click();
+ vm.$el.querySelector('.js-preview-link').click();
- expect(
- vm.$emit,
- ).toHaveBeenCalledWith('toggle-markdown');
+ expect(vm.$emit).toHaveBeenCalledWith('preview-markdown');
+
+ vm.$el.querySelector('.js-write-link').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('write-markdown');
});
it('blurs preview link after click', (done) => {
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
new file mode 100644
index 00000000000..818ef0af3c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('toolbar', () => {
+ let vm;
+ const Toolbar = Vue.extend(toolbar);
+ const props = {
+ markdownDocsPath: '',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user can attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, props);
+ });
+
+ it('should render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).not.toBeNull();
+ });
+ });
+
+ describe('user cannot attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, Object.assign({}, props, {
+ canAttachFile: false,
+ }));
+ });
+
+ it('should not render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
new file mode 100644
index 00000000000..a5db0b2c59e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Skeleton loading container', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(skeletonLoadingContainer);
+ vm = mountComponent(component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders 6 skeleton lines by default', () => {
+ expect(vm.$el.querySelector('.skeleton-line-6')).not.toBeNull();
+ });
+
+ it('renders in full mode by default', () => {
+ expect(vm.$el.classList.contains('animation-container-small')).toBeFalsy();
+ });
+
+ describe('small', () => {
+ beforeEach((done) => {
+ vm.small = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('renders in small mode', () => {
+ expect(vm.$el.classList.contains('animation-container-small')).toBeTruthy();
+ });
+ });
+
+ describe('lines', () => {
+ beforeEach((done) => {
+ vm.lines = 5;
+
+ Vue.nextTick(done);
+ });
+
+ it('renders 5 lines', () => {
+ expect(vm.$el.querySelector('.skeleton-line-5')).not.toBeNull();
+ expect(vm.$el.querySelector('.skeleton-line-6')).toBeNull();
+ });
+ });
+});
diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb
index 580450eef1e..b5c1a360ba9 100644
--- a/spec/lib/additional_email_headers_interceptor_spec.rb
+++ b/spec/lib/additional_email_headers_interceptor_spec.rb
@@ -1,12 +1,29 @@
require 'spec_helper'
describe AdditionalEmailHeadersInterceptor do
- it 'adds Auto-Submitted header' do
- mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver
+ let(:mail) do
+ ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello')
+ end
+
+ before do
+ mail.deliver_now
+ end
+ it 'adds Auto-Submitted header' do
expect(mail.header['To'].value).to eq('test@mail.com')
expect(mail.header['From'].value).to eq('info@mail.com')
expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
end
+
+ context 'when the same mail object is sent twice' do
+ before do
+ mail.deliver_now
+ end
+
+ it 'does not add the Auto-Submitted header twice' do
+ expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
+ expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
+ end
+ end
end
diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb
index 049d025a5b9..84adaebdcbe 100644
--- a/spec/lib/banzai/commit_renderer_spec.rb
+++ b/spec/lib/banzai/commit_renderer_spec.rb
@@ -10,7 +10,7 @@ describe Banzai::CommitRenderer do
described_class::ATTRIBUTES.each do |attr|
expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr)
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, {})
end
described_class.render([project.commit], project, user)
diff --git a/spec/lib/banzai/filter/absolute_link_filter_spec.rb b/spec/lib/banzai/filter/absolute_link_filter_spec.rb
new file mode 100644
index 00000000000..a3ad056efcd
--- /dev/null
+++ b/spec/lib/banzai/filter/absolute_link_filter_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Banzai::Filter::AbsoluteLinkFilter do
+ def filter(doc, context = {})
+ described_class.call(doc, context)
+ end
+
+ context 'with html links' do
+ context 'if only_path is false' do
+ let(:only_path_context) do
+ { only_path: false }
+ end
+ let(:fake_url) { 'http://www.example.com' }
+
+ before do
+ allow(Gitlab.config.gitlab).to receive(:url).and_return(fake_url)
+ end
+
+ context 'has the .gfm class' do
+ it 'converts a relative url into absolute' do
+ doc = filter(link('/foo', 'gfm'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq "#{fake_url}/foo"
+ end
+
+ it 'does not change the url if it already absolute' do
+ doc = filter(link("#{fake_url}/foo", 'gfm'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq "#{fake_url}/foo"
+ end
+
+ context 'if relative_url_root is set' do
+ it 'joins the url without without doubling the path' do
+ allow(Gitlab.config.gitlab).to receive(:url).and_return("#{fake_url}/gitlab/")
+ doc = filter(link("/gitlab/foo", 'gfm'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq "#{fake_url}/gitlab/foo"
+ end
+ end
+ end
+
+ context 'has not the .gfm class' do
+ it 'does not convert a relative url into absolute' do
+ doc = filter(link('/foo'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq '/foo'
+ end
+ end
+ end
+
+ context 'if only_path is not false' do
+ it 'does not convert a relative url into absolute' do
+ expect(filter(link('/foo', 'gfm')).at_css('a')['href']).to eq '/foo'
+ expect(filter(link('/foo')).at_css('a')['href']).to eq '/foo'
+ end
+ end
+ end
+
+ def link(path, css_class = '')
+ %(<a class="#{css_class}" href="#{path}">example</a>)
+ end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 9c74c9b8c99..3c98b18f99b 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -317,6 +317,68 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
+ context 'group context' do
+ let(:group) { create(:group) }
+ let(:context) { { project: nil, group: group } }
+
+ it 'ignores shorthanded issue reference' do
+ reference = "##{issue.iid}"
+ text = "Fixed #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(described_class).to receive(:find_object)
+ .with(project, issue.iid)
+ .and_return(nil)
+
+ reference = "#{project.full_path}##{issue.iid}"
+ text = "Issue #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for complete cross-reference' do
+ reference = "#{project.full_path}##{issue.iid}"
+ doc = reference_filter("See #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project)
+ end
+
+ it 'ignores reference for shorthand cross-reference' do
+ reference = "#{project.path}##{issue.iid}"
+ text = "See #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for url cross-reference' do
+ reference = helper.url_for_issue(issue.iid, project) + "#note_123"
+
+ doc = reference_filter("See #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq(helper.url_for_issue(issue.iid, project) + "#note_123")
+ end
+
+ it 'links to a valid reference for cross-reference in link href' do
+ reference = "#{helper.url_for_issue(issue.iid, project) + "#note_123"}"
+ reference_link = %{<a href="#{reference}">Reference</a>}
+
+ doc = reference_filter("See #{reference_link}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + "#note_123"
+ end
+
+ it 'links to a valid reference for issue reference in the link href' do
+ reference = issue.to_reference(group)
+ reference_link = %{<a href="#{reference}">Reference</a>}
+ doc = reference_filter("See #{reference_link}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project)
+ end
+ end
+
describe '#issues_per_project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 2cd30a5e302..862b1fe3fd3 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do
expect(reference_filter(act).to_html).to eq exp
end
end
+
+ describe 'group context' do
+ it 'points to referenced project issues page' do
+ project = create(:project)
+ label = create(:label, project: project)
+ reference = "#{project.full_path}~#{label.name}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_issues_url(project, label_name: label.name))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index ed2788f8a33..158844e25ae 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -214,4 +214,14 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}!#{merge.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_merge_request_url(project, merge))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb
new file mode 100644
index 00000000000..532d25e121d
--- /dev/null
+++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MermaidFilter do
+ include FilterSpecHelper
+
+ it 'adds `js-render-mermaid` class to the `pre` tag' do
+ doc = filter("<pre class='code highlight js-syntax-highlight mermaid' lang='mermaid' v-pre='true'><code>graph TD;\n A--&gt;B;\n</code></pre>")
+ result = doc.xpath('descendant-or-self::pre').first
+
+ expect(result[:class]).to include('js-render-mermaid')
+ end
+end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index fe7a8c84c9e..6a9087d2e59 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -294,8 +294,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- context 'project milestones' do
- let(:milestone) { create(:milestone, project: project) }
+ shared_context 'project milestones' do
let(:reference) { milestone.to_reference(format: :iid) }
include_examples 'reference parsing'
@@ -309,8 +308,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
it_behaves_like 'cross project shorthand reference'
end
- context 'group milestones' do
- let(:milestone) { create(:milestone, group: group) }
+ shared_context 'group milestones' do
let(:reference) { milestone.to_reference(format: :name) }
include_examples 'reference parsing'
@@ -343,4 +341,43 @@ describe Banzai::Filter::MilestoneReferenceFilter do
expect(doc.css('a')).to be_empty
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ milestone = create(:milestone, project: project)
+ reference = "#{project.full_path}%#{milestone.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+ end
+
+ context 'when milestone is open' do
+ context 'project milestones' do
+ let(:milestone) { create(:milestone, project: project) }
+
+ include_context 'project milestones'
+ end
+
+ context 'group milestones' do
+ let(:milestone) { create(:milestone, group: group) }
+
+ include_context 'group milestones'
+ end
+ end
+
+ context 'when milestone is closed' do
+ context 'project milestones' do
+ let(:milestone) { create(:milestone, :closed, project: project) }
+
+ include_context 'project milestones'
+ end
+
+ context 'group milestones' do
+ let(:milestone) { create(:milestone, :closed, group: group) }
+
+ include_context 'group milestones'
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 90ac4c7b238..3a07a6dc179 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}$#{snippet.id}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 5a23e0e70cc..9f2efa05a01 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext" do
result = filter('<pre><code lang="ruby">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
end
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 34dac1db69a..fc03741976e 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do
end
end
+ context 'in group context' do
+ let(:group) { create(:group) }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_developer(group_member)
+ end
+
+ let(:context) { { author: group_member, project: nil, group: group } }
+
+ it 'supports a special @all mention' do
+ reference = User.reference_prefix + 'all'
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
+
+ it 'supports mentioning a single user' do
+ reference = group_member.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member)
+ end
+
+ it 'supports mentioning a group' do
+ reference = group.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group)
+ end
+ end
+
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
deleted file mode 100644
index 32764bee5eb..00000000000
--- a/spec/lib/banzai/note_renderer_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::NoteRenderer do
- describe '.render' do
- it 'renders a Note' do
- note = double(:note)
- project = double(:project)
- wiki = double(:wiki)
- user = double(:user)
-
- expect(Banzai::ObjectRenderer).to receive(:new)
- .with(project, user,
- requested_path: 'foo',
- project_wiki: wiki,
- ref: 'bar')
- .and_call_original
-
- expect_any_instance_of(Banzai::ObjectRenderer)
- .to receive(:render).with([note], :note)
-
- described_class.render([note], project, user, 'foo', wiki, 'bar')
- end
- end
-end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index b172a1b718c..074d521a5c6 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -22,7 +22,7 @@ describe Banzai::ObjectRenderer do
end
it 'retrieves field content using Banzai::Renderer.render_field' do
- expect(Banzai::Renderer).to receive(:render_field).with(object, :note).and_call_original
+ expect(Banzai::Renderer).to receive(:render_field).with(object, :note, {}).and_call_original
renderer.render([object], :note)
end
@@ -68,7 +68,7 @@ describe Banzai::ObjectRenderer do
end
it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title).and_call_original
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title, {}).and_call_original
renderer.render([commit], :title)
end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 81a04a2d46d..650cecfc778 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -18,7 +18,7 @@ describe Banzai::Renderer do
let(:commit) { create(:project, :repository).commit }
it 'returns cacheless render field' do
- expect(renderer).to receive(:cacheless_render_field).with(commit, :title)
+ expect(renderer).to receive(:cacheless_render_field).with(commit, :title, {})
renderer.render_field(commit, :title)
end
diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb
index 84cacdd3f0d..010deae822c 100644
--- a/spec/lib/container_registry/path_spec.rb
+++ b/spec/lib/container_registry/path_spec.rb
@@ -86,6 +86,24 @@ describe ContainerRegistry::Path do
it { is_expected.to be_valid }
end
+
+ context 'when path contains double underscore' do
+ let(:path) { 'my/repository__name' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when path contains invalid separator with dot' do
+ let(:path) { 'some/registry-.name' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when path contains invalid separator with underscore' do
+ let(:path) { 'some/registry._name' }
+
+ it { is_expected.not_to be_valid }
+ end
end
describe '#has_repository?' do
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 1076c63b5f2..10020511bf8 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -13,6 +13,47 @@ describe Feature do
end
end
+ describe '.persisted_names' do
+ it 'returns the names of the persisted features' do
+ Feature::FlipperFeature.create!(key: 'foo')
+
+ expect(described_class.persisted_names).to eq(%w[foo])
+ end
+
+ it 'returns an empty Array when no features are presisted' do
+ expect(described_class.persisted_names).to be_empty
+ end
+
+ it 'caches the feature names when request store is active', :request_store do
+ Feature::FlipperFeature.create!(key: 'foo')
+
+ expect(Feature::FlipperFeature)
+ .to receive(:feature_names)
+ .once
+ .and_call_original
+
+ 2.times do
+ expect(described_class.persisted_names).to eq(%w[foo])
+ end
+ end
+ end
+
+ describe '.persisted?' do
+ it 'returns true for a persisted feature' do
+ Feature::FlipperFeature.create!(key: 'foo')
+
+ feature = double(:feature, name: 'foo')
+
+ expect(described_class.persisted?(feature)).to eq(true)
+ end
+
+ it 'returns false for a feature that is not persisted' do
+ feature = double(:feature, name: 'foo')
+
+ expect(described_class.persisted?(feature)).to eq(false)
+ end
+ end
+
describe '.all' do
let(:features) { Set.new }
diff --git a/spec/lib/github/client_spec.rb b/spec/lib/github/client_spec.rb
deleted file mode 100644
index b846096fe25..00000000000
--- a/spec/lib/github/client_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe Github::Client do
- let(:connection) { spy }
- let(:rate_limit) { double(get: [false, 1]) }
- let(:client) { described_class.new({}) }
- let(:results) { double }
- let(:response) { double }
-
- before do
- allow(Faraday).to receive(:new).and_return(connection)
- allow(Github::RateLimit).to receive(:new).with(connection).and_return(rate_limit)
- end
-
- describe '#get' do
- before do
- allow(Github::Response).to receive(:new).with(results).and_return(response)
- end
-
- it 'uses a default per_page param' do
- expect(connection).to receive(:get).with('/foo', per_page: 100).and_return(results)
-
- expect(client.get('/foo')).to eq(response)
- end
-
- context 'with per_page given' do
- it 'overwrites the default per_page' do
- expect(connection).to receive(:get).with('/foo', per_page: 30).and_return(results)
-
- expect(client.get('/foo', per_page: 30)).to eq(response)
- end
- end
- end
-end
diff --git a/spec/lib/github/import/legacy_diff_note_spec.rb b/spec/lib/github/import/legacy_diff_note_spec.rb
deleted file mode 100644
index 8c50b46cacb..00000000000
--- a/spec/lib/github/import/legacy_diff_note_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'spec_helper'
-
-describe Github::Import::LegacyDiffNote do
- describe '#type' do
- it 'returns the original note type' do
- expect(described_class.new.type).to eq('LegacyDiffNote')
- end
- end
-end
diff --git a/spec/lib/github/import/note_spec.rb b/spec/lib/github/import/note_spec.rb
deleted file mode 100644
index fcdccd9e097..00000000000
--- a/spec/lib/github/import/note_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'spec_helper'
-
-describe Github::Import::Note do
- describe '#type' do
- it 'returns the original note type' do
- expect(described_class.new.type).to eq('Note')
- end
- end
-end
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
new file mode 100644
index 00000000000..ffcd90b9fcb
--- /dev/null
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::RequestAuthenticator do
+ let(:env) do
+ {
+ 'rack.input' => '',
+ 'REQUEST_METHOD' => 'GET'
+ }
+ end
+ let(:request) { ActionDispatch::Request.new(env) }
+
+ subject { described_class.new(request) }
+
+ describe '#user' do
+ let!(:sessionless_user) { build(:user) }
+ let!(:session_user) { build(:user) }
+
+ it 'returns sessionless user first' do
+ allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user)
+ allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
+
+ expect(subject.user).to eq sessionless_user
+ end
+
+ it 'returns session user if no sessionless user found' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
+
+ expect(subject.user).to eq session_user
+ end
+
+ it 'returns nil if no user found' do
+ expect(subject.user).to be_blank
+ end
+
+ it 'bubbles up exceptions' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_raise(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ describe '#find_sessionless_user' do
+ let!(:access_token_user) { build(:user) }
+ let!(:rss_token_user) { build(:user) }
+
+ it 'returns access_token user first' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user)
+ allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
+
+ expect(subject.find_sessionless_user).to eq access_token_user
+ end
+
+ it 'returns rss_token user if no access_token user found' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
+
+ expect(subject.find_sessionless_user).to eq rss_token_user
+ end
+
+ it 'returns nil if no user found' do
+ expect(subject.find_sessionless_user).to be_blank
+ end
+
+ it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError)
+
+ expect(subject.find_sessionless_user).to be_blank
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
new file mode 100644
index 00000000000..4637816570c
--- /dev/null
+++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
@@ -0,0 +1,194 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::UserAuthFinders do
+ include described_class
+
+ let(:user) { create(:user) }
+ let(:env) do
+ {
+ 'rack.input' => ''
+ }
+ end
+ let(:request) { Rack::Request.new(env)}
+
+ def set_param(key, value)
+ request.update_param(key, value)
+ end
+
+ describe '#find_user_from_warden' do
+ context 'with CSRF token' do
+ before do
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true)
+ end
+
+ context 'with invalid credentials' do
+ it 'returns nil' do
+ expect(find_user_from_warden).to be_nil
+ end
+ end
+
+ context 'with valid credentials' do
+ it 'returns the user' do
+ env['warden'] = double("warden", authenticate: user)
+
+ expect(find_user_from_warden).to eq user
+ end
+ end
+ end
+
+ context 'without CSRF token' do
+ it 'returns nil' do
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false)
+ env['warden'] = double("warden", authenticate: user)
+
+ expect(find_user_from_warden).to be_nil
+ end
+ end
+ end
+
+ describe '#find_user_from_rss_token' do
+ context 'when the request format is atom' do
+ before do
+ env['HTTP_ACCEPT'] = 'application/atom+xml'
+ end
+
+ it 'returns user if valid rss_token' do
+ set_param(:rss_token, user.rss_token)
+
+ expect(find_user_from_rss_token).to eq user
+ end
+
+ it 'returns nil if rss_token is blank' do
+ expect(find_user_from_rss_token).to be_nil
+ end
+
+ it 'returns exception if invalid rss_token' do
+ set_param(:rss_token, 'invalid_token')
+
+ expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ context 'when the request format is not atom' do
+ it 'returns nil' do
+ set_param(:rss_token, user.rss_token)
+
+ expect(find_user_from_rss_token).to be_nil
+ end
+ end
+ end
+
+ describe '#find_user_from_access_token' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it 'returns nil if no access_token present' do
+ expect(find_personal_access_token).to be_nil
+ end
+
+ context 'when validate_access_token! returns valid' do
+ it 'returns user' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect(find_user_from_access_token).to eq user
+ end
+
+ it 'returns exception if token has no user' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
+
+ expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+ end
+
+ describe '#find_personal_access_token' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ context 'passed as header' do
+ it 'returns token if valid personal_access_token' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect(find_personal_access_token).to eq personal_access_token
+ end
+ end
+
+ context 'passed as param' do
+ it 'returns token if valid personal_access_token' do
+ set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, personal_access_token.token)
+
+ expect(find_personal_access_token).to eq personal_access_token
+ end
+ end
+
+ it 'returns nil if no personal_access_token' do
+ expect(find_personal_access_token).to be_nil
+ end
+
+ it 'returns exception if invalid personal_access_token' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid_token'
+
+ expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ describe '#find_oauth_access_token' do
+ let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
+ let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
+
+ context 'passed as header' do
+ it 'returns token if valid oauth_access_token' do
+ env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
+
+ expect(find_oauth_access_token.token).to eq token.token
+ end
+ end
+
+ context 'passed as param' do
+ it 'returns user if valid oauth_access_token' do
+ set_param(:access_token, token.token)
+
+ expect(find_oauth_access_token.token).to eq token.token
+ end
+ end
+
+ it 'returns nil if no oauth_access_token' do
+ expect(find_oauth_access_token).to be_nil
+ end
+
+ it 'returns exception if invalid oauth_access_token' do
+ env['HTTP_AUTHORIZATION'] = "Bearer invalid_token"
+
+ expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ describe '#validate_access_token!' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it 'returns nil if no access_token present' do
+ expect(validate_access_token!).to be_nil
+ end
+
+ context 'token is not valid' do
+ before do
+ allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
+ end
+
+ it 'returns Gitlab::Auth::ExpiredError if token expired' do
+ personal_access_token.expires_at = 1.day.ago
+
+ expect { validate_access_token! }.to raise_error(Gitlab::Auth::ExpiredError)
+ end
+
+ it 'returns Gitlab::Auth::RevokedError if token revoked' do
+ personal_access_token.revoke!
+
+ expect { validate_access_token! }.to raise_error(Gitlab::Auth::RevokedError)
+ end
+
+ it 'returns Gitlab::Auth::InsufficientScopeError if invalid token scope' do
+ expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index af1db2c3455..3164d2ebf04 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Auth do
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
- expect(subject::API_SCOPES).to eq [:api, :read_user]
+ expect(subject::API_SCOPES).to eq %i[api read_user sudo]
end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.optional_scopes).to eq %i[read_user read_registry openid]
+ expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid]
end
context 'registry_scopes' do
@@ -133,6 +133,25 @@ describe Gitlab::Auth do
gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')
end
+
+ it 'grants deploy key write permissions' do
+ project = create(:project)
+ key = create(:deploy_key, can_push: true)
+ create(:deploy_keys_project, deploy_key: key, project: project)
+ token = Gitlab::LfsToken.new(key).token
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_write_authentication_abilities))
+ end
+
+ it 'does not grant deploy key write permissions' do
+ project = create(:project)
+ key = create(:deploy_key, can_push: true)
+ token = Gitlab::LfsToken.new(key).token
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ end
end
context 'while using OAuth tokens as passwords' do
@@ -164,7 +183,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, full_authentication_abilities))
end
context 'when registry is enabled' do
@@ -176,7 +195,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image]))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image]))
end
end
@@ -184,14 +203,14 @@ describe Gitlab::Auth do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_access_token, full_authentication_abilities))
end
it 'limits abilities based on scope' do
personal_access_token = create(:personal_access_token, scopes: ['read_user'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, []))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, []))
end
it 'fails if password is nil' do
@@ -234,7 +253,7 @@ describe Gitlab::Auth do
it 'throws an error suggesting user create a PAT when internal auth is disabled' do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false }
- expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError)
+ expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
end
@@ -326,10 +345,15 @@ describe Gitlab::Auth do
]
end
- def full_authentication_abilities
+ def read_write_authentication_abilities
read_authentication_abilities + [
:push_code,
- :create_container_image,
+ :create_container_image
+ ]
+ end
+
+ def full_authentication_abilities
+ read_write_authentication_abilities + [
:admin_container_image
]
end
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
index 1a4ea2bac48..79d2c071446 100644
--- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -93,7 +93,14 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
end
it 'knows it is finished for this range' do
- expect(migration.missing_members?(1, 7)).to be_falsy
+ expect(migration.missing_members?(1, 8)).to be_falsy
+ end
+
+ it 'does not miss members for forks of forks for which the root was deleted' do
+ forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id)
+ base1.destroy
+
+ expect(migration.missing_members?(7, 10)).to be_falsy
end
context 'with more forks' do
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
index 2c2684a6fc9..994992f79d4 100644
--- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -3,12 +3,9 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do
let(:migration) { described_class.new }
let(:base1) { create(:project) }
- let(:base1_fork1) { create(:project) }
- let(:base1_fork2) { create(:project) }
let(:base2) { create(:project) }
let(:base2_fork1) { create(:project) }
- let(:base2_fork2) { create(:project) }
let!(:forked_project_links) { table(:forked_project_links) }
let!(:fork_networks) { table(:fork_networks) }
@@ -21,21 +18,24 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
# A normal fork link
forked_project_links.create(id: 1,
forked_from_project_id: base1.id,
- forked_to_project_id: base1_fork1.id)
+ forked_to_project_id: create(:project).id)
forked_project_links.create(id: 2,
forked_from_project_id: base1.id,
- forked_to_project_id: base1_fork2.id)
-
+ forked_to_project_id: create(:project).id)
forked_project_links.create(id: 3,
forked_from_project_id: base2.id,
forked_to_project_id: base2_fork1.id)
+
+ # create a fork of a fork
forked_project_links.create(id: 4,
forked_from_project_id: base2_fork1.id,
forked_to_project_id: create(:project).id)
-
forked_project_links.create(id: 5,
- forked_from_project_id: base2.id,
- forked_to_project_id: base2_fork2.id)
+ forked_from_project_id: create(:project).id,
+ forked_to_project_id: create(:project).id)
+
+ # Stub out the calls to the other migrations
+ allow(BackgroundMigrationWorker).to receive(:perform_in)
migration.perform(1, 3)
end
@@ -80,11 +80,11 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
end
it 'only processes a single batch of links at a time' do
- expect(fork_network_members.count).to eq(5)
+ expect(fork_networks.count).to eq(2)
migration.perform(3, 5)
- expect(fork_network_members.count).to eq(7)
+ expect(fork_networks.count).to eq(3)
end
it 'can be repeated without effect' do
diff --git a/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb
new file mode 100644
index 00000000000..0cb753c5853
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateMergeRequestsLatestMergeRequestDiffId, :migration, schema: 20171026082505 do
+ let(:projects_table) { table(:projects) }
+ let(:merge_requests_table) { table(:merge_requests) }
+ let(:merge_request_diffs_table) { table(:merge_request_diffs) }
+
+ let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') }
+
+ def create_mr!(name, diffs: 0)
+ merge_request =
+ merge_requests_table.create!(target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: name,
+ title: name)
+
+ diffs.times do
+ merge_request_diffs_table.create!(merge_request_id: merge_request.id)
+ end
+
+ merge_request
+ end
+
+ def diffs_for(merge_request)
+ merge_request_diffs_table.where(merge_request_id: merge_request.id)
+ end
+
+ describe '#perform' do
+ it 'ignores MRs without diffs' do
+ merge_request_without_diff = create_mr!('without_diff')
+ mr_id = merge_request_without_diff.id
+
+ expect(merge_request_without_diff.latest_merge_request_diff_id).to be_nil
+
+ expect { subject.perform(mr_id, mr_id) }
+ .not_to change { merge_request_without_diff.reload.latest_merge_request_diff_id }
+ end
+
+ it 'ignores MRs that have a diff ID already set' do
+ merge_request_with_multiple_diffs = create_mr!('with_multiple_diffs', diffs: 3)
+ diff_id = diffs_for(merge_request_with_multiple_diffs).minimum(:id)
+ mr_id = merge_request_with_multiple_diffs.id
+
+ merge_request_with_multiple_diffs.update!(latest_merge_request_diff_id: diff_id)
+
+ expect { subject.perform(mr_id, mr_id) }
+ .not_to change { merge_request_with_multiple_diffs.reload.latest_merge_request_diff_id }
+ end
+
+ it 'migrates multiple MR diffs to the correct values' do
+ merge_requests = Array.new(3).map.with_index { |_, i| create_mr!(i, diffs: 3) }
+
+ subject.perform(merge_requests.first.id, merge_requests.last.id)
+
+ merge_requests.each do |merge_request|
+ expect(merge_request.reload.latest_merge_request_diff_id)
+ .to eq(diffs_for(merge_request).maximum(:id))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
new file mode 100644
index 00000000000..7f3bf5fc41c
--- /dev/null
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe Gitlab::BareRepositoryImport::Importer, repository: true do
+ let!(:admin) { create(:admin) }
+ let!(:base_dir) { Dir.mktmpdir + '/' }
+ let(:bare_repository) { Gitlab::BareRepositoryImport::Repository.new(base_dir, File.join(base_dir, "#{project_path}.git")) }
+
+ subject(:importer) { described_class.new(admin, bare_repository) }
+
+ before do
+ allow(described_class).to receive(:log)
+ end
+
+ after do
+ FileUtils.rm_rf(base_dir)
+ end
+
+ shared_examples 'importing a repository' do
+ describe '.execute' do
+ it 'creates a project for a repository in storage' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+ fake_importer = double
+
+ expect(described_class).to receive(:new).and_return(fake_importer)
+ expect(fake_importer).to receive(:create_project_if_needed)
+
+ described_class.execute(base_dir)
+ end
+
+ it 'skips wiki repos' do
+ repo_dir = File.join(base_dir, 'the-group', 'the-project.wiki.git')
+ FileUtils.mkdir_p(File.join(repo_dir))
+
+ expect(described_class).to receive(:log).with(" * Skipping repo #{repo_dir}")
+ expect(described_class).not_to receive(:new)
+
+ described_class.execute(base_dir)
+ end
+
+ context 'without admin users' do
+ let(:admin) { nil }
+
+ it 'raises an error' do
+ expect { described_class.execute(base_dir) }.to raise_error(Gitlab::BareRepositoryImport::Importer::NoAdminError)
+ end
+ end
+ end
+
+ describe '#create_project_if_needed' do
+ it 'starts an import for a project that did not exist' do
+ expect(importer).to receive(:create_project)
+
+ importer.create_project_if_needed
+ end
+
+ it 'skips importing when the project already exists' do
+ project = create(:project, path: 'a-project', namespace: existing_group)
+
+ expect(importer).not_to receive(:create_project)
+ expect(importer).to receive(:log).with(" * #{project.name} (#{project_path}) exists")
+
+ importer.create_project_if_needed
+ end
+
+ it 'creates a project with the correct path in the database' do
+ importer.create_project_if_needed
+
+ expect(Project.find_by_full_path(project_path)).not_to be_nil
+ end
+
+ it 'creates the Git repo in disk' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+
+ importer.create_project_if_needed
+
+ project = Project.find_by_full_path(project_path)
+
+ expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
+ end
+
+ context 'hashed storage enabled' do
+ it 'creates a project with the correct path in the database' do
+ stub_application_setting(hashed_storage_enabled: true)
+
+ importer.create_project_if_needed
+
+ expect(Project.find_by_full_path(project_path)).not_to be_nil
+ end
+ end
+ end
+ end
+
+ context 'with subgroups', :nested_groups do
+ let(:project_path) { 'a-group/a-sub-group/a-project' }
+
+ let(:existing_group) do
+ group = create(:group, path: 'a-group')
+ create(:group, path: 'a-sub-group', parent: group)
+ end
+
+ it_behaves_like 'importing a repository'
+ end
+
+ context 'without subgroups' do
+ let(:project_path) { 'a-group/a-project' }
+ let(:existing_group) { create(:group, path: 'a-group') }
+
+ it_behaves_like 'importing a repository'
+ end
+
+ context 'without groups' do
+ let(:project_path) { 'a-project' }
+
+ it 'starts an import for a project that did not exist' do
+ expect(importer).to receive(:create_project)
+
+ importer.create_project_if_needed
+ end
+
+ it 'creates a project with the correct path in the database' do
+ importer.create_project_if_needed
+
+ expect(Project.find_by_full_path("#{admin.full_path}/#{project_path}")).not_to be_nil
+ end
+
+ it 'creates the Git repo in disk' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+
+ importer.create_project_if_needed
+
+ project = Project.find_by_full_path("#{admin.full_path}/#{project_path}")
+
+ expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
+ end
+ end
+
+ context 'with Wiki' do
+ let(:project_path) { 'a-group/a-project' }
+ let(:existing_group) { create(:group, path: 'a-group') }
+
+ it_behaves_like 'importing a repository'
+
+ it 'creates the Wiki git repo in disk' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.wiki.git"))
+
+ importer.create_project_if_needed
+
+ project = Project.find_by_full_path(project_path)
+
+ expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ end
+ end
+
+ context 'when subgroups are not available' do
+ let(:project_path) { 'a-group/a-sub-group/a-project' }
+
+ before do
+ expect(Group).to receive(:supports_nested_groups?) { false }
+ end
+
+ describe '#create_project_if_needed' do
+ it 'raises an error' do
+ expect { importer.create_project_if_needed }.to raise_error('Nested groups are not supported on MySQL')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
new file mode 100644
index 00000000000..2db737f5fb6
--- /dev/null
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe ::Gitlab::BareRepositoryImport::Repository do
+ let(:project_repo_path) { described_class.new('/full/path/', '/full/path/to/repo.git') }
+
+ it 'stores the repo path' do
+ expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git')
+ end
+
+ it 'stores the group path' do
+ expect(project_repo_path.group_path).to eq('to')
+ end
+
+ it 'stores the project name' do
+ expect(project_repo_path.project_name).to eq('repo')
+ end
+
+ it 'stores the wiki path' do
+ expect(project_repo_path.wiki_path).to eq('/full/path/to/repo.wiki.git')
+ end
+
+ describe '#wiki?' do
+ it 'returns true if it is a wiki' do
+ wiki_path = described_class.new('/full/path/', '/full/path/to/a/b/my.wiki.git')
+
+ expect(wiki_path.wiki?).to eq(true)
+ end
+
+ it 'returns false if it is not a wiki' do
+ expect(project_repo_path.wiki?).to eq(false)
+ end
+ end
+
+ describe '#hashed?' do
+ it 'returns true if it is a hashed folder' do
+ path = described_class.new('/full/path/', '/full/path/@hashed/my.repo.git')
+
+ expect(path.hashed?).to eq(true)
+ end
+
+ it 'returns false if it is not a hashed folder' do
+ expect(project_repo_path.hashed?).to eq(false)
+ end
+ end
+
+ describe '#project_full_path' do
+ it 'returns the project full path' do
+ expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_importer_spec.rb b/spec/lib/gitlab/bare_repository_importer_spec.rb
deleted file mode 100644
index 36d1844b5b1..00000000000
--- a/spec/lib/gitlab/bare_repository_importer_spec.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::BareRepositoryImporter, repository: true do
- subject(:importer) { described_class.new('default', project_path) }
-
- let!(:admin) { create(:admin) }
-
- before do
- allow(described_class).to receive(:log)
- end
-
- shared_examples 'importing a repository' do
- describe '.execute' do
- it 'creates a project for a repository in storage' do
- FileUtils.mkdir_p(File.join(TestEnv.repos_path, "#{project_path}.git"))
- fake_importer = double
-
- expect(described_class).to receive(:new).with('default', project_path)
- .and_return(fake_importer)
- expect(fake_importer).to receive(:create_project_if_needed)
-
- described_class.execute
- end
-
- it 'skips wiki repos' do
- FileUtils.mkdir_p(File.join(TestEnv.repos_path, 'the-group', 'the-project.wiki.git'))
-
- expect(described_class).to receive(:log).with(' * Skipping wiki repo')
- expect(described_class).not_to receive(:new)
-
- described_class.execute
- end
- end
-
- describe '#initialize' do
- context 'without admin users' do
- let(:admin) { nil }
-
- it 'raises an error' do
- expect { importer }.to raise_error(Gitlab::BareRepositoryImporter::NoAdminError)
- end
- end
- end
-
- describe '#create_project_if_needed' do
- it 'starts an import for a project that did not exist' do
- expect(importer).to receive(:create_project)
-
- importer.create_project_if_needed
- end
-
- it 'skips importing when the project already exists' do
- project = create(:project, path: 'a-project', namespace: existing_group)
-
- expect(importer).not_to receive(:create_project)
- expect(importer).to receive(:log).with(" * #{project.name} (#{project_path}) exists")
-
- importer.create_project_if_needed
- end
-
- it 'creates a project with the correct path in the database' do
- importer.create_project_if_needed
-
- expect(Project.find_by_full_path(project_path)).not_to be_nil
- end
- end
- end
-
- context 'with subgroups', :nested_groups do
- let(:project_path) { 'a-group/a-sub-group/a-project' }
-
- let(:existing_group) do
- group = create(:group, path: 'a-group')
- create(:group, path: 'a-sub-group', parent: group)
- end
-
- it_behaves_like 'importing a repository'
- end
-
- context 'without subgroups' do
- let(:project_path) { 'a-group/a-project' }
- let(:existing_group) { create(:group, path: 'a-group') }
-
- it_behaves_like 'importing a repository'
- end
-
- context 'when subgroups are not available' do
- let(:project_path) { 'a-group/a-sub-group/a-project' }
-
- before do
- expect(Group).to receive(:supports_nested_groups?) { false }
- end
-
- describe '#create_project_if_needed' do
- it 'raises an error' do
- expect { importer.create_project_if_needed }.to raise_error('Nested groups are not supported on MySQL')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index a66347ead76..a6a1d9e619f 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do
create(
:project,
import_source: project_identifier,
+ import_url: "https://bitbucket.org/#{project_identifier}.git",
import_data_attributes: { credentials: data }
)
end
let(:importer) { described_class.new(project) }
+ let(:gitlab_shell) { double }
let(:issues_statuses_sample_data) do
{
@@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do
}
end
+ before do
+ allow(importer).to receive(:gitlab_shell) { gitlab_shell }
+ end
+
context 'issues statuses' do
before do
# HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
@@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do
end
it 'maps statuses to open or closed' do
+ allow(importer).to receive(:import_wiki)
+
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
expect(project.issues.where(state: "opened").size).to eq(2)
end
- it 'calls import_wiki' do
- expect(importer).to receive(:import_wiki)
- importer.execute
+ describe 'wiki import' do
+ it 'is skipped when the wiki exists' do
+ expect(project.wiki).to receive(:repository_exists?) { true }
+ expect(importer.gitlab_shell).not_to receive(:import_repository)
+
+ importer.execute
+
+ expect(importer.errors).to be_empty
+ end
+
+ it 'imports to the project disk_path' do
+ expect(project.wiki).to receive(:repository_exists?) { false }
+ expect(importer.gitlab_shell).to receive(:import_repository).with(
+ project.repository_storage_path,
+ project.wiki.disk_path,
+ project.import_url + '/wiki'
+ )
+
+ importer.execute
+
+ expect(importer.errors).to be_empty
+ end
end
end
end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 6c25b7349e1..c2bca816aae 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -11,13 +11,13 @@ describe Gitlab::Checks::ChangeAccess do
let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
let(:protocol) { 'ssh' }
- subject do
+ subject(:change_access) do
described_class.new(
changes,
project: project,
user_access: user_access,
protocol: protocol
- ).exec
+ )
end
before do
@@ -26,7 +26,7 @@ describe Gitlab::Checks::ChangeAccess do
context 'without failed checks' do
it "doesn't raise an error" do
- expect { subject }.not_to raise_error
+ expect { subject.exec }.not_to raise_error
end
end
@@ -34,7 +34,7 @@ describe Gitlab::Checks::ChangeAccess do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end
end
@@ -45,7 +45,7 @@ describe Gitlab::Checks::ChangeAccess do
allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
end
context 'with protected tag' do
@@ -61,7 +61,7 @@ describe Gitlab::Checks::ChangeAccess do
let(:newrev) { '0000000000000000000000000000000000000000' }
it 'is prevented' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
end
end
@@ -70,7 +70,7 @@ describe Gitlab::Checks::ChangeAccess do
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
it 'is prevented' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
end
end
end
@@ -81,14 +81,14 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/tags/v9.1.0' }
it 'prevents creation below access level' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
end
context 'when user has access' do
let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
it 'allows tag creation' do
- expect { subject }.not_to raise_error
+ expect { subject.exec }.not_to raise_error
end
end
end
@@ -101,7 +101,7 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/heads/master' }
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
end
end
@@ -114,7 +114,7 @@ describe Gitlab::Checks::ChangeAccess do
it 'raises an error if the user is not allowed to do forced pushes to protected branches' do
expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
end
it 'raises an error if the user is not allowed to merge to protected branches' do
@@ -122,13 +122,13 @@ describe Gitlab::Checks::ChangeAccess do
expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
end
it 'raises an error if the user is not allowed to push to protected branches' do
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
end
context 'branch deletion' do
@@ -137,7 +137,7 @@ describe Gitlab::Checks::ChangeAccess do
context 'if the user is not allowed to delete protected branches' do
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.')
end
end
@@ -150,18 +150,32 @@ describe Gitlab::Checks::ChangeAccess do
let(:protocol) { 'web' }
it 'allows branch deletion' do
- expect { subject }.not_to raise_error
+ expect { subject.exec }.not_to raise_error
end
end
context 'over SSH or HTTP' do
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
end
end
end
end
end
end
+
+ context 'LFS integrity check' do
+ it 'fails if any LFS blobs are missing' do
+ allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(true)
+
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/)
+ end
+
+ it 'succeeds if LFS objects have already been uploaded' do
+ allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(false)
+
+ expect { subject.exec }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
new file mode 100644
index 00000000000..17756621221
--- /dev/null
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::LfsIntegrity do
+ include ProjectForksHelper
+ let(:project) { create(:project, :repository) }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+
+ subject { described_class.new(project, newrev) }
+
+ describe '#objects_missing?' do
+ let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
+
+ before do
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block|
+ lazy_block.call([blob_object.id])
+ end
+ end
+
+ context 'with LFS not enabled' do
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+
+ subject.objects_missing?
+ end
+ end
+
+ context 'with LFS enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ context 'deletion' do
+ let(:newrev) { nil }
+
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
+ it 'is true if any LFS blobs are missing' do
+ expect(subject.objects_missing?).to be_truthy
+ end
+
+ it 'is false if LFS objects have already been uploaded' do
+ lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
+ create(:lfs_objects_project, project: project, lfs_object: lfs_object)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
+ context 'for forked project' do
+ let(:parent_project) { create(:project, :repository) }
+ let(:project) { fork_project(parent_project, nil, repository: true) }
+
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ it 'is true parent project is missing LFS objects' do
+ expect(subject.objects_missing?).to be_truthy
+ end
+
+ it 'is false parent project already conatins LFS objects for the fork' do
+ lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
+ create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 809fda11879..2a3f7807fdb 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -77,8 +77,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when PST (Pacific Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when PDT (Pacific Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
end
@@ -100,8 +112,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when CET (Central European Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when CEST (Central European Summer Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
@@ -111,8 +135,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when EST (Eastern Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when EDT (Eastern Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
index 5a7a42d84c0..9cdebaa5cf2 100644
--- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_cancel' }
+ it { expect(subject.action_icon).to eq 'cancel' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 8768302eda1..d196bc6a4c2 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'passed'
- expect(status.icon).to eq 'icon_status_success'
+ expect(status.icon).to eq 'status_success'
expect(status.favicon).to eq 'favicon_status_success'
expect(status.label).to eq 'passed'
expect(status).to have_details
@@ -57,7 +57,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_failed'
+ expect(status.icon).to eq 'status_failed'
expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
@@ -84,7 +84,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_warning'
+ expect(status.icon).to eq 'status_warning'
expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed (allowed to fail)'
expect(status).to have_details
@@ -113,7 +113,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'canceled'
- expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.icon).to eq 'status_canceled'
expect(status.favicon).to eq 'favicon_status_canceled'
expect(status.label).to eq 'canceled'
expect(status).to have_details
@@ -139,7 +139,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'running'
- expect(status.icon).to eq 'icon_status_running'
+ expect(status.icon).to eq 'status_running'
expect(status.favicon).to eq 'favicon_status_running'
expect(status.label).to eq 'running'
expect(status).to have_details
@@ -165,7 +165,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'pending'
- expect(status.icon).to eq 'icon_status_pending'
+ expect(status.icon).to eq 'status_pending'
expect(status.favicon).to eq 'favicon_status_pending'
expect(status.label).to eq 'pending'
expect(status).to have_details
@@ -190,7 +190,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'skipped'
- expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.icon).to eq 'status_skipped'
expect(status.favicon).to eq 'favicon_status_skipped'
expect(status.label).to eq 'skipped'
expect(status).to have_details
@@ -219,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
- expect(status.icon).to eq 'icon_status_manual'
+ expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to include 'manual play action'
expect(status).to have_details
@@ -274,7 +274,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
- expect(status.icon).to eq 'icon_status_manual'
+ expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to eq 'manual stop action (not allowed)'
expect(status).to have_details
diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
index 20f71459738..99a5a7e4aca 100644
--- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do
describe '#icon' do
it 'returns a warning icon' do
- expect(subject.icon).to eq 'icon_status_warning'
+ expect(subject.icon).to eq 'status_warning'
end
end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index 32b2e62e4e0..81d5f553fd1 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Play do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_play' }
+ it { expect(subject.action_icon).to eq 'play' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
index 21026f2c968..14d42e0d70f 100644
--- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Retryable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_retry' }
+ it { expect(subject.action_icon).to eq 'retry' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index e0425103f41..18e250772f0 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -38,7 +38,7 @@ describe Gitlab::Ci::Status::Build::Stop do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_stop' }
+ it { expect(subject.action_icon).to eq 'stop' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 530639a5897..dc74d7e28c5 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Canceled do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_canceled' }
+ it { expect(subject.icon).to eq 'status_canceled' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index aef982e17f1..ce4333f2aca 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Created do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_created' }
+ it { expect(subject.icon).to eq 'status_created' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 9a25743885c..a4a92117c7f 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Failed do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_failed' }
+ it { expect(subject.icon).to eq 'status_failed' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
index 6fdc3801d71..0463f2e1aff 100644
--- a/spec/lib/gitlab/ci/status/manual_spec.rb
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Manual do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_manual' }
+ it { expect(subject.icon).to eq 'status_manual' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index ffc53f0506b..0e25358dd8a 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Pending do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_pending' }
+ it { expect(subject.icon).to eq 'status_pending' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 0babf1fb54e..9c9d431bb5d 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Running do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_running' }
+ it { expect(subject.icon).to eq 'status_running' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index 670747c9f0b..63694ca0ea6 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Skipped do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_skipped' }
+ it { expect(subject.icon).to eq 'status_skipped' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index ff65b074808..2f67df71c4f 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Success do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_success' }
+ it { expect(subject.icon).to eq 'status_success' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
index 7e2269397c6..4582354e739 100644
--- a/spec/lib/gitlab/ci/status/success_warning_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::SuccessWarning do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_warning' }
+ it { expect(subject.icon).to eq 'status_warning' }
end
describe '#group' do
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index bf981d2f6f6..92792144429 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -84,6 +84,13 @@ describe Gitlab::Conflict::File do
expect(line.text).to eq(html_to_text(line.rich_text))
end
end
+
+ # This spec will break if Rouge's highlighting changes, but we need to
+ # ensure that the lines are actually highlighted.
+ it 'highlights the lines correctly' do
+ expect(conflict_file.lines.first.rich_text)
+ .to eq("<span id=\"LC1\" class=\"line\" lang=\"ruby\"><span class=\"k\">module</span> <span class=\"nn\">Gitlab</span></span>\n")
+ end
end
describe '#sections' do
diff --git a/spec/lib/gitlab/database/grant_spec.rb b/spec/lib/gitlab/database/grant_spec.rb
index 651da3e8476..5ebf3f399b6 100644
--- a/spec/lib/gitlab/database/grant_spec.rb
+++ b/spec/lib/gitlab/database/grant_spec.rb
@@ -1,16 +1,6 @@
require 'spec_helper'
describe Gitlab::Database::Grant do
- describe '.scope_to_current_user' do
- it 'scopes the relation to the current user' do
- user = Gitlab::Database.username
- column = Gitlab::Database.postgresql? ? :grantee : :User
- names = described_class.scope_to_current_user.pluck(column).uniq
-
- expect(names).to eq([user])
- end
- end
-
describe '.create_and_execute_trigger' do
it 'returns true when the user can create and execute a trigger' do
# We assume the DB/user is set up correctly so that triggers can be
@@ -18,13 +8,11 @@ describe Gitlab::Database::Grant do
expect(described_class.create_and_execute_trigger?('users')).to eq(true)
end
- it 'returns false when the user can not create and/or execute a trigger' do
- allow(described_class).to receive(:scope_to_current_user)
- .and_return(described_class.none)
-
- result = described_class.create_and_execute_trigger?('kittens')
-
- expect(result).to eq(false)
+ it 'returns false when the user can not create and/or execute a trigger', :postgresql do
+ # In case of MySQL the user may have SUPER permissions, making it
+ # impossible to have `false` returned when running tests; hence we only
+ # run these tests on PostgreSQL.
+ expect(described_class.create_and_execute_trigger?('foo')).to eq(false)
end
end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index 8922370b0a0..e850b5cd6a4 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
+ it 'does not move the repositories when hashed storage is enabled' do
+ project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:repository])
+
+ expect(subject).not_to receive(:move_repository)
+
+ subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
+ end
+
it 'moves uploads' do
expect(subject).to receive(:move_uploads)
.with('known-parent/the-path', 'known-parent/the-path0')
@@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
+ it 'does not move uploads when hashed storage is enabled for attachments' do
+ project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:attachments])
+
+ expect(subject).not_to receive(:move_uploads)
+
+ subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
+ end
+
it 'moves pages' do
expect(subject).to receive(:move_pages)
.with('known-parent/the-path', 'known-parent/the-path0')
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 5fa94999d25..fcddfad3f9f 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -202,6 +202,26 @@ describe Gitlab::Database do
it 'handles non-UTF-8 data' do
expect { described_class.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
end
+
+ context 'when using PostgreSQL' do
+ before do
+ allow(described_class).to receive(:mysql?).and_return(false)
+ end
+
+ it 'allows the returning of the IDs of the inserted rows' do
+ result = double(:result, values: [['10']])
+
+ expect(connection)
+ .to receive(:execute)
+ .with(/RETURNING id/)
+ .and_return(result)
+
+ ids = described_class
+ .bulk_insert('test', [{ number: 10 }], return_ids: true)
+
+ expect(ids).to eq([10])
+ end
+ end
end
describe '.create_connection_pool' do
@@ -256,4 +276,26 @@ describe Gitlab::Database do
expect(described_class.false_value).to eq 0
end
end
+
+ describe '#sanitize_timestamp' do
+ let(:max_timestamp) { Time.at((1 << 31) - 1) }
+
+ subject { described_class.sanitize_timestamp(timestamp) }
+
+ context 'with a timestamp smaller than MAX_TIMESTAMP_VALUE' do
+ let(:timestamp) { max_timestamp - 10.years }
+
+ it 'returns the given timestamp' do
+ expect(subject).to eq(timestamp)
+ end
+ end
+
+ context 'with a timestamp larger than MAX_TIMESTAMP_VALUE' do
+ let(:timestamp) { max_timestamp + 1.second }
+
+ it 'returns MAX_TIMESTAMP_VALUE' do
+ expect(subject).to eq(max_timestamp)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index c91895cedc3..ff9acfd08b9 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -116,12 +116,8 @@ describe Gitlab::Diff::File do
end
context 'when renamed' do
- let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') }
- let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') }
-
- before do
- allow(diff_file.new_blob).to receive(:id).and_return(diff_file.old_blob.id)
- end
+ let(:commit) { project.commit('94bb47ca1297b7b3731ff2a36923640991e9236f') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('CHANGELOG.md') }
it 'returns false' do
expect(diff_file.content_changed?).to be_falsey
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 412a0093d97..c04a9688503 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -143,6 +143,16 @@ describe Gitlab::Git::Blob, seed_helper: true do
expect(blob.loaded_size).to eq(blob_size)
end
end
+
+ context 'when sha references a tree' do
+ it 'returns nil' do
+ tree = Gitlab::Git::Commit.find(repository, 'master').tree
+
+ blob = Gitlab::Git::Blob.raw(repository, tree.oid)
+
+ expect(blob).to be_nil
+ end
+ end
end
describe '.raw' do
@@ -226,6 +236,51 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
+ describe '.batch_lfs_pointers' do
+ let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree }
+
+ let(:non_lfs_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'master',
+ 'README.md'
+ )
+ end
+
+ let(:lfs_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/image.jpg'
+ )
+ end
+
+ it 'returns a list of Gitlab::Git::Blob' do
+ blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
+
+ expect(blobs.count).to eq(1)
+ expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ end
+
+ it 'silently ignores tree objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
+
+ expect(blobs).to eq([])
+ end
+
+ it 'silently ignores non lfs objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+
+ expect(blobs).to eq([])
+ end
+
+ it 'avoids loading large blobs into memory' do
+ expect(repository).not_to receive(:lookup)
+
+ described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+ end
+ end
+
describe 'encoding' do
context 'file with russian text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index ee657101f4c..65edc750f39 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -487,6 +487,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
loop do
break if @count.zero?
+
# It is critical to decrement before yielding. We may never reach the lines after 'yield'.
@count -= 1
yield @value
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
new file mode 100644
index 00000000000..c9007d7d456
--- /dev/null
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Git::LfsChanges do
+ let(:project) { create(:project, :repository) }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:blob_object_id) { '0c304a93cb8430108629bbbcaa27db3343299bc0' }
+
+ subject { described_class.new(project.repository, newrev) }
+
+ describe 'new_pointers' do
+ before do
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_yield([blob_object_id])
+ end
+
+ it 'uses rev-list to find new objects' do
+ rev_list = double
+ allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
+
+ expect(rev_list).to receive(:new_objects).and_return([])
+
+ subject.new_pointers
+ end
+
+ it 'filters new objects to find lfs pointers' do
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id])
+
+ subject.new_pointers(object_limit: 1)
+ end
+
+ it 'limits new_objects using object_limit' do
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [])
+
+ subject.new_pointers(object_limit: 0)
+ end
+ end
+
+ describe 'all_pointers' do
+ it 'uses rev-list to find all objects' do
+ rev_list = double
+ allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
+ allow(rev_list).to receive(:all_objects).and_yield([blob_object_id])
+
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id])
+
+ subject.all_pointers
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/popen_spec.rb b/spec/lib/gitlab/git/popen_spec.rb
index 2b65bc1cf15..b033ede9062 100644
--- a/spec/lib/gitlab/git/popen_spec.rb
+++ b/spec/lib/gitlab/git/popen_spec.rb
@@ -53,6 +53,23 @@ describe 'Gitlab::Git::Popen' do
it { expect(status).to be_zero }
it { expect(output).to eq('hello') }
end
+
+ context 'with lazy block' do
+ it 'yields a lazy io' do
+ expect_lazy_io = lambda do |io|
+ expect(io).to be_a Enumerator::Lazy
+ expect(io.inspect).to include('#<IO:fd')
+ end
+
+ klass.new.popen(%w[ls], path, lazy_block: expect_lazy_io)
+ end
+
+ it "doesn't wait for process exit" do
+ Timeout.timeout(2) do
+ klass.new.popen(%w[yes], path, lazy_block: ->(io) {})
+ end
+ end
+ end
end
context 'popen_with_timeout' do
diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb
new file mode 100644
index 00000000000..0506210887c
--- /dev/null
+++ b/spec/lib/gitlab/git/remote_repository_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RemoteRepository, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ subject { described_class.new(repository) }
+
+ describe '#empty_repo?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:repository, :result) do
+ Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') | false
+ Gitlab::Git::Repository.new('default', 'does-not-exist.git', '') | true
+ end
+
+ with_them do
+ it { expect(subject.empty_repo?).to eq(result) }
+ end
+ end
+
+ describe '#commit_id' do
+ it 'returns an OID if the revision exists' do
+ expect(subject.commit_id('v1.0.0')).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+
+ it 'is nil when the revision does not exist' do
+ expect(subject.commit_id('does-not-exist')).to be_nil
+ end
+ end
+
+ describe '#branch_exists?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch, :result) do
+ 'master' | true
+ 'does-not-exist' | false
+ end
+
+ with_them do
+ it { expect(subject.branch_exists?(branch)).to eq(result) }
+ end
+ end
+
+ describe '#same_repository?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:other_repository, :result) do
+ repository | true
+ Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '') | true
+ Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '') | false
+ Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '') | false
+ Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '') | false
+ end
+
+ with_them do
+ it { expect(subject.same_repository?(other_repository)).to eq(result) }
+ end
+ end
+
+ describe '#fetch_env' do
+ let(:remote_repository) { described_class.new(repository) }
+
+ let(:gitaly_client) { double(:gitaly_client) }
+ let(:address) { 'fake-address' }
+ let(:token) { 'fake-token' }
+
+ subject { remote_repository.fetch_env }
+
+ before do
+ allow(remote_repository).to receive(:gitaly_client).and_return(gitaly_client)
+
+ expect(gitaly_client).to receive(:address).with(repository.storage).and_return(address)
+ expect(gitaly_client).to receive(:token).with(repository.storage).and_return(token)
+ end
+
+ it { expect(subject).to be_a(Hash) }
+ it { expect(subject['GITALY_ADDRESS']).to eq(address) }
+ it { expect(subject['GITALY_TOKEN']).to eq(token) }
+ it { expect(subject['GITALY_WD']).to eq(Dir.pwd) }
+
+ it 'creates a plausible GIT_SSH_COMMAND' do
+ git_ssh_command = subject['GIT_SSH_COMMAND']
+
+ expect(git_ssh_command).to start_with('/')
+ expect(git_ssh_command).to end_with('/gitaly-ssh upload-pack')
+ end
+
+ it 'creates a plausible GITALY_PAYLOAD' do
+ req = Gitaly::SSHUploadPackRequest.decode_json(subject['GITALY_PAYLOAD'])
+
+ expect(remote_repository.gitaly_repository).to eq(req.repository)
+ end
+
+ context 'when the token is blank' do
+ let(:token) { '' }
+
+ it { expect(subject.keys).not_to include('GITALY_TOKEN') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index b161d25b96c..f0da77c61bb 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -449,7 +449,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -484,7 +483,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -544,7 +542,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
@@ -559,10 +556,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#remote_delete" do
+ describe "#remove_remote" do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_delete("expendable")
+ @repo.remove_remote("expendable")
end
it "should remove the remote" do
@@ -570,42 +567,107 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
- describe "#remote_add" do
+ describe "#remote_update" do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
+ @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
end
it "should add the remote" do
- expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote")
+ expect(@repo.rugged.remotes["expendable"].url).to(
+ eq(TEST_NORMAL_REPO_PATH)
+ )
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
- describe "#remote_update" do
+ describe '#fetch_mirror' do
+ let(:new_repository) do
+ Gitlab::Git::Repository.new('default', 'my_project.git', '')
+ end
+
+ subject { new_repository.fetch_mirror(repository.path) }
+
+ before do
+ Gitlab::Shell.new.add_repository('default', 'my_project')
+ end
+
+ after do
+ Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project')
+ end
+
+ it 'fetches a url as a mirror remote' do
+ subject
+
+ expect(refs(new_repository.path)).to eq(refs(repository.path))
+ end
+
+ context 'with keep-around refs' do
+ let(:sha) { SeedRepo::Commit::ID }
+ let(:keep_around_ref) { "refs/keep-around/#{sha}" }
+ let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
+
+ before do
+ repository.rugged.references.create(keep_around_ref, sha, force: true)
+ repository.rugged.references.create(tmp_ref, sha, force: true)
+ end
+
+ it 'includes the temporary and keep-around refs' do
+ subject
+
+ expect(refs(new_repository.path)).to include(keep_around_ref)
+ expect(refs(new_repository.path)).to include(tmp_ref)
+ end
+ end
+ end
+
+ describe '#remote_tags' do
+ let(:remote_name) { 'upstream' }
+ let(:target_commit_id) { SeedRepo::Commit::ID }
+ let(:user) { create(:user) }
+ let(:tag_name) { 'v0.0.1' }
+ let(:tag_message) { 'My tag' }
+ let(:remote_repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+
+ subject { repository.remote_tags(remote_name) }
+
+ before do
+ repository.add_remote(remote_name, remote_repository.path)
+ remote_repository.add_tag(tag_name, user: user, target: target_commit_id)
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ it 'gets the remote tags' do
+ expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag)
+ expect(subject.first.name).to eq(tag_name)
+ expect(subject.first.dereferenced_target.id).to eq(target_commit_id)
+ end
+ end
+
+ describe '#remote_exists?' do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
+ @repo.add_remote("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
end
- it "should add the remote" do
- expect(@repo.rugged.remotes["expendable"].url).to(
- eq(TEST_NORMAL_REPO_PATH)
- )
+ it 'returns true for an existing remote' do
+ expect(@repo.remote_exists?('new_remote')).to eq(true)
end
- after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
- ensure_seeds
+ it 'returns false for a non-existing remote' do
+ expect(@repo.remote_exists?('foo')).to eq(false)
end
end
@@ -1068,7 +1130,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1115,7 +1176,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1163,6 +1223,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
+ let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
it "read every file paths of master branch" do
@@ -1184,6 +1245,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
it "returns empty array when not existed branch" do
expect(not_existed_branch.length).to equal(0)
end
+
+ it "returns valid utf-8 data" do
+ expect(utf8_file_paths.map { |file| file.force_encoding('utf-8') }).to all(be_valid_encoding)
+ end
end
describe "#copy_gitattributes" do
@@ -1336,13 +1401,30 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#batch_existence' do
+ let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] }
+
+ it 'returns existing refs back' do
+ result = repository.batch_existence(refs)
+
+ expect(result).to eq([SeedRepo::RubyBlob::ID])
+ end
+
+ context 'existing: true' do
+ it 'inverts meaning and returns non-existing refs' do
+ result = repository.batch_existence(refs, existing: false)
+
+ expect(result).to eq(%w(deadbeef 909e6157199))
+ end
+ end
+ end
+
describe '#local_branches' do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1459,36 +1541,61 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#fetch_source_branch' do
- let(:local_ref) { 'refs/merge-requests/1/head' }
+ describe '#fetch_source_branch!' do
+ shared_examples '#fetch_source_branch!' do
+ let(:local_ref) { 'refs/merge-requests/1/head' }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
- context 'when the branch exists' do
- let(:source_branch) { 'master' }
+ after do
+ ensure_seeds
+ end
- it 'writes the ref' do
- expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/)
+ context 'when the branch exists' do
+ context 'when the commit does not exist locally' do
+ let(:source_branch) { 'new-branch-for-fetch-source-branch' }
+ let(:source_rugged) { source_repository.rugged }
+ let(:new_oid) { new_commit_edit_old_file(source_rugged).oid }
- repository.fetch_source_branch(repository, source_branch, local_ref)
- end
+ before do
+ source_rugged.branches.create(source_branch, new_oid)
+ end
- it 'returns true' do
- expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(true)
- end
- end
+ it 'writes the ref' do
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
+ expect(repository.commit(local_ref).sha).to eq(new_oid)
+ end
+ end
- context 'when the branch does not exist' do
- let(:source_branch) { 'definitely-not-master' }
+ context 'when the commit exists locally' do
+ let(:source_branch) { 'master' }
+ let(:expected_oid) { SeedRepo::LastCommit::ID }
- it 'does not write the ref' do
- expect(repository).not_to receive(:write_ref)
+ it 'writes the ref' do
+ # Sanity check: the commit should already exist
+ expect(repository.commit(expected_oid)).not_to be_nil
- repository.fetch_source_branch(repository, source_branch, local_ref)
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
+ expect(repository.commit(local_ref).sha).to eq(expected_oid)
+ end
+ end
end
- it 'returns false' do
- expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(false)
+ context 'when the branch does not exist' do
+ let(:source_branch) { 'definitely-not-master' }
+
+ it 'does not write the ref' do
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false)
+ expect(repository.commit(local_ref)).to be_nil
+ end
end
end
+
+ it_behaves_like '#fetch_source_branch!'
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#fetch_source_branch!'
+ end
end
describe '#rm_branch' do
@@ -1564,7 +1671,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1609,38 +1715,97 @@ describe Gitlab::Git::Repository, seed_helper: true do
subject { repository.ff_merge(user, source_sha, target_branch) }
- it 'performs a ff_merge' do
- expect(subject.newrev).to eq(source_sha)
- expect(subject.repo_created).to be(false)
- expect(subject.branch_created).to be(false)
+ shared_examples '#ff_merge' do
+ it 'performs a ff_merge' do
+ expect(subject.newrev).to eq(source_sha)
+ expect(subject.repo_created).to be(false)
+ expect(subject.branch_created).to be(false)
- expect(repository.commit(target_branch).id).to eq(source_sha)
- end
+ expect(repository.commit(target_branch).id).to eq(source_sha)
+ end
+
+ context 'with a non-existing target branch' do
+ subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
+
+ it 'throws an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
- context 'with a non-existing target branch' do
- subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
+ context 'with a non-existing source commit' do
+ let(:source_sha) { 'f001' }
- it 'throws an ArgumentError' do
- expect { subject }.to raise_error(ArgumentError)
+ it 'throws an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the source sha is not a descendant of the branch head' do
+ let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+
+ it "doesn't perform the ff_merge" do
+ expect { subject }.to raise_error(Gitlab::Git::CommitError)
+
+ expect(repository.commit(target_branch).id).to eq(branch_head)
+ end
end
end
- context 'with a non-existing source commit' do
- let(:source_sha) { 'f001' }
+ context 'with gitaly' do
+ it "calls Gitaly's OperationService" do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_ff_branch).with(user, source_sha, target_branch)
+ .and_return(nil)
- it 'throws an ArgumentError' do
- expect { subject }.to raise_error(ArgumentError)
+ subject
end
+
+ it_behaves_like '#ff_merge'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#ff_merge'
end
+ end
- context 'when the source sha is not a descendant of the branch head' do
- let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+ describe '#fetch' do
+ let(:git_path) { Gitlab.config.git.bin_path }
+ let(:remote_name) { 'my_remote' }
- it "doesn't perform the ff_merge" do
- expect { subject }.to raise_error(Gitlab::Git::CommitError)
+ subject { repository.fetch(remote_name) }
- expect(repository.commit(target_branch).id).to eq(branch_head)
- end
+ 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 '#delete_all_refs_except' do
+ let(:repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+
+ before do
+ repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
+ repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1")
+ repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae")
+ repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ it 'deletes all refs except those with the specified prefixes' do
+ repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads))
+ expect(repository.ref_exists?("refs/delete/a")).to be(false)
+ expect(repository.ref_exists?("refs/also-delete/b")).to be(false)
+ expect(repository.ref_exists?("refs/keep/c")).to be(true)
+ expect(repository.ref_exists?("refs/also-keep/d")).to be(true)
+ expect(repository.ref_exists?("refs/heads/master")).to be(true)
end
end
@@ -1719,4 +1884,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
+
+ def refs(dir)
+ IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line|
+ line.split("\t").last
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index c0eac98d718..eaf74951b0e 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -2,53 +2,112 @@ require 'spec_helper'
describe Gitlab::Git::RevList do
let(:project) { create(:project, :repository) }
+ let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+ let(:env_hash) do
+ {
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ }
+ end
before do
- expect(Gitlab::Git::Env).to receive(:all).and_return({
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
- })
+ allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash.symbolize_keys)
end
- context "#new_refs" do
- let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+ def args_for_popen(args_list)
+ [
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ 'rev-list',
+ *args_list
+ ]
+ end
- it 'calls out to `popen`' do
- expect(rev_list).to receive(:popen).with([
- Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- 'rev-list',
- 'newrev',
- '--not',
- '--all'
- ],
+ def stub_popen_rev_list(*additional_args, output:)
+ args = args_for_popen(additional_args)
+
+ expect(rev_list).to receive(:popen).with(args, nil, env_hash)
+ .and_return([output, 0])
+ end
+
+ def stub_lazy_popen_rev_list(*additional_args, output:)
+ params = [
+ args_for_popen(additional_args),
nil,
- {
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- }).and_return(["sha1\nsha2", 0])
+ env_hash,
+ hash_including(lazy_block: anything)
+ ]
+
+ expect(rev_list).to receive(:popen).with(*params) do |*_, lazy_block:|
+ lazy_block.call(output.split("\n").lazy)
+ end
+ end
+
+ context "#new_refs" do
+ it 'calls out to `popen`' do
+ stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2")
expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end
end
+ context '#new_objects' do
+ it 'fetches list of newly pushed objects using rev-list' do
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects).to eq(%w[sha1 sha2])
+ end
+
+ it 'can skip pathless objects' do
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file")
+
+ expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2])
+ end
+
+ it 'can yield a lazy enumerator' do
+ stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ rev_list.new_objects do |object_ids|
+ expect(object_ids).to be_a Enumerator::Lazy
+ end
+ end
+
+ it 'returns the result of the block when given' do
+ stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ objects = rev_list.new_objects do |object_ids|
+ object_ids.first
+ end
+
+ expect(objects).to eq 'sha1'
+ end
+
+ it 'can accept list of references to exclude' do
+ stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects(not_in: ['master'])).to eq(%w[sha1 sha2])
+ end
+
+ it 'handles empty list of references to exclude as listing all known objects' do
+ stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects(not_in: [])).to eq(%w[sha1 sha2])
+ end
+ end
+
+ context '#all_objects' do
+ it 'fetches list of all pushed objects using rev-list' do
+ stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.all_objects).to eq(%w[sha1 sha2])
+ end
+ end
+
context "#missed_ref" do
let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
- expect(rev_list).to receive(:popen).with([
- Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- 'rev-list',
- '--max-count=1',
- 'oldrev',
- '^newrev'
- ],
- nil,
- {
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- }).and_return(["sha1\nsha2", 0])
+ stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2")
expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index e144e28b5d8..d9ec28ab02e 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -89,4 +89,38 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ describe '#user_ff_branch' do
+ let(:target_branch) { 'my-branch' }
+ let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:request) do
+ Gitaly::UserFFBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch: target_branch,
+ commit_id: source_sha,
+ user: gitaly_user
+ )
+ end
+ let(:branch_update) do
+ Gitaly::OperationBranchUpdate.new(
+ commit_id: source_sha,
+ repo_created: false,
+ branch_created: false
+ )
+ end
+ let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) }
+
+ subject { client.user_ff_branch(user, source_sha, target_branch) }
+
+ it 'sends a user_ff_branch message and returns a BranchUpdate object' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_ff_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
+ expect(subject.newrev).to eq(source_sha)
+ expect(subject.repo_created).to be(false)
+ expect(subject.branch_created).to be(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 8127b4842b7..951e146a30a 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -104,4 +104,17 @@ describe Gitlab::GitalyClient::RefService do
expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError)
end
end
+
+ describe '#delete_refs' do
+ let(:prefixes) { %w(refs/heads refs/keep-around) }
+
+ it 'sends a delete_refs message' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:delete_refs)
+ .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash))
+ .and_return(double('delete_refs_response'))
+
+ client.delete_refs(except_with_prefixes: prefixes)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
new file mode 100644
index 00000000000..6ad9f5ef766
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::WikiService do
+ let(:project) { create(:project) }
+ let(:storage_name) { project.repository_storage }
+ let(:relative_path) { project.disk_path + '.git' }
+ let(:client) { described_class.new(project.repository) }
+ let(:commit) { create(:gitaly_commit) }
+ let(:page_version) { Gitaly::WikiPageVersion.new(format: 'markdown', commit: commit) }
+ let(:page_info) { { title: 'My Page', raw_data: 'a', version: page_version } }
+
+ describe '#find_page' do
+ let(:response) do
+ [
+ Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(page_info)),
+ Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b'))
+ ]
+ end
+ let(:wiki_page) { subject.first }
+ let(:wiki_page_version) { subject.last }
+
+ subject { client.find_page(title: 'My Page', version: 'master', dir: '') }
+
+ it 'sends a wiki_find_page message' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_find_page)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([].each)
+
+ subject
+ end
+
+ it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_find_page)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(response.each)
+
+ expect(wiki_page.title).to eq('My Page')
+ expect(wiki_page.raw_data).to eq('ab')
+ expect(wiki_page_version.format).to eq('markdown')
+ end
+ end
+
+ describe '#get_all_pages' do
+ let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } }
+ let(:response) do
+ [
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_info)),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b')),
+ Gitaly::WikiGetAllPagesResponse.new(end_of_page: true),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_2_info)),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'd')),
+ Gitaly::WikiGetAllPagesResponse.new(end_of_page: true)
+ ]
+ end
+ let(:wiki_page_1) { subject[0].first }
+ let(:wiki_page_1_version) { subject[0].last }
+ let(:wiki_page_2) { subject[1].first }
+ let(:wiki_page_2_version) { subject[1].last }
+
+ subject { client.get_all_pages }
+
+ it 'sends a wiki_get_all_pages message' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_get_all_pages)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([].each)
+
+ subject
+ end
+
+ it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion for each page' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_get_all_pages)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(response.each)
+
+ expect(subject.size).to be(2)
+ expect(wiki_page_1.title).to eq('My Page')
+ expect(wiki_page_1.raw_data).to eq('ab')
+ expect(wiki_page_1_version.format).to eq('markdown')
+ expect(wiki_page_2.title).to eq('My Page 2')
+ expect(wiki_page_2.raw_data).to eq('cd')
+ expect(wiki_page_2_version.format).to eq('markdown')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
new file mode 100644
index 00000000000..91229d9c7d4
--- /dev/null
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::BulkImporting do
+ let(:importer) do
+ Class.new { include(Gitlab::GithubImport::BulkImporting) }.new
+ end
+
+ describe '#build_database_rows' do
+ it 'returns an Array containing the rows to insert' do
+ object = double(:object, title: 'Foo')
+
+ expect(importer)
+ .to receive(:build)
+ .with(object)
+ .and_return({ title: 'Foo' })
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(false)
+
+ enum = [[object, 1]].to_enum
+
+ expect(importer.build_database_rows(enum)).to eq([{ title: 'Foo' }])
+ end
+
+ it 'does not import objects that have already been imported' do
+ object = double(:object, title: 'Foo')
+
+ expect(importer)
+ .not_to receive(:build)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(true)
+
+ enum = [[object, 1]].to_enum
+
+ expect(importer.build_database_rows(enum)).to be_empty
+ end
+ end
+
+ describe '#bulk_insert' do
+ it 'bulk inserts rows into the database' do
+ rows = [{ title: 'Foo' }] * 10
+ model = double(:model, table_name: 'kittens')
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .ordered
+ .with('kittens', rows.first(5))
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .ordered
+ .with('kittens', rows.last(5))
+
+ importer.bulk_insert(model, rows, batch_size: 5)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/caching_spec.rb b/spec/lib/gitlab/github_import/caching_spec.rb
new file mode 100644
index 00000000000..70ecdc16da1
--- /dev/null
+++ b/spec/lib/gitlab/github_import/caching_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Caching, :clean_gitlab_redis_cache do
+ describe '.read' do
+ it 'reads a value from the cache' do
+ described_class.write('foo', 'bar')
+
+ expect(described_class.read('foo')).to eq('bar')
+ end
+
+ it 'returns nil if the cache key does not exist' do
+ expect(described_class.read('foo')).to be_nil
+ end
+
+ it 'refreshes the cache key if a value is present' do
+ described_class.write('foo', 'bar')
+
+ redis = double(:redis)
+
+ expect(redis).to receive(:get).with(/foo/).and_return('bar')
+ expect(redis).to receive(:expire).with(/foo/, described_class::TIMEOUT)
+ expect(Gitlab::Redis::Cache).to receive(:with).twice.and_yield(redis)
+
+ described_class.read('foo')
+ end
+
+ it 'does not refresh the cache key if a value is empty' do
+ described_class.write('foo', nil)
+
+ redis = double(:redis)
+
+ expect(redis).to receive(:get).with(/foo/).and_return('')
+ expect(redis).not_to receive(:expire)
+ expect(Gitlab::Redis::Cache).to receive(:with).and_yield(redis)
+
+ described_class.read('foo')
+ end
+ end
+
+ describe '.read_integer' do
+ it 'returns an Integer' do
+ described_class.write('foo', '10')
+
+ expect(described_class.read_integer('foo')).to eq(10)
+ end
+
+ it 'returns nil if no value was found' do
+ expect(described_class.read_integer('foo')).to be_nil
+ end
+ end
+
+ describe '.write' do
+ it 'writes a value to the cache and returns the written value' do
+ expect(described_class.write('foo', 10)).to eq(10)
+ expect(described_class.read('foo')).to eq('10')
+ end
+ end
+
+ describe '.set_add' do
+ it 'adds a value to a set' do
+ described_class.set_add('foo', 10)
+ described_class.set_add('foo', 10)
+
+ key = described_class.cache_key_for('foo')
+ values = Gitlab::Redis::Cache.with { |r| r.smembers(key) }
+
+ expect(values).to eq(['10'])
+ end
+ end
+
+ describe '.set_includes?' do
+ it 'returns false when the key does not exist' do
+ expect(described_class.set_includes?('foo', 10)).to eq(false)
+ end
+
+ it 'returns false when the value is not present in the set' do
+ described_class.set_add('foo', 10)
+
+ expect(described_class.set_includes?('foo', 20)).to eq(false)
+ end
+
+ it 'returns true when the set includes the given value' do
+ described_class.set_add('foo', 10)
+
+ expect(described_class.set_includes?('foo', 10)).to eq(true)
+ end
+ end
+
+ describe '.write_multiple' do
+ it 'sets multiple keys' do
+ mapping = { 'foo' => 10, 'bar' => 20 }
+
+ described_class.write_multiple(mapping)
+
+ mapping.each do |key, value|
+ full_key = described_class.cache_key_for(key)
+ found = Gitlab::Redis::Cache.with { |r| r.get(full_key) }
+
+ expect(found).to eq(value.to_s)
+ end
+ end
+ end
+
+ describe '.expire' do
+ it 'sets the expiration time of a key' do
+ timeout = 1.hour.to_i
+
+ described_class.write('foo', 'bar', timeout: 2.hours.to_i)
+ described_class.expire('foo', timeout)
+
+ key = described_class.cache_key_for('foo')
+ found_ttl = Gitlab::Redis::Cache.with { |r| r.ttl(key) }
+
+ expect(found_ttl).to be <= timeout
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 66273255b6f..5b2642d9473 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -1,97 +1,392 @@
require 'spec_helper'
describe Gitlab::GithubImport::Client do
- let(:token) { '123456' }
- let(:github_provider) { Settingslogic.new('app_id' => 'asd123', 'app_secret' => 'asd123', 'name' => 'github', 'args' => { 'client_options' => {} }) }
+ describe '#parallel?' do
+ it 'returns true when the client is running in parallel mode' do
+ client = described_class.new('foo', parallel: true)
- subject(:client) { described_class.new(token) }
+ expect(client).to be_parallel
+ end
+
+ it 'returns false when the client is running in sequential mode' do
+ client = described_class.new('foo', parallel: false)
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([github_provider])
+ expect(client).not_to be_parallel
+ end
end
- it 'convert OAuth2 client options to symbols' do
- client.client.options.keys.each do |key|
- expect(key).to be_kind_of(Symbol)
+ describe '#user' do
+ it 'returns the details for the given username' do
+ client = described_class.new('foo')
+
+ expect(client.octokit).to receive(:user).with('foo')
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.user('foo')
end
end
- it 'does not crash (e.g. Settingslogic::MissingSetting) when verify_ssl config is not present' do
- expect { client.api }.not_to raise_error
+ describe '#repository' do
+ it 'returns the details of a repository' do
+ client = described_class.new('foo')
+
+ expect(client.octokit).to receive(:repo).with('foo/bar')
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.repository('foo/bar')
+ end
end
- context 'when config is missing' do
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ describe '#labels' do
+ it 'returns the labels' do
+ client = described_class.new('foo')
+
+ expect(client)
+ .to receive(:each_object)
+ .with(:labels, 'foo/bar')
+
+ client.labels('foo/bar')
end
+ end
- it 'is still possible to get an Octokit client' do
- expect { client.api }.not_to raise_error
+ describe '#milestones' do
+ it 'returns the milestones' do
+ client = described_class.new('foo')
+
+ expect(client)
+ .to receive(:each_object)
+ .with(:milestones, 'foo/bar')
+
+ client.milestones('foo/bar')
end
+ end
- it 'is not be possible to get an OAuth2 client' do
- expect { client.client }.to raise_error(Projects::ImportService::Error)
+ describe '#releases' do
+ it 'returns the releases' do
+ client = described_class.new('foo')
+
+ expect(client)
+ .to receive(:each_object)
+ .with(:releases, 'foo/bar')
+
+ client.releases('foo/bar')
end
end
- context 'allow SSL verification to be configurable on API' do
+ describe '#each_page' do
+ let(:client) { described_class.new('foo') }
+ let(:object1) { double(:object1) }
+ let(:object2) { double(:object2) }
+
before do
- github_provider['verify_ssl'] = false
+ allow(client)
+ .to receive(:with_rate_limit)
+ .and_yield
+
+ allow(client.octokit)
+ .to receive(:public_send)
+ .and_return([object1])
+
+ response = double(:response, data: [object2], rels: { next: nil })
+ next_page = double(:next_page, get: response)
+
+ allow(client.octokit)
+ .to receive(:last_response)
+ .and_return(double(:last_response, rels: { next: next_page }))
+ end
+
+ context 'without a block' do
+ it 'returns an Enumerator' do
+ expect(client.each_page(:foo)).to be_an_instance_of(Enumerator)
+ end
+
+ it 'the returned Enumerator returns Page objects' do
+ enum = client.each_page(:foo)
+
+ page1 = enum.next
+ page2 = enum.next
+
+ expect(page1).to be_an_instance_of(described_class::Page)
+ expect(page2).to be_an_instance_of(described_class::Page)
+
+ expect(page1.objects).to eq([object1])
+ expect(page1.number).to eq(1)
+
+ expect(page2.objects).to eq([object2])
+ expect(page2.number).to eq(2)
+ end
+ end
+
+ context 'with a block' do
+ it 'yields every retrieved page to the supplied block' do
+ pages = []
+
+ client.each_page(:foo) { |page| pages << page }
+
+ expect(pages[0]).to be_an_instance_of(described_class::Page)
+ expect(pages[1]).to be_an_instance_of(described_class::Page)
+
+ expect(pages[0].objects).to eq([object1])
+ expect(pages[0].number).to eq(1)
+
+ expect(pages[1].objects).to eq([object2])
+ expect(pages[1].number).to eq(2)
+ end
+
+ it 'starts at the given page' do
+ pages = []
+
+ client.each_page(:foo, page: 2) { |page| pages << page }
+
+ expect(pages[0].number).to eq(2)
+ expect(pages[1].number).to eq(3)
+ end
+ end
+ end
+
+ describe '#with_rate_limit' do
+ let(:client) { described_class.new('foo') }
+
+ it 'yields the supplied block when enough requests remain' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { |b| client.with_rate_limit(&b) }.to yield_control
+ end
+
+ it 'waits before yielding if not enough requests remain' do
+ expect(client).to receive(:requests_remaining?).and_return(false)
+ expect(client).to receive(:raise_or_wait_for_rate_limit)
+
+ expect { |b| client.with_rate_limit(&b) }.to yield_control
+ end
+
+ it 'waits and retries the operation if all requests were consumed in the supplied block' do
+ retries = 0
+
+ expect(client).to receive(:requests_remaining?).and_return(true)
+ expect(client).to receive(:raise_or_wait_for_rate_limit)
+
+ client.with_rate_limit do
+ if retries.zero?
+ retries += 1
+ raise(Octokit::TooManyRequests)
+ end
+ end
+
+ expect(retries).to eq(1)
+ end
+
+ it 'increments the request count counter' do
+ expect(client.request_count_counter)
+ .to receive(:increment)
+ .and_call_original
+
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ client.with_rate_limit { }
+ end
+
+ it 'ignores rate limiting when disabled' do
+ expect(client)
+ .to receive(:rate_limiting_enabled?)
+ .and_return(false)
+
+ expect(client)
+ .not_to receive(:requests_remaining?)
+
+ expect(client.with_rate_limit { 10 }).to eq(10)
+ end
+ end
+
+ describe '#requests_remaining?' do
+ let(:client) { described_class.new('foo') }
+
+ it 'returns true if enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(9000)
+
+ expect(client.requests_remaining?).to eq(true)
+ end
+
+ it 'returns false if not enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(1)
+
+ expect(client.requests_remaining?).to eq(false)
+ end
+ end
+
+ describe '#raise_or_wait_for_rate_limit' do
+ it 'raises RateLimitError when running in parallel mode' do
+ client = described_class.new('foo', parallel: true)
+
+ expect { client.raise_or_wait_for_rate_limit }
+ .to raise_error(Gitlab::GithubImport::RateLimitError)
end
- it 'uses supplied value' do
- expect(client.client.options[:connection_opts][:ssl]).to eq({ verify: false })
- expect(client.api.connection_options[:ssl]).to eq({ verify: false })
+ it 'sleeps when running in sequential mode' do
+ client = described_class.new('foo', parallel: false)
+
+ expect(client).to receive(:rate_limit_resets_in).and_return(1)
+ expect(client).to receive(:sleep).with(1)
+
+ client.raise_or_wait_for_rate_limit
+ end
+
+ it 'increments the rate limit counter' do
+ client = described_class.new('foo', parallel: false)
+
+ expect(client)
+ .to receive(:rate_limit_resets_in)
+ .and_return(1)
+
+ expect(client)
+ .to receive(:sleep)
+ .with(1)
+
+ expect(client.rate_limit_counter)
+ .to receive(:increment)
+ .and_call_original
+
+ client.raise_or_wait_for_rate_limit
+ end
+ end
+
+ describe '#remaining_requests' do
+ it 'returns the number of remaining requests' do
+ client = described_class.new('foo')
+ rate_limit = double(remaining: 1)
+
+ expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
+ expect(client.remaining_requests).to eq(1)
+ end
+ end
+
+ describe '#rate_limit_resets_in' do
+ it 'returns the number of seconds after which the rate limit is reset' do
+ client = described_class.new('foo')
+ rate_limit = double(resets_in: 1)
+
+ expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
+
+ expect(client.rate_limit_resets_in).to eq(6)
end
end
describe '#api_endpoint' do
- context 'when provider does not specity an API endpoint' do
- it 'uses GitHub root API endpoint' do
- expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ let(:client) { described_class.new('foo') }
+
+ context 'without a custom endpoint configured in Omniauth' do
+ it 'returns the default API endpoint' do
+ expect(client)
+ .to receive(:custom_api_endpoint)
+ .and_return(nil)
+
+ expect(client.api_endpoint).to eq('https://api.github.com')
end
end
- context 'when provider specify a custom API endpoint' do
- before do
- github_provider['args']['client_options']['site'] = 'https://github.company.com/'
+ context 'with a custom endpoint configured in Omniauth' do
+ it 'returns the custom endpoint' do
+ endpoint = 'https://github.kittens.com'
+
+ expect(client)
+ .to receive(:custom_api_endpoint)
+ .and_return(endpoint)
+
+ expect(client.api_endpoint).to eq(endpoint)
end
+ end
+ end
+
+ describe '#custom_api_endpoint' do
+ let(:client) { described_class.new('foo') }
+
+ context 'without a custom endpoint' do
+ it 'returns nil' do
+ expect(client)
+ .to receive(:github_omniauth_provider)
+ .and_return({})
+
+ expect(client.custom_api_endpoint).to be_nil
+ end
+ end
+
+ context 'with a custom endpoint' do
+ it 'returns the API endpoint' do
+ endpoint = 'https://github.kittens.com'
+
+ expect(client)
+ .to receive(:github_omniauth_provider)
+ .and_return({ 'args' => { 'client_options' => { 'site' => endpoint } } })
- it 'uses the custom API endpoint' do
- expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options)
- expect(client.api.api_endpoint).to eq 'https://github.company.com/'
+ expect(client.custom_api_endpoint).to eq(endpoint)
end
end
+ end
+
+ describe '#default_api_endpoint' do
+ it 'returns the default API endpoint' do
+ client = described_class.new('foo')
+
+ expect(client.default_api_endpoint).to eq('https://api.github.com')
+ end
+ end
+
+ describe '#verify_ssl' do
+ let(:client) { described_class.new('foo') }
- context 'when given a host' do
- subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') }
+ context 'without a custom configuration' do
+ it 'returns true' do
+ expect(client)
+ .to receive(:github_omniauth_provider)
+ .and_return({})
- it 'builds a endpoint with the given host and the default API version' do
- expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ expect(client.verify_ssl).to eq(true)
end
end
- context 'when given an API version' do
- subject(:client) { described_class.new(token, api_version: 'v3') }
+ context 'with a custom configuration' do
+ it 'returns the configured value' do
+ expect(client.verify_ssl).to eq(false)
+ end
+ end
+ end
+
+ describe '#github_omniauth_provider' do
+ let(:client) { described_class.new('foo') }
- it 'does not use the API version without a host' do
- expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ context 'without a configured provider' do
+ it 'returns an empty Hash' do
+ expect(Gitlab.config.omniauth)
+ .to receive(:providers)
+ .and_return([])
+
+ expect(client.github_omniauth_provider).to eq({})
end
end
- context 'when given a host and version' do
- subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') }
+ context 'with a configured provider' do
+ it 'returns the provider details as a Hash' do
+ hash = client.github_omniauth_provider
- it 'builds a endpoint with the given options' do
- expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ expect(hash['name']).to eq('github')
+ expect(hash['url']).to eq('https://github.com/')
end
end
end
- it 'does not raise error when rate limit is disabled' do
- stub_request(:get, /api.github.com/)
- allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+ describe '#rate_limiting_enabled?' do
+ let(:client) { described_class.new('foo') }
- expect { client.issues {} }.not_to raise_error
+ it 'returns true when using GitHub.com' do
+ expect(client.rate_limiting_enabled?).to eq(true)
+ end
+
+ it 'returns false for GitHub enterprise installations' do
+ expect(client)
+ .to receive(:api_endpoint)
+ .and_return('https://github.kittens.com/')
+
+ expect(client.rate_limiting_enabled?).to eq(false)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
new file mode 100644
index 00000000000..1568c657a1e
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -0,0 +1,152 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::DiffNoteImporter do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:user) { create(:user) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:hunk) do
+ '@@ -1 +1 @@
+ -Hello
+ +Hello world'
+ end
+
+ let(:note) do
+ Gitlab::GithubImport::Representation::DiffNote.new(
+ noteable_type: 'MergeRequest',
+ noteable_id: 1,
+ commit_id: '123abc',
+ file_path: 'README.md',
+ diff_hunk: hunk,
+ author: Gitlab::GithubImport::Representation::User
+ .new(id: user.id, login: user.username),
+ note: 'Hello',
+ created_at: created_at,
+ updated_at: updated_at,
+ github_id: 1
+ )
+ end
+
+ let(:importer) { described_class.new(note, project, client) }
+
+ describe '#execute' do
+ context 'when the merge request no longer exists' do
+ it 'does not import anything' do
+ expect(Gitlab::Database).not_to receive(:bulk_insert)
+
+ importer.execute
+ end
+ end
+
+ context 'when the merge request exists' do
+ let!(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ before do
+ allow(importer)
+ .to receive(:find_merge_request_id)
+ .and_return(merge_request.id)
+ end
+
+ it 'imports the note' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([user.id, true])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ LegacyDiffNote.table_name,
+ [
+ {
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ project_id: project.id,
+ author_id: user.id,
+ note: 'Hello',
+ system: false,
+ commit_id: '123abc',
+ line_code: note.line_code,
+ type: 'LegacyDiffNote',
+ created_at: created_at,
+ updated_at: updated_at,
+ st_diff: note.diff_hash.to_yaml
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+
+ it 'imports the note when the author could not be found' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ LegacyDiffNote.table_name,
+ [
+ {
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ project_id: project.id,
+ author_id: project.creator_id,
+ note: "*Created by: #{user.username}*\n\nHello",
+ system: false,
+ commit_id: '123abc',
+ line_code: note.line_code,
+ type: 'LegacyDiffNote',
+ created_at: created_at,
+ updated_at: updated_at,
+ st_diff: note.diff_hash.to_yaml
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+
+ it 'produces a valid LegacyDiffNote' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([user.id, true])
+
+ importer.execute
+
+ note = project.notes.diff_notes.take
+
+ expect(note).to be_valid
+ expect(note.diff).to be_an_instance_of(Gitlab::Git::Diff)
+ end
+
+ it 'does not import the note when a foreign key error is raised' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.execute }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#find_merge_request_id' do
+ it 'returns a merge request ID' do
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:database_id)
+ .and_return(10)
+
+ expect(importer.find_merge_request_id).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
new file mode 100644
index 00000000000..4713c6795bb
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::DiffNotesImporter do
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ let(:github_comment) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/pull/42',
+ path: 'README.md',
+ commit_id: '123abc',
+ diff_hunk: "@@ -1 +1 @@\n-Hello\n+Hello world",
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ id: 1
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports diff notes in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports diff notes in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each diff note in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ diff_note_importer = double(:diff_note_importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::Importer::DiffNoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::DiffNote),
+ project,
+ client
+ )
+ .and_return(diff_note_importer)
+
+ expect(diff_note_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each diff note in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::ImportDiffNoteWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the ID of the given note' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(github_comment))
+ .to eq(1)
+ end
+ end
+
+ describe '#collection_options' do
+ it 'returns an empty Hash' do
+ # For large projects (e.g. kubernetes/kubernetes) GitHub's API may produce
+ # HTTP 500 errors when using explicit sorting options, regardless of what
+ # order you sort in. Not using any sorting options at all allows us to
+ # work around this.
+ importer = described_class.new(project, client)
+
+ expect(importer.collection_options).to eq({})
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
new file mode 100644
index 00000000000..665b31ef244
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter do
+ describe '#execute' do
+ it 'imports an issue and its labels' do
+ issue = double(:issue)
+ project = double(:project)
+ client = double(:client)
+ label_links_instance = double(:label_links_importer)
+ importer = described_class.new(issue, project, client)
+
+ expect(Gitlab::GithubImport::Importer::IssueImporter)
+ .to receive(:import_if_issue)
+ .with(issue, project, client)
+
+ expect(Gitlab::GithubImport::Importer::LabelLinksImporter)
+ .to receive(:new)
+ .with(issue, project, client)
+ .and_return(label_links_instance)
+
+ expect(label_links_instance)
+ .to receive(:execute)
+
+ importer.execute
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
new file mode 100644
index 00000000000..d34ca0b76b8
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -0,0 +1,201 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:issue) do
+ Gitlab::GithubImport::Representation::Issue.new(
+ iid: 42,
+ title: 'My Issue',
+ description: 'This is my issue',
+ milestone_number: 1,
+ state: :opened,
+ assignees: [
+ Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+ Gitlab::GithubImport::Representation::User.new(id: 5, login: 'bob')
+ ],
+ label_names: %w[bug],
+ author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ pull_request: false
+ )
+ end
+
+ describe '.import_if_issue' do
+ it 'imports an issuable if it is a regular issue' do
+ importer = double(:importer)
+
+ expect(described_class)
+ .to receive(:new)
+ .with(issue, project, client)
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+
+ described_class.import_if_issue(issue, project, client)
+ end
+
+ it 'does not import the issuable if it is a pull request' do
+ expect(issue).to receive(:pull_request?).and_return(true)
+
+ expect(described_class).not_to receive(:new)
+
+ described_class.import_if_issue(issue, project, client)
+ end
+ end
+
+ describe '#execute' do
+ let(:importer) { described_class.new(issue, project, client) }
+
+ it 'creates the issue and assignees' do
+ expect(importer)
+ .to receive(:create_issue)
+ .and_return(10)
+
+ expect(importer)
+ .to receive(:create_assignees)
+ .with(10)
+
+ expect(importer.issuable_finder)
+ .to receive(:cache_database_id)
+ .with(10)
+
+ importer.execute
+ end
+ end
+
+ describe '#create_issue' do
+ let(:importer) { described_class.new(issue, project, client) }
+
+ before do
+ allow(importer.milestone_finder)
+ .to receive(:id_for)
+ .with(issue)
+ .and_return(milestone.id)
+ end
+
+ context 'when the issue author could be found' do
+ it 'creates the issue with the found author as the issue author' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Issue',
+ author_id: user.id,
+ project_id: project.id,
+ description: 'This is my issue',
+ milestone_id: milestone.id,
+ state: :opened,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.issues
+ )
+ .and_call_original
+
+ importer.create_issue
+ end
+ end
+
+ context 'when the issue author could not be found' do
+ it 'creates the issue with the project creator as the issue author' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Issue',
+ author_id: project.creator_id,
+ project_id: project.id,
+ description: "*Created by: alice*\n\nThis is my issue",
+ milestone_id: milestone.id,
+ state: :opened,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.issues
+ )
+ .and_call_original
+
+ importer.create_issue
+ end
+ end
+
+ context 'when the import fails due to a foreign key error' do
+ it 'does not raise any errors' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.create_issue }.not_to raise_error
+ end
+ end
+
+ it 'produces a valid Issue' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ importer.create_issue
+
+ expect(project.issues.take).to be_valid
+ end
+
+ it 'returns the ID of the created issue' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ expect(importer.create_issue).to be_a_kind_of(Numeric)
+ end
+ end
+
+ describe '#create_assignees' do
+ it 'inserts the issue assignees in bulk' do
+ importer = described_class.new(issue, project, client)
+
+ allow(importer.user_finder)
+ .to receive(:user_id_for)
+ .ordered.with(issue.assignees[0])
+ .and_return(4)
+
+ allow(importer.user_finder)
+ .to receive(:user_id_for)
+ .ordered.with(issue.assignees[1])
+ .and_return(5)
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ IssueAssignee.table_name,
+ [{ issue_id: 1, user_id: 4 }, { issue_id: 1, user_id: 5 }]
+ )
+
+ importer.create_assignees(1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
new file mode 100644
index 00000000000..e237e79e94b
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::IssuesImporter do
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:github_issue) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Issue',
+ body: 'This is my issue',
+ milestone: double(:milestone, number: 4),
+ state: 'open',
+ assignees: [double(:user, id: 4, login: 'alice')],
+ labels: [double(:label, name: 'bug')],
+ user: double(:user, id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ pull_request: false
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports issues in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports issues in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each issue in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ issue_importer = double(:importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_issue)
+
+ expect(Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Issue),
+ project,
+ client
+ )
+ .and_return(issue_importer)
+
+ expect(issue_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each issue in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_issue)
+
+ expect(Gitlab::GithubImport::ImportIssueWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the issue number of the given issue' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(github_issue))
+ .to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
new file mode 100644
index 00000000000..e2a71e78574
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::LabelLinksImporter do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:issue) do
+ double(
+ :issue,
+ iid: 4,
+ label_names: %w[bug],
+ issuable_type: Issue,
+ pull_request?: false
+ )
+ end
+
+ let(:importer) { described_class.new(issue, project, client) }
+
+ describe '#execute' do
+ it 'creates the label links' do
+ importer = described_class.new(issue, project, client)
+
+ expect(importer).to receive(:create_labels)
+
+ importer.execute
+ end
+ end
+
+ describe '#create_labels' do
+ it 'inserts the label links in bulk' do
+ expect(importer.label_finder)
+ .to receive(:id_for)
+ .with('bug')
+ .and_return(2)
+
+ expect(importer)
+ .to receive(:find_target_id)
+ .and_return(1)
+
+ Timecop.freeze do
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ LabelLink.table_name,
+ [
+ {
+ label_id: 2,
+ target_id: 1,
+ target_type: Issue,
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now
+ }
+ ]
+ )
+
+ importer.create_labels
+ end
+ end
+
+ it 'does not insert label links for non-existing labels' do
+ expect(importer.label_finder)
+ .to receive(:id_for)
+ .with('bug')
+ .and_return(nil)
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(LabelLink.table_name, [])
+
+ importer.create_labels
+ end
+ end
+
+ describe '#find_target_id' do
+ it 'returns the ID of the issuable to create the label link for' do
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:database_id)
+ .and_return(10)
+
+ expect(importer.find_target_id).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
new file mode 100644
index 00000000000..156ef96a0fa
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::LabelsImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+ let(:importer) { described_class.new(project, client) }
+
+ describe '#execute' do
+ it 'imports the labels in bulk' do
+ label_hash = { title: 'bug', color: '#fffaaa' }
+
+ expect(importer)
+ .to receive(:build_labels)
+ .and_return([label_hash])
+
+ expect(importer)
+ .to receive(:bulk_insert)
+ .with(Label, [label_hash])
+
+ expect(importer)
+ .to receive(:build_labels_cache)
+
+ importer.execute
+ end
+ end
+
+ describe '#build_labels' do
+ it 'returns an Array containnig label rows' do
+ label = double(:label, name: 'bug', color: 'ffffff')
+
+ expect(importer).to receive(:each_label).and_return([label])
+
+ rows = importer.build_labels
+
+ expect(rows.length).to eq(1)
+ expect(rows[0][:title]).to eq('bug')
+ end
+
+ it 'does not create labels that already exist' do
+ create(:label, project: project, title: 'bug')
+
+ label = double(:label, name: 'bug', color: 'ffffff')
+
+ expect(importer).to receive(:each_label).and_return([label])
+ expect(importer.build_labels).to be_empty
+ end
+ end
+
+ describe '#build_labels_cache' do
+ it 'builds the labels cache' do
+ expect_any_instance_of(Gitlab::GithubImport::LabelFinder)
+ .to receive(:build_cache)
+
+ importer.build_labels_cache
+ end
+ end
+
+ describe '#build' do
+ let(:label_hash) do
+ importer.build(double(:label, name: 'bug', color: 'ffffff'))
+ end
+
+ it 'returns the attributes of the label as a Hash' do
+ expect(label_hash).to be_an_instance_of(Hash)
+ end
+
+ context 'the returned Hash' do
+ it 'includes the label title' do
+ expect(label_hash[:title]).to eq('bug')
+ end
+
+ it 'includes the label color' do
+ expect(label_hash[:color]).to eq('#ffffff')
+ end
+
+ it 'includes the project ID' do
+ expect(label_hash[:project_id]).to eq(project.id)
+ end
+
+ it 'includes the label type' do
+ expect(label_hash[:type]).to eq('ProjectLabel')
+ end
+
+ it 'includes the created timestamp' do
+ Timecop.freeze do
+ expect(label_hash[:created_at]).to eq(Time.zone.now)
+ end
+ end
+
+ it 'includes the updated timestamp' do
+ Timecop.freeze do
+ expect(label_hash[:updated_at]).to eq(Time.zone.now)
+ end
+ end
+ end
+ end
+
+ describe '#each_label' do
+ it 'returns the labels' do
+ expect(client)
+ .to receive(:labels)
+ .with('foo/bar')
+
+ importer.each_label
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
new file mode 100644
index 00000000000..b1cac3b6e46
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+ let(:importer) { described_class.new(project, client) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:milestone) do
+ double(
+ :milestone,
+ number: 1,
+ title: '1.0',
+ description: 'The first release',
+ state: 'open',
+ created_at: created_at,
+ updated_at: updated_at
+ )
+ end
+
+ describe '#execute' do
+ it 'imports the milestones in bulk' do
+ milestone_hash = { number: 1, title: '1.0' }
+
+ expect(importer)
+ .to receive(:build_milestones)
+ .and_return([milestone_hash])
+
+ expect(importer)
+ .to receive(:bulk_insert)
+ .with(Milestone, [milestone_hash])
+
+ expect(importer)
+ .to receive(:build_milestones_cache)
+
+ importer.execute
+ end
+ end
+
+ describe '#build_milestones' do
+ it 'returns an Array containnig milestone rows' do
+ expect(importer)
+ .to receive(:each_milestone)
+ .and_return([milestone])
+
+ rows = importer.build_milestones
+
+ expect(rows.length).to eq(1)
+ expect(rows[0][:title]).to eq('1.0')
+ end
+
+ it 'does not create milestones that already exist' do
+ create(:milestone, project: project, title: '1.0', iid: 1)
+
+ expect(importer)
+ .to receive(:each_milestone)
+ .and_return([milestone])
+
+ expect(importer.build_milestones).to be_empty
+ end
+ end
+
+ describe '#build_milestones_cache' do
+ it 'builds the milestones cache' do
+ expect_any_instance_of(Gitlab::GithubImport::MilestoneFinder)
+ .to receive(:build_cache)
+
+ importer.build_milestones_cache
+ end
+ end
+
+ describe '#build' do
+ let(:milestone_hash) { importer.build(milestone) }
+
+ it 'returns the attributes of the milestone as a Hash' do
+ expect(milestone_hash).to be_an_instance_of(Hash)
+ end
+
+ context 'the returned Hash' do
+ it 'includes the milestone number' do
+ expect(milestone_hash[:iid]).to eq(1)
+ end
+
+ it 'includes the milestone title' do
+ expect(milestone_hash[:title]).to eq('1.0')
+ end
+
+ it 'includes the milestone description' do
+ expect(milestone_hash[:description]).to eq('The first release')
+ end
+
+ it 'includes the project ID' do
+ expect(milestone_hash[:project_id]).to eq(project.id)
+ end
+
+ it 'includes the milestone state' do
+ expect(milestone_hash[:state]).to eq(:active)
+ end
+
+ it 'includes the created timestamp' do
+ expect(milestone_hash[:created_at]).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(milestone_hash[:updated_at]).to eq(updated_at)
+ end
+ end
+ end
+
+ describe '#each_milestone' do
+ it 'returns the milestones' do
+ expect(client)
+ .to receive(:milestones)
+ .with('foo/bar', state: 'all')
+
+ importer.each_milestone
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
new file mode 100644
index 00000000000..9bdcc42be19
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::NoteImporter do
+ let(:client) { double(:client) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:github_note) do
+ Gitlab::GithubImport::Representation::Note.new(
+ noteable_id: 1,
+ noteable_type: 'Issue',
+ author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+ note: 'This is my note',
+ created_at: created_at,
+ updated_at: updated_at,
+ github_id: 1
+ )
+ end
+
+ let(:importer) { described_class.new(github_note, project, client) }
+
+ describe '#execute' do
+ context 'when the noteable exists' do
+ let!(:issue_row) { create(:issue, project: project, iid: 1) }
+
+ before do
+ allow(importer)
+ .to receive(:find_noteable_id)
+ .and_return(issue_row.id)
+ end
+
+ context 'when the author could be found' do
+ it 'imports the note with the found author as the note author' do
+ expect(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([user.id, true])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ Note.table_name,
+ [
+ {
+ noteable_type: 'Issue',
+ noteable_id: issue_row.id,
+ project_id: project.id,
+ author_id: user.id,
+ note: 'This is my note',
+ system: false,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+ end
+
+ context 'when the note author could not be found' do
+ it 'imports the note with the project creator as the note author' do
+ expect(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ Note.table_name,
+ [
+ {
+ noteable_type: 'Issue',
+ noteable_id: issue_row.id,
+ project_id: project.id,
+ author_id: project.creator_id,
+ note: "*Created by: alice*\n\nThis is my note",
+ system: false,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+ end
+ end
+
+ context 'when the noteable does not exist' do
+ it 'does not import the note' do
+ expect(Gitlab::Database).not_to receive(:bulk_insert)
+
+ importer.execute
+ end
+ end
+
+ context 'when the import fails due to a foreign key error' do
+ it 'does not raise any errors' do
+ issue_row = create(:issue, project: project, iid: 1)
+
+ allow(importer)
+ .to receive(:find_noteable_id)
+ .and_return(issue_row.id)
+
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([user.id, true])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.execute }.not_to raise_error
+ end
+ end
+
+ it 'produces a valid Note' do
+ issue_row = create(:issue, project: project, iid: 1)
+
+ allow(importer)
+ .to receive(:find_noteable_id)
+ .and_return(issue_row.id)
+
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([user.id, true])
+
+ importer.execute
+
+ expect(project.notes.take).to be_valid
+ end
+ end
+
+ describe '#find_noteable_id' do
+ it 'returns the ID of the noteable' do
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:database_id)
+ .and_return(10)
+
+ expect(importer.find_noteable_id).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
new file mode 100644
index 00000000000..f046d13f879
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::NotesImporter do
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ let(:github_comment) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/issues/42',
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ id: 1
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports notes in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports notes in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each note in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ note_importer = double(:note_importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::Importer::NoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Note),
+ project,
+ client
+ )
+ .and_return(note_importer)
+
+ expect(note_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each note in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::ImportNoteWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the ID of the given note' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(github_comment))
+ .to eq(1)
+ end
+ end
+
+ describe '#collection_options' do
+ it 'returns an empty Hash' do
+ # For large projects (e.g. kubernetes/kubernetes) GitHub's API may produce
+ # HTTP 500 errors when using explicit sorting options, regardless of what
+ # order you sort in. Not using any sorting options at all allows us to
+ # work around this.
+ importer = described_class.new(project, client)
+
+ expect(importer.collection_options).to eq({})
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
new file mode 100644
index 00000000000..35f3fdf8304
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -0,0 +1,221 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, :repository) }
+ let(:client) { double(:client) }
+ let(:user) { create(:user) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+ let(:merged_at) { Time.new(2017, 1, 1, 12, 17) }
+
+ let(:source_commit) { project.repository.commit('feature') }
+ let(:target_commit) { project.repository.commit('master') }
+ let(:milestone) { create(:milestone, project: project) }
+
+ let(:pull_request) do
+ alice = Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice')
+
+ Gitlab::GithubImport::Representation::PullRequest.new(
+ iid: 42,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_branch: 'feature',
+ source_branch_sha: source_commit.id,
+ target_branch: 'master',
+ target_branch_sha: target_commit.id,
+ source_repository_id: 400,
+ target_repository_id: 200,
+ source_repository_owner: 'alice',
+ state: :closed,
+ milestone_number: milestone.iid,
+ author: alice,
+ assignee: alice,
+ created_at: created_at,
+ updated_at: updated_at,
+ merged_at: merged_at
+ )
+ end
+
+ let(:importer) { described_class.new(pull_request, project, client) }
+
+ describe '#execute' do
+ it 'imports the pull request' do
+ expect(importer)
+ .to receive(:create_merge_request)
+ .and_return(10)
+
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:cache_database_id)
+ .with(10)
+
+ importer.execute
+ end
+ end
+
+ describe '#create_merge_request' do
+ before do
+ allow(importer.milestone_finder)
+ .to receive(:id_for)
+ .with(pull_request)
+ .and_return(milestone.id)
+ end
+
+ context 'when the author could be found' do
+ before do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([user.id, true])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+ end
+
+ it 'imports the pull request with the pull request author as the merge request author' do
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'alice:feature',
+ target_branch: 'master',
+ state: :merged,
+ milestone_id: milestone.id,
+ author_id: user.id,
+ assignee_id: user.id,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.merge_requests
+ )
+ .and_call_original
+
+ importer.create_merge_request
+ end
+
+ it 'returns the ID of the created merge request' do
+ id = importer.create_merge_request
+
+ expect(id).to be_a_kind_of(Numeric)
+ end
+
+ it 'creates the merge request diffs' do
+ importer.create_merge_request
+
+ mr = project.merge_requests.take
+
+ expect(mr.merge_request_diffs.exists?).to eq(true)
+ end
+ end
+
+ context 'when the author could not be found' do
+ it 'imports the pull request with the project creator as the merge request author' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([project.creator_id, false])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Pull Request',
+ description: "*Created by: alice*\n\nThis is my pull request",
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'alice:feature',
+ target_branch: 'master',
+ state: :merged,
+ milestone_id: milestone.id,
+ author_id: project.creator_id,
+ assignee_id: user.id,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.merge_requests
+ )
+ .and_call_original
+
+ importer.create_merge_request
+ end
+ end
+
+ context 'when the source and target branch are identical' do
+ it 'uses a generated source branch name for the merge request' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([user.id, true])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+
+ allow(pull_request)
+ .to receive(:source_repository_id)
+ .and_return(pull_request.target_repository_id)
+
+ allow(pull_request)
+ .to receive(:source_branch)
+ .and_return('master')
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'master-42',
+ target_branch: 'master',
+ state: :merged,
+ milestone_id: milestone.id,
+ author_id: user.id,
+ assignee_id: user.id,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.merge_requests
+ )
+ .and_call_original
+
+ importer.create_merge_request
+ end
+ end
+
+ context 'when the import fails due to a foreign key error' do
+ it 'does not raise any errors' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([user.id, true])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.create_merge_request }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
new file mode 100644
index 00000000000..d72572cd510
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -0,0 +1,272 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::PullRequestsImporter do
+ let(:project) { create(:project, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ let(:pull_request) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Pull Request',
+ body: 'This is my pull request',
+ state: 'closed',
+ head: double(
+ :head,
+ sha: '123abc',
+ ref: 'my-feature',
+ repo: double(:repo, id: 400),
+ user: double(:user, id: 4, login: 'alice')
+ ),
+ base: double(
+ :base,
+ sha: '456def',
+ ref: 'master',
+ repo: double(:repo, id: 200)
+ ),
+ milestone: double(:milestone, number: 4),
+ user: double(:user, id: 4, login: 'alice'),
+ assignee: double(:user, id: 4, login: 'alice'),
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ merged_at: Time.zone.now
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports pull requests in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports pull requests in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each pull request in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ pull_request_importer = double(:pull_request_importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(pull_request)
+
+ expect(Gitlab::GithubImport::Importer::PullRequestImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::PullRequest),
+ project,
+ client
+ )
+ .and_return(pull_request_importer)
+
+ expect(pull_request_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each note in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(pull_request)
+
+ expect(Gitlab::GithubImport::ImportPullRequestWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#each_object_to_import', :clean_gitlab_redis_cache do
+ let(:importer) { described_class.new(project, client) }
+
+ before do
+ page = double(:page, objects: [pull_request], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(
+ :pull_requests,
+ 'foo/bar',
+ { state: 'all', sort: 'created', direction: 'asc', page: 1 }
+ )
+ .and_yield(page)
+ end
+
+ it 'yields every pull request to the supplied block' do
+ expect { |b| importer.each_object_to_import(&b) }
+ .to yield_with_args(pull_request)
+ end
+
+ it 'updates the repository if a pull request was updated after the last clone' do
+ expect(importer)
+ .to receive(:update_repository?)
+ .with(pull_request)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_repository)
+
+ importer.each_object_to_import { }
+ end
+ end
+
+ describe '#update_repository' do
+ it 'updates the repository' do
+ importer = described_class.new(project, client)
+
+ expect(project.repository)
+ .to receive(:fetch_remote)
+ .with('github', forced: false)
+
+ expect(Rails.logger)
+ .to receive(:info)
+ .with(an_instance_of(String))
+
+ expect(importer.repository_updates_counter)
+ .to receive(:increment)
+ .with(project: project.path_with_namespace)
+ .and_call_original
+
+ Timecop.freeze do
+ importer.update_repository
+
+ expect(project.last_repository_updated_at).to eq(Time.zone.now)
+ end
+ end
+ end
+
+ describe '#update_repository?' do
+ let(:importer) { described_class.new(project, client) }
+
+ context 'when the pull request was updated after the last update' do
+ let(:pr) do
+ double(
+ :pr,
+ updated_at: Time.zone.now,
+ head: double(:head, sha: '123'),
+ base: double(:base, sha: '456')
+ )
+ end
+
+ before do
+ allow(project)
+ .to receive(:last_repository_updated_at)
+ .and_return(1.year.ago)
+ end
+
+ it 'returns true when the head SHA is not present' do
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.head.sha)
+ .and_return(false)
+
+ expect(importer.update_repository?(pr)).to eq(true)
+ end
+
+ it 'returns true when the base SHA is not present' do
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.head.sha)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.base.sha)
+ .and_return(false)
+
+ expect(importer.update_repository?(pr)).to eq(true)
+ end
+
+ it 'returns false if both the head and base SHAs are present' do
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.head.sha)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.base.sha)
+ .and_return(true)
+
+ expect(importer.update_repository?(pr)).to eq(false)
+ end
+ end
+
+ context 'when the pull request was updated before the last update' do
+ it 'returns false' do
+ pr = double(:pr, updated_at: 1.year.ago)
+
+ allow(project)
+ .to receive(:last_repository_updated_at)
+ .and_return(Time.zone.now)
+
+ expect(importer.update_repository?(pr)).to eq(false)
+ end
+ end
+ end
+
+ describe '#commit_exists?' do
+ let(:importer) { described_class.new(project, client) }
+
+ it 'returns true when a commit exists' do
+ expect(project.repository)
+ .to receive(:lookup)
+ .with('123')
+ .and_return(double(:commit))
+
+ expect(importer.commit_exists?('123')).to eq(true)
+ end
+
+ it 'returns false when a commit does not exist' do
+ expect(project.repository)
+ .to receive(:lookup)
+ .with('123')
+ .and_raise(Rugged::OdbError)
+
+ expect(importer.commit_exists?('123')).to eq(false)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the PR number of the given PR' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(pull_request))
+ .to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
new file mode 100644
index 00000000000..23ae026fb14
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::ReleasesImporter do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:importer) { described_class.new(project, client) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:release) do
+ double(
+ :release,
+ tag_name: '1.0',
+ body: 'This is my release',
+ created_at: created_at,
+ updated_at: updated_at
+ )
+ end
+
+ describe '#execute' do
+ it 'imports the releases in bulk' do
+ release_hash = {
+ tag_name: '1.0',
+ description: 'This is my release',
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(importer).to receive(:build_releases).and_return([release_hash])
+ expect(importer).to receive(:bulk_insert).with(Release, [release_hash])
+
+ importer.execute
+ end
+ end
+
+ describe '#build_releases' do
+ it 'returns an Array containnig release rows' do
+ expect(importer).to receive(:each_release).and_return([release])
+
+ rows = importer.build_releases
+
+ expect(rows.length).to eq(1)
+ expect(rows[0][:tag]).to eq('1.0')
+ end
+
+ it 'does not create releases that already exist' do
+ create(:release, project: project, tag: '1.0', description: '1.0')
+
+ expect(importer).to receive(:each_release).and_return([release])
+ expect(importer.build_releases).to be_empty
+ end
+
+ it 'uses a default release description if none is provided' do
+ expect(release).to receive(:body).and_return('')
+ expect(importer).to receive(:each_release).and_return([release])
+
+ release = importer.build_releases.first
+
+ expect(release[:description]).to eq('Release for tag 1.0')
+ end
+ end
+
+ describe '#build' do
+ let(:release_hash) { importer.build(release) }
+
+ it 'returns the attributes of the release as a Hash' do
+ expect(release_hash).to be_an_instance_of(Hash)
+ end
+
+ context 'the returned Hash' do
+ it 'includes the tag name' do
+ expect(release_hash[:tag]).to eq('1.0')
+ end
+
+ it 'includes the release description' do
+ expect(release_hash[:description]).to eq('This is my release')
+ end
+
+ it 'includes the project ID' do
+ expect(release_hash[:project_id]).to eq(project.id)
+ end
+
+ it 'includes the created timestamp' do
+ expect(release_hash[:created_at]).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(release_hash[:updated_at]).to eq(updated_at)
+ end
+ end
+ end
+
+ describe '#each_release' do
+ let(:release) { double(:release) }
+
+ before do
+ allow(project).to receive(:import_source).and_return('foo/bar')
+
+ allow(client)
+ .to receive(:releases)
+ .with('foo/bar')
+ .and_return([release].to_enum)
+ end
+
+ it 'returns an Enumerator' do
+ expect(importer.each_release).to be_an_instance_of(Enumerator)
+ end
+
+ it 'yields every release to the Enumerator' do
+ expect(importer.each_release.next).to eq(release)
+ end
+ end
+
+ describe '#description_for' do
+ it 'returns the description when present' do
+ expect(importer.description_for(release)).to eq(release.body)
+ end
+
+ it 'returns a generated description when one is not present' do
+ allow(release).to receive(:body).and_return('')
+
+ expect(importer.description_for(release)).to eq('Release for tag 1.0')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
new file mode 100644
index 00000000000..80539807711
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -0,0 +1,264 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::RepositoryImporter do
+ let(:repository) { double(:repository) }
+ let(:client) { double(:client) }
+
+ let(:project) do
+ double(
+ :project,
+ import_url: 'foo.git',
+ import_source: 'foo/bar',
+ repository_storage_path: 'foo',
+ disk_path: 'foo',
+ repository: repository
+ )
+ end
+
+ let(:importer) { described_class.new(project, client) }
+ let(:shell_adapter) { Gitlab::Shell.new }
+
+ before do
+ # The method "gitlab_shell" returns a new instance every call, making
+ # it harder to set expectations. To work around this we'll stub the method
+ # and return the same instance on every call.
+ allow(importer).to receive(:gitlab_shell).and_return(shell_adapter)
+ end
+
+ describe '#import_wiki?' do
+ it 'returns true if the wiki should be imported' do
+ repo = double(:repo, has_wiki: true)
+
+ expect(client)
+ .to receive(:repository)
+ .with('foo/bar')
+ .and_return(repo)
+
+ expect(project)
+ .to receive(:wiki_repository_exists?)
+ .and_return(false)
+
+ expect(importer.import_wiki?).to eq(true)
+ end
+
+ it 'returns false if the GitHub wiki is disabled' do
+ repo = double(:repo, has_wiki: false)
+
+ expect(client)
+ .to receive(:repository)
+ .with('foo/bar')
+ .and_return(repo)
+
+ expect(importer.import_wiki?).to eq(false)
+ end
+
+ it 'returns false if the wiki has already been imported' do
+ repo = double(:repo, has_wiki: true)
+
+ expect(client)
+ .to receive(:repository)
+ .with('foo/bar')
+ .and_return(repo)
+
+ expect(project)
+ .to receive(:wiki_repository_exists?)
+ .and_return(true)
+
+ expect(importer.import_wiki?).to eq(false)
+ end
+ end
+
+ describe '#execute' do
+ it 'imports the repository and wiki' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_clone_time)
+
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'does not import the repository if it already exists' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(true)
+
+ expect(importer)
+ .not_to receive(:import_repository)
+
+ expect(importer)
+ .to receive(:import_wiki_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_clone_time)
+
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'does not import the wiki if it is disabled' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:import_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_clone_time)
+
+ expect(importer)
+ .not_to receive(:import_wiki_repository)
+
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'does not import the wiki if the repository could not be imported' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_repository)
+ .and_return(false)
+
+ expect(importer)
+ .not_to receive(:update_clone_time)
+
+ expect(importer)
+ .not_to receive(:import_wiki_repository)
+
+ expect(importer.execute).to eq(false)
+ end
+ end
+
+ describe '#import_repository' do
+ it 'imports the repository' do
+ expect(project)
+ .to receive(:ensure_repository)
+
+ expect(importer)
+ .to receive(:configure_repository_remote)
+
+ expect(repository)
+ .to receive(:fetch_remote)
+ .with('github', forced: true)
+
+ expect(importer.import_repository).to eq(true)
+ end
+
+ it 'marks the import as failed when an error was raised' do
+ expect(project).to receive(:ensure_repository)
+ .and_raise(Gitlab::Git::Repository::NoRepository)
+
+ expect(importer)
+ .to receive(:fail_import)
+ .and_return(false)
+
+ expect(importer.import_repository).to eq(false)
+ end
+ end
+
+ describe '#configure_repository_remote' do
+ it 'configures the remote details' do
+ expect(repository)
+ .to receive(:remote_exists?)
+ .with('github')
+ .and_return(false)
+
+ expect(repository)
+ .to receive(:add_remote)
+ .with('github', 'foo.git')
+
+ expect(repository)
+ .to receive(:set_import_remote_as_mirror)
+ .with('github')
+
+ expect(repository)
+ .to receive(:add_remote_fetch_config)
+
+ importer.configure_repository_remote
+ end
+
+ it 'does not configure the remote if already configured' do
+ expect(repository)
+ .to receive(:remote_exists?)
+ .with('github')
+ .and_return(true)
+
+ expect(repository)
+ .not_to receive(:add_remote)
+
+ importer.configure_repository_remote
+ end
+ end
+
+ describe '#import_wiki_repository' do
+ it 'imports the wiki repository' do
+ expect(importer.gitlab_shell)
+ .to receive(:import_repository)
+ .with('foo', 'foo.wiki', 'foo.wiki.git')
+
+ expect(importer.import_wiki_repository).to eq(true)
+ end
+
+ it 'marks the import as failed if an error was raised' do
+ expect(importer.gitlab_shell)
+ .to receive(:import_repository)
+ .and_raise(Gitlab::Shell::Error)
+
+ expect(importer)
+ .to receive(:fail_import)
+ .and_return(false)
+
+ expect(importer.import_wiki_repository).to eq(false)
+ end
+ end
+
+ describe '#fail_import' do
+ it 'marks the import as failed' do
+ expect(project).to receive(:mark_import_as_failed).with('foo')
+
+ expect(importer.fail_import('foo')).to eq(false)
+ end
+ end
+
+ describe '#update_clone_time' do
+ it 'sets the timestamp for when the cloning process finished' do
+ Timecop.freeze do
+ expect(project)
+ .to receive(:update_column)
+ .with(:last_repository_updated_at, Time.zone.now)
+
+ importer.update_clone_time
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
new file mode 100644
index 00000000000..da69911812a
--- /dev/null
+++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do
+ let(:project) { double(:project, id: 4) }
+ let(:issue) do
+ double(:issue, issuable_type: MergeRequest, iid: 1)
+ end
+
+ let(:finder) { described_class.new(project, issue) }
+
+ describe '#database_id' do
+ it 'returns nil when no cache is in place' do
+ expect(finder.database_id).to be_nil
+ end
+
+ it 'returns the ID of an issuable when the cache is in place' do
+ finder.cache_database_id(10)
+
+ expect(finder.database_id).to eq(10)
+ end
+
+ it 'raises TypeError when the object is not supported' do
+ finder = described_class.new(project, double(:issue))
+
+ expect { finder.database_id }.to raise_error(TypeError)
+ end
+ end
+
+ describe '#cache_database_id' do
+ it 'caches the ID of a database row' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with('github-import/issuable-finder/4/MergeRequest/1', 10)
+
+ finder.cache_database_id(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/label_finder_spec.rb b/spec/lib/gitlab/github_import/label_finder_spec.rb
new file mode 100644
index 00000000000..8ba766944d6
--- /dev/null
+++ b/spec/lib/gitlab/github_import/label_finder_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:finder) { described_class.new(project) }
+ let!(:bug) { create(:label, project: project, name: 'Bug') }
+ let!(:feature) { create(:label, project: project, name: 'Feature') }
+
+ describe '#id_for' do
+ context 'with a cache in place' do
+ before do
+ finder.build_cache
+ end
+
+ it 'returns the ID of the given label' do
+ expect(finder.id_for(feature.name)).to eq(feature.id)
+ end
+
+ it 'returns nil for an empty cache key' do
+ key = finder.cache_key_for(bug.name)
+
+ Gitlab::GithubImport::Caching.write(key, '')
+
+ expect(finder.id_for(bug.name)).to be_nil
+ end
+
+ it 'returns nil for a non existing label name' do
+ expect(finder.id_for('kittens')).to be_nil
+ end
+ end
+
+ context 'without a cache in place' do
+ it 'returns nil for a label' do
+ expect(finder.id_for(feature.name)).to be_nil
+ end
+ end
+ end
+
+ describe '#build_cache' do
+ it 'builds the cache of all project labels' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write_multiple)
+ .with(
+ {
+ "github-import/label-finder/#{project.id}/Bug" => bug.id,
+ "github-import/label-finder/#{project.id}/Feature" => feature.id
+ }
+ )
+ .and_call_original
+
+ finder.build_cache
+ end
+ end
+
+ describe '#cache_key_for' do
+ it 'returns the cache key for a label name' do
+ expect(finder.cache_key_for('foo'))
+ .to eq("github-import/label-finder/#{project.id}/foo")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb
new file mode 100644
index 00000000000..1ff5b9d66b3
--- /dev/null
+++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::MarkdownText do
+ describe '.format' do
+ it 'formats the text' do
+ author = double(:author, login: 'Alice')
+ text = described_class.format('Hello', author)
+
+ expect(text).to eq("*Created by: Alice*\n\nHello")
+ end
+ end
+
+ describe '#to_s' do
+ it 'returns the text when the author was found' do
+ author = double(:author, login: 'Alice')
+ text = described_class.new('Hello', author, true)
+
+ expect(text.to_s).to eq('Hello')
+ end
+
+ it 'returns the text with an extra header when the author was not found' do
+ author = double(:author, login: 'Alice')
+ text = described_class.new('Hello', author)
+
+ expect(text.to_s).to eq("*Created by: Alice*\n\nHello")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/milestone_finder_spec.rb b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
new file mode 100644
index 00000000000..dff931a2fe8
--- /dev/null
+++ b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache do
+ let!(:project) { create(:project) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let(:finder) { described_class.new(project) }
+
+ describe '#id_for' do
+ let(:issuable) { double(:issuable, milestone_number: milestone.iid) }
+
+ context 'with a cache in place' do
+ before do
+ finder.build_cache
+ end
+
+ it 'returns the milestone ID of the given issuable' do
+ expect(finder.id_for(issuable)).to eq(milestone.id)
+ end
+
+ it 'returns nil for an empty cache key' do
+ key = finder.cache_key_for(milestone.iid)
+
+ Gitlab::GithubImport::Caching.write(key, '')
+
+ expect(finder.id_for(issuable)).to be_nil
+ end
+
+ it 'returns nil for an issuable with a non-existing milestone' do
+ expect(finder.id_for(double(:issuable, milestone_number: 5))).to be_nil
+ end
+ end
+
+ context 'without a cache in place' do
+ it 'returns nil' do
+ expect(finder.id_for(issuable)).to be_nil
+ end
+ end
+ end
+
+ describe '#build_cache' do
+ it 'builds the cache of all project milestones' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write_multiple)
+ .with("github-import/milestone-finder/#{project.id}/1" => milestone.id)
+ .and_call_original
+
+ finder.build_cache
+ end
+ end
+
+ describe '#cache_key_for' do
+ it 'returns the cache key for an IID' do
+ expect(finder.cache_key_for(10))
+ .to eq("github-import/milestone-finder/#{project.id}/10")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/page_counter_spec.rb b/spec/lib/gitlab/github_import/page_counter_spec.rb
new file mode 100644
index 00000000000..c2613a9a415
--- /dev/null
+++ b/spec/lib/gitlab/github_import/page_counter_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
+ let(:project) { double(:project, id: 1) }
+ let(:counter) { described_class.new(project, :issues) }
+
+ describe '#initialize' do
+ it 'sets the initial page number to 1 when no value is cached' do
+ expect(counter.current).to eq(1)
+ end
+
+ it 'sets the initial page number to the cached value when one is present' do
+ Gitlab::GithubImport::Caching.write(counter.cache_key, 2)
+
+ expect(described_class.new(project, :issues).current).to eq(2)
+ end
+ end
+
+ describe '#set' do
+ it 'overwrites the page number when the given number is greater than the current number' do
+ counter.set(4)
+ expect(counter.current).to eq(4)
+ end
+
+ it 'does not overwrite the page number when the given number is lower than the current number' do
+ counter.set(2)
+ counter.set(1)
+
+ expect(counter.current).to eq(2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
new file mode 100644
index 00000000000..e2a821d4d5c
--- /dev/null
+++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ParallelImporter do
+ describe '.async?' do
+ it 'returns true' do
+ expect(described_class).to be_async
+ end
+ end
+
+ describe '#execute', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project) }
+ let(:importer) { described_class.new(project) }
+
+ before do
+ expect(Gitlab::GithubImport::Stage::ImportRepositoryWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+ .and_return('123')
+ end
+
+ it 'schedules the importing of the repository' do
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'sets the JID in Redis' do
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:set)
+ .with("github-importer/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ .and_call_original
+
+ importer.execute
+ end
+
+ it 'updates the import JID of the project' do
+ importer.execute
+
+ expect(project.import_jid).to eq("github-importer/#{project.id}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
new file mode 100644
index 00000000000..98205d3ee25
--- /dev/null
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -0,0 +1,296 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ParallelScheduling do
+ let(:importer_class) do
+ Class.new do
+ include(Gitlab::GithubImport::ParallelScheduling)
+
+ def collection_method
+ :issues
+ end
+ end
+ end
+
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ expect(importer_class.new(project, client)).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = importer_class.new(project, client, parallel: false)
+
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ it 'imports data in parallel when running in parallel mode' do
+ importer = importer_class.new(project, client)
+ waiter = double(:waiter)
+
+ expect(importer)
+ .to receive(:parallel_import)
+ .and_return(waiter)
+
+ expect(importer.execute)
+ .to eq(waiter)
+ end
+
+ it 'imports data in parallel when running in sequential mode' do
+ importer = importer_class.new(project, client, parallel: false)
+
+ expect(importer)
+ .to receive(:sequential_import)
+ .and_return([])
+
+ expect(importer.execute)
+ .to eq([])
+ end
+
+ it 'expires the cache used for tracking already imported objects' do
+ importer = importer_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:expire)
+ .with(importer.already_imported_cache_key, a_kind_of(Numeric))
+
+ importer.execute
+ end
+ end
+
+ describe '#sequential_import' do
+ let(:importer) { importer_class.new(project, client, parallel: false) }
+
+ it 'imports data in sequence' do
+ repr_class = double(:representation_class)
+ repr_instance = double(:representation_instance)
+ gh_importer = double(:github_importer)
+ gh_importer_instance = double(:github_importer_instance)
+ object = double(:object)
+
+ expect(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(object)
+
+ expect(importer)
+ .to receive(:representation_class)
+ .and_return(repr_class)
+
+ expect(repr_class)
+ .to receive(:from_api_response)
+ .with(object)
+ .and_return(repr_instance)
+
+ expect(importer)
+ .to receive(:importer_class)
+ .and_return(gh_importer)
+
+ expect(gh_importer)
+ .to receive(:new)
+ .with(repr_instance, project, client)
+ .and_return(gh_importer_instance)
+
+ expect(gh_importer_instance)
+ .to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ let(:importer) { importer_class.new(project, client) }
+
+ it 'imports data in parallel' do
+ repr_class = double(:representation)
+ worker_class = double(:worker)
+ object = double(:object)
+
+ expect(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(object)
+
+ expect(importer)
+ .to receive(:representation_class)
+ .and_return(repr_class)
+
+ expect(importer)
+ .to receive(:sidekiq_worker_class)
+ .and_return(worker_class)
+
+ expect(repr_class)
+ .to receive(:from_api_response)
+ .with(object)
+ .and_return({ title: 'Foo' })
+
+ expect(worker_class)
+ .to receive(:perform_async)
+ .with(project.id, { title: 'Foo' }, an_instance_of(String))
+
+ expect(importer.parallel_import)
+ .to be_an_instance_of(Gitlab::JobWaiter)
+ end
+ end
+
+ describe '#each_object_to_import' do
+ let(:importer) { importer_class.new(project, client) }
+ let(:object) { double(:object) }
+
+ before do
+ expect(importer)
+ .to receive(:collection_options)
+ .and_return({ state: 'all' })
+ end
+
+ it 'yields every object to import' do
+ page = double(:page, objects: [object], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 1 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(1)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:mark_as_imported)
+ .with(object)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .to yield_with_args(object)
+ end
+
+ it 'resumes from the last page' do
+ page = double(:page, objects: [object], number: 2)
+
+ expect(importer.page_counter)
+ .to receive(:current)
+ .and_return(2)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 2 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(2)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:mark_as_imported)
+ .with(object)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .to yield_with_args(object)
+ end
+
+ it 'does not yield any objects if the page number was not set' do
+ page = double(:page, objects: [object], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 1 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(1)
+ .and_return(false)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .not_to yield_control
+ end
+
+ it 'does not yield the object if it was already imported' do
+ page = double(:page, objects: [object], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 1 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(1)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(true)
+
+ expect(importer)
+ .not_to receive(:mark_as_imported)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .not_to yield_control
+ end
+ end
+
+ describe '#already_imported?', :clean_gitlab_redis_cache do
+ let(:importer) { importer_class.new(project, client) }
+
+ it 'returns false when an object has not yet been imported' do
+ object = double(:object, id: 10)
+
+ expect(importer)
+ .to receive(:id_for_already_imported_cache)
+ .with(object)
+ .and_return(object.id)
+
+ expect(importer.already_imported?(object))
+ .to eq(false)
+ end
+
+ it 'returns true when an object has already been imported' do
+ object = double(:object, id: 10)
+
+ allow(importer)
+ .to receive(:id_for_already_imported_cache)
+ .with(object)
+ .and_return(object.id)
+
+ importer.mark_as_imported(object)
+
+ expect(importer.already_imported?(object))
+ .to eq(true)
+ end
+ end
+
+ describe '#mark_as_imported', :clean_gitlab_redis_cache do
+ it 'marks an object as already imported' do
+ object = double(:object, id: 10)
+ importer = importer_class.new(project, client)
+
+ expect(importer)
+ .to receive(:id_for_already_imported_cache)
+ .with(object)
+ .and_return(object.id)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:set_add)
+ .with(importer.already_imported_cache_key, object.id)
+ .and_call_original
+
+ importer.mark_as_imported(object)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
new file mode 100644
index 00000000000..7b0a1ea4948
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
@@ -0,0 +1,164 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::DiffNote do
+ let(:hunk) do
+ '@@ -1 +1 @@
+ -Hello
+ +Hello world'
+ end
+
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ shared_examples 'a DiffNote' do
+ it 'returns an instance of DiffNote' do
+ expect(note).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned DiffNote' do
+ it 'includes the number of the note' do
+ expect(note.noteable_id).to eq(42)
+ end
+
+ it 'includes the file path of the diff' do
+ expect(note.file_path).to eq('README.md')
+ end
+
+ it 'includes the commit ID' do
+ expect(note.commit_id).to eq('123abc')
+ end
+
+ it 'includes the user details' do
+ expect(note.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(note.author.id).to eq(4)
+ expect(note.author.login).to eq('alice')
+ end
+
+ it 'includes the note body' do
+ expect(note.note).to eq('Hello world')
+ end
+
+ it 'includes the created timestamp' do
+ expect(note.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(note.updated_at).to eq(updated_at)
+ end
+
+ it 'includes the GitHub ID' do
+ expect(note.github_id).to eq(1)
+ end
+
+ it 'returns the noteable type' do
+ expect(note.noteable_type).to eq('MergeRequest')
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/pull/42',
+ path: 'README.md',
+ commit_id: '123abc',
+ diff_hunk: hunk,
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: created_at,
+ updated_at: updated_at,
+ id: 1
+ )
+ end
+
+ it_behaves_like 'a DiffNote' do
+ let(:note) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ note = described_class.from_api_response(response)
+
+ expect(note.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a DiffNote' do
+ let(:hash) do
+ {
+ 'noteable_type' => 'MergeRequest',
+ 'noteable_id' => 42,
+ 'file_path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => hunk,
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+ end
+
+ let(:note) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'noteable_type' => 'MergeRequest',
+ 'noteable_id' => 42,
+ 'file_path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => hunk,
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+
+ note = described_class.from_json_hash(hash)
+
+ expect(note.author).to be_nil
+ end
+ end
+
+ describe '#line_code' do
+ it 'returns a String' do
+ note = described_class.new(diff_hunk: hunk, file_path: 'README.md')
+
+ expect(note.line_code).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#diff_hash' do
+ it 'returns a Hash containing the diff details' do
+ note = described_class.from_json_hash(
+ 'noteable_type' => 'MergeRequest',
+ 'noteable_id' => 42,
+ 'file_path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => hunk,
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ )
+
+ expect(note.diff_hash).to eq(
+ diff: hunk,
+ new_path: 'README.md',
+ old_path: 'README.md',
+ a_mode: '100644',
+ b_mode: '100644',
+ new_file: false
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
new file mode 100644
index 00000000000..15de0fe49ff
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::ExposeAttribute do
+ it 'defines a getter method that returns an attribute value' do
+ klass = Class.new do
+ include Gitlab::GithubImport::Representation::ExposeAttribute
+
+ expose_attribute :number
+
+ attr_reader :attributes
+
+ def initialize
+ @attributes = { number: 42 }
+ end
+ end
+
+ expect(klass.new.number).to eq(42)
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/issue_spec.rb b/spec/lib/gitlab/github_import/representation/issue_spec.rb
new file mode 100644
index 00000000000..99330ce42cb
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/issue_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::Issue do
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ shared_examples 'an Issue' do
+ it 'returns an instance of Issue' do
+ expect(issue).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned Issue' do
+ it 'includes the issue number' do
+ expect(issue.iid).to eq(42)
+ end
+
+ it 'includes the issue title' do
+ expect(issue.title).to eq('My Issue')
+ end
+
+ it 'includes the issue description' do
+ expect(issue.description).to eq('This is my issue')
+ end
+
+ it 'includes the milestone number' do
+ expect(issue.milestone_number).to eq(4)
+ end
+
+ it 'includes the issue state' do
+ expect(issue.state).to eq(:opened)
+ end
+
+ it 'includes the issue assignees' do
+ expect(issue.assignees[0])
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(issue.assignees[0].id).to eq(4)
+ expect(issue.assignees[0].login).to eq('alice')
+ end
+
+ it 'includes the label names' do
+ expect(issue.label_names).to eq(%w[bug])
+ end
+
+ it 'includes the author details' do
+ expect(issue.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(issue.author.id).to eq(4)
+ expect(issue.author.login).to eq('alice')
+ end
+
+ it 'includes the created timestamp' do
+ expect(issue.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(issue.updated_at).to eq(updated_at)
+ end
+
+ it 'is not a pull request' do
+ expect(issue.pull_request?).to eq(false)
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Issue',
+ body: 'This is my issue',
+ milestone: double(:milestone, number: 4),
+ state: 'open',
+ assignees: [double(:user, id: 4, login: 'alice')],
+ labels: [double(:label, name: 'bug')],
+ user: double(:user, id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ pull_request: false
+ )
+ end
+
+ it_behaves_like 'an Issue' do
+ let(:issue) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ issue = described_class.from_api_response(response)
+
+ expect(issue.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'an Issue' do
+ let(:hash) do
+ {
+ 'iid' => 42,
+ 'title' => 'My Issue',
+ 'description' => 'This is my issue',
+ 'milestone_number' => 4,
+ 'state' => 'opened',
+ 'assignees' => [{ 'id' => 4, 'login' => 'alice' }],
+ 'label_names' => %w[bug],
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'pull_request' => false
+ }
+ end
+
+ let(:issue) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Issue',
+ 'description' => 'This is my issue',
+ 'milestone_number' => 4,
+ 'state' => 'opened',
+ 'assignees' => [{ 'id' => 4, 'login' => 'alice' }],
+ 'label_names' => %w[bug],
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'pull_request' => false
+ }
+
+ issue = described_class.from_json_hash(hash)
+
+ expect(issue.author).to be_nil
+ end
+ end
+
+ describe '#labels?' do
+ it 'returns true when the issue has labels assigned' do
+ issue = described_class.new(label_names: %w[bug])
+
+ expect(issue.labels?).to eq(true)
+ end
+
+ it 'returns false when the issue has no labels assigned' do
+ issue = described_class.new(label_names: [])
+
+ expect(issue.labels?).to eq(false)
+ end
+ end
+
+ describe '#pull_request?' do
+ it 'returns false for an issue' do
+ issue = described_class.new(pull_request: false)
+
+ expect(issue.pull_request?).to eq(false)
+ end
+
+ it 'returns true for a pull request' do
+ issue = described_class.new(pull_request: true)
+
+ expect(issue.pull_request?).to eq(true)
+ end
+ end
+
+ describe '#truncated_title' do
+ it 'truncates the title to 255 characters' do
+ object = described_class.new(title: 'm' * 300)
+
+ expect(object.truncated_title.length).to eq(255)
+ end
+
+ it 'does not truncate the title if it is shorter than 255 characters' do
+ object = described_class.new(title: 'foo')
+
+ expect(object.truncated_title).to eq('foo')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/note_spec.rb b/spec/lib/gitlab/github_import/representation/note_spec.rb
new file mode 100644
index 00000000000..f2c1c66b357
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/note_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::Note do
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ shared_examples 'a Note' do
+ it 'returns an instance of Note' do
+ expect(note).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned Note' do
+ it 'includes the noteable ID' do
+ expect(note.noteable_id).to eq(42)
+ end
+
+ it 'includes the noteable type' do
+ expect(note.noteable_type).to eq('Issue')
+ end
+
+ it 'includes the author details' do
+ expect(note.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(note.author.id).to eq(4)
+ expect(note.author.login).to eq('alice')
+ end
+
+ it 'includes the note body' do
+ expect(note.note).to eq('Hello world')
+ end
+
+ it 'includes the created timestamp' do
+ expect(note.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(note.updated_at).to eq(updated_at)
+ end
+
+ it 'includes the GitHub ID' do
+ expect(note.github_id).to eq(1)
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/issues/42',
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: created_at,
+ updated_at: updated_at,
+ id: 1
+ )
+ end
+
+ it_behaves_like 'a Note' do
+ let(:note) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ note = described_class.from_api_response(response)
+
+ expect(note.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a Note' do
+ let(:hash) do
+ {
+ 'noteable_id' => 42,
+ 'noteable_type' => 'Issue',
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+ end
+
+ let(:note) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'noteable_id' => 42,
+ 'noteable_type' => 'Issue',
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+
+ note = described_class.from_json_hash(hash)
+
+ expect(note.author).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
new file mode 100644
index 00000000000..33f6ff0ae6a
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
@@ -0,0 +1,288 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::PullRequest do
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+ let(:merged_at) { Time.new(2017, 1, 1, 12, 17) }
+
+ shared_examples 'a PullRequest' do
+ it 'returns an instance of PullRequest' do
+ expect(pr).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned PullRequest' do
+ it 'includes the pull request number' do
+ expect(pr.iid).to eq(42)
+ end
+
+ it 'includes the pull request title' do
+ expect(pr.title).to eq('My Pull Request')
+ end
+
+ it 'includes the pull request description' do
+ expect(pr.description).to eq('This is my pull request')
+ end
+
+ it 'includes the source branch name' do
+ expect(pr.source_branch).to eq('my-feature')
+ end
+
+ it 'includes the source branch SHA' do
+ expect(pr.source_branch_sha).to eq('123abc')
+ end
+
+ it 'includes the target branch name' do
+ expect(pr.target_branch).to eq('master')
+ end
+
+ it 'includes the target branch SHA' do
+ expect(pr.target_branch_sha).to eq('456def')
+ end
+
+ it 'includes the milestone number' do
+ expect(pr.milestone_number).to eq(4)
+ end
+
+ it 'includes the user details' do
+ expect(pr.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(pr.author.id).to eq(4)
+ expect(pr.author.login).to eq('alice')
+ end
+
+ it 'includes the assignee details' do
+ expect(pr.assignee)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(pr.assignee.id).to eq(4)
+ expect(pr.assignee.login).to eq('alice')
+ end
+
+ it 'includes the created timestamp' do
+ expect(pr.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(pr.updated_at).to eq(updated_at)
+ end
+
+ it 'includes the merged timestamp' do
+ expect(pr.merged_at).to eq(merged_at)
+ end
+
+ it 'includes the source repository ID' do
+ expect(pr.source_repository_id).to eq(400)
+ end
+
+ it 'includes the target repository ID' do
+ expect(pr.target_repository_id).to eq(200)
+ end
+
+ it 'includes the source repository owner name' do
+ expect(pr.source_repository_owner).to eq('alice')
+ end
+
+ it 'includes the pull request state' do
+ expect(pr.state).to eq(:merged)
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Pull Request',
+ body: 'This is my pull request',
+ state: 'closed',
+ head: double(
+ :head,
+ sha: '123abc',
+ ref: 'my-feature',
+ repo: double(:repo, id: 400),
+ user: double(:user, id: 4, login: 'alice')
+ ),
+ base: double(
+ :base,
+ sha: '456def',
+ ref: 'master',
+ repo: double(:repo, id: 200)
+ ),
+ milestone: double(:milestone, number: 4),
+ user: double(:user, id: 4, login: 'alice'),
+ assignee: double(:user, id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ merged_at: merged_at
+ )
+ end
+
+ it_behaves_like 'a PullRequest' do
+ let(:pr) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ pr = described_class.from_api_response(response)
+
+ expect(pr.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a PullRequest' do
+ let(:hash) do
+ {
+ 'iid' => 42,
+ 'title' => 'My Pull Request',
+ 'description' => 'This is my pull request',
+ 'source_branch' => 'my-feature',
+ 'source_branch_sha' => '123abc',
+ 'target_branch' => 'master',
+ 'target_branch_sha' => '456def',
+ 'source_repository_id' => 400,
+ 'target_repository_id' => 200,
+ 'source_repository_owner' => 'alice',
+ 'state' => 'closed',
+ 'milestone_number' => 4,
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'assignee' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'merged_at' => merged_at.to_s
+ }
+ end
+
+ let(:pr) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Pull Request',
+ 'description' => 'This is my pull request',
+ 'source_branch' => 'my-feature',
+ 'source_branch_sha' => '123abc',
+ 'target_branch' => 'master',
+ 'target_branch_sha' => '456def',
+ 'source_repository_id' => 400,
+ 'target_repository_id' => 200,
+ 'source_repository_owner' => 'alice',
+ 'state' => 'closed',
+ 'milestone_number' => 4,
+ 'assignee' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'merged_at' => merged_at.to_s
+ }
+
+ pr = described_class.from_json_hash(hash)
+
+ expect(pr.author).to be_nil
+ end
+ end
+
+ describe '#state' do
+ it 'returns :opened for an open pull request' do
+ pr = described_class.new(state: :opened)
+
+ expect(pr.state).to eq(:opened)
+ end
+
+ it 'returns :closed for a closed pull request' do
+ pr = described_class.new(state: :closed)
+
+ expect(pr.state).to eq(:closed)
+ end
+
+ it 'returns :merged for a merged pull request' do
+ pr = described_class.new(state: :closed, merged_at: merged_at)
+
+ expect(pr.state).to eq(:merged)
+ end
+ end
+
+ describe '#cross_project?' do
+ it 'returns false for a pull request submitted from the target project' do
+ pr = described_class.new(source_repository_id: 1, target_repository_id: 1)
+
+ expect(pr).not_to be_cross_project
+ end
+
+ it 'returns true for a pull request submitted from a different project' do
+ pr = described_class.new(source_repository_id: 1, target_repository_id: 2)
+
+ expect(pr).to be_cross_project
+ end
+
+ it 'returns true if no source repository is present' do
+ pr = described_class.new(target_repository_id: 2)
+
+ expect(pr).to be_cross_project
+ end
+ end
+
+ describe '#formatted_source_branch' do
+ context 'for a cross-project pull request' do
+ it 'includes the owner name in the branch name' do
+ pr = described_class.new(
+ source_repository_owner: 'foo',
+ source_branch: 'branch',
+ target_branch: 'master',
+ source_repository_id: 1,
+ target_repository_id: 2
+ )
+
+ expect(pr.formatted_source_branch).to eq('foo:branch')
+ end
+ end
+
+ context 'for a regular pull request' do
+ it 'returns the source branch name' do
+ pr = described_class.new(
+ source_repository_owner: 'foo',
+ source_branch: 'branch',
+ target_branch: 'master',
+ source_repository_id: 1,
+ target_repository_id: 1
+ )
+
+ expect(pr.formatted_source_branch).to eq('branch')
+ end
+ end
+
+ context 'for a pull request with the same source and target branches' do
+ it 'returns a generated source branch name' do
+ pr = described_class.new(
+ iid: 1,
+ source_repository_owner: 'foo',
+ source_branch: 'branch',
+ target_branch: 'branch',
+ source_repository_id: 1,
+ target_repository_id: 1
+ )
+
+ expect(pr.formatted_source_branch).to eq('branch-1')
+ end
+ end
+ end
+
+ describe '#truncated_title' do
+ it 'truncates the title to 255 characters' do
+ object = described_class.new(title: 'm' * 300)
+
+ expect(object.truncated_title.length).to eq(255)
+ end
+
+ it 'does not truncate the title if it is shorter than 255 characters' do
+ object = described_class.new(title: 'foo')
+
+ expect(object.truncated_title).to eq('foo')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
new file mode 100644
index 00000000000..c296aa0a45b
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::ToHash do
+ describe '#to_hash' do
+ let(:user) { double(:user, attributes: { login: 'alice' }) }
+
+ let(:issue) do
+ double(
+ :issue,
+ attributes: { user: user, assignees: [user], number: 42 }
+ )
+ end
+
+ let(:issue_hash) { issue.to_hash }
+
+ before do
+ user.extend(described_class)
+ issue.extend(described_class)
+ end
+
+ it 'converts an object to a Hash' do
+ expect(issue_hash).to be_an_instance_of(Hash)
+ end
+
+ it 'converts nested objects to Hashes' do
+ expect(issue_hash[:user]).to eq({ login: 'alice' })
+ end
+
+ it 'converts Array values to Hashes' do
+ expect(issue_hash[:assignees]).to eq([{ login: 'alice' }])
+ end
+
+ it 'keeps values as-is if they do not respond to #to_hash' do
+ expect(issue_hash[:number]).to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/user_spec.rb b/spec/lib/gitlab/github_import/representation/user_spec.rb
new file mode 100644
index 00000000000..4e63e8ea568
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/user_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::User do
+ shared_examples 'a User' do
+ it 'returns an instance of User' do
+ expect(user).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned User' do
+ it 'includes the user ID' do
+ expect(user.id).to eq(42)
+ end
+
+ it 'includes the username' do
+ expect(user.login).to eq('alice')
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ it_behaves_like 'a User' do
+ let(:response) { double(:response, id: 42, login: 'alice') }
+ let(:user) { described_class.from_api_response(response) }
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a User' do
+ let(:hash) { { 'id' => 42, 'login' => 'alice' } }
+ let(:user) { described_class.from_json_hash(hash) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation_spec.rb b/spec/lib/gitlab/github_import/representation_spec.rb
new file mode 100644
index 00000000000..0b0610817b0
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation do
+ describe '.symbolize_hash' do
+ it 'returns a Hash with the keys as Symbols' do
+ hash = described_class.symbolize_hash('number' => 10)
+
+ expect(hash).to eq({ number: 10 })
+ end
+
+ it 'parses timestamp fields into Time instances' do
+ hash = described_class.symbolize_hash('created_at' => '2017-01-01 12:00')
+
+ expect(hash[:created_at]).to be_an_instance_of(Time)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/sequential_importer_spec.rb b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
new file mode 100644
index 00000000000..6089b0b751f
--- /dev/null
+++ b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::SequentialImporter do
+ describe '#execute' do
+ it 'imports a project in sequence' do
+ repository = double(:repository)
+ project = double(:project, id: 1, repository: repository)
+ importer = described_class.new(project, token: 'foo')
+
+ expect_any_instance_of(Gitlab::GithubImport::Importer::RepositoryImporter)
+ .to receive(:execute)
+
+ described_class::SEQUENTIAL_IMPORTERS.each do |klass|
+ instance = double(:instance)
+
+ expect(klass).to receive(:new)
+ .with(project, importer.client)
+ .and_return(instance)
+
+ expect(instance).to receive(:execute)
+ end
+
+ described_class::PARALLEL_IMPORTERS.each do |klass|
+ instance = double(:instance)
+
+ expect(klass).to receive(:new)
+ .with(project, importer.client, parallel: false)
+ .and_return(instance)
+
+ expect(instance).to receive(:execute)
+ end
+
+ expect(repository).to receive(:after_import)
+ expect(importer.execute).to eq(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
new file mode 100644
index 00000000000..29f4c00d9c7
--- /dev/null
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -0,0 +1,333 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:finder) { described_class.new(project, client) }
+
+ describe '#author_id_for' do
+ it 'returns the user ID for the author of an object' do
+ user = double(:user, id: 4, login: 'kittens')
+ note = double(:note, author: user)
+
+ expect(finder).to receive(:user_id_for).with(user).and_return(42)
+
+ expect(finder.author_id_for(note)).to eq([42, true])
+ end
+
+ it 'returns the ID of the project creator if no user ID could be found' do
+ user = double(:user, id: 4, login: 'kittens')
+ note = double(:note, author: user)
+
+ expect(finder).to receive(:user_id_for).with(user).and_return(nil)
+
+ expect(finder.author_id_for(note)).to eq([project.creator_id, false])
+ end
+
+ it 'returns the ID of the ghost user when the object has no user' do
+ note = double(:note, author: nil)
+
+ expect(finder.author_id_for(note)).to eq([User.ghost.id, true])
+ end
+
+ it 'returns the ID of the ghost user when the given object is nil' do
+ expect(finder.author_id_for(nil)).to eq([User.ghost.id, true])
+ end
+ end
+
+ describe '#assignee_id_for' do
+ it 'returns the user ID for the assignee of an issuable' do
+ user = double(:user, id: 4, login: 'kittens')
+ issue = double(:issue, assignee: user)
+
+ expect(finder).to receive(:user_id_for).with(user).and_return(42)
+ expect(finder.assignee_id_for(issue)).to eq(42)
+ end
+
+ it 'returns nil if the issuable does not have an assignee' do
+ issue = double(:issue, assignee: nil)
+
+ expect(finder).not_to receive(:user_id_for)
+ expect(finder.assignee_id_for(issue)).to be_nil
+ end
+ end
+
+ describe '#user_id_for' do
+ it 'returns the user ID for the given user' do
+ user = double(:user, id: 4, login: 'kittens')
+
+ expect(finder).to receive(:find).with(user.id, user.login).and_return(42)
+ expect(finder.user_id_for(user)).to eq(42)
+ end
+ end
+
+ describe '#find' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(finder).to receive(:email_for_github_username)
+ .and_return(user.email)
+ end
+
+ context 'without a cache' do
+ before do
+ allow(finder).to receive(:find_from_cache).and_return([false, nil])
+ expect(finder).to receive(:find_id_from_database).and_call_original
+ end
+
+ it 'finds a GitLab user for a GitHub user ID' do
+ user.identities.create!(provider: :github, extern_uid: 42)
+
+ expect(finder.find(42, user.username)).to eq(user.id)
+ end
+
+ it 'finds a GitLab user for a GitHub Email address' do
+ expect(finder.find(42, user.username)).to eq(user.id)
+ end
+ end
+
+ context 'with a cache' do
+ it 'returns the cached user ID' do
+ expect(finder).to receive(:find_from_cache).and_return([true, user.id])
+ expect(finder).not_to receive(:find_id_from_database)
+
+ expect(finder.find(42, user.username)).to eq(user.id)
+ end
+
+ it 'does not query the database if the cache key exists but is empty' do
+ expect(finder).to receive(:find_from_cache).and_return([true, nil])
+ expect(finder).not_to receive(:find_id_from_database)
+
+ expect(finder.find(42, user.username)).to be_nil
+ end
+ end
+ end
+
+ describe '#find_from_cache' do
+ it 'retrieves a GitLab user ID for a GitHub user ID' do
+ expect(finder)
+ .to receive(:cached_id_for_github_id)
+ .with(42)
+ .and_return([true, 4])
+
+ expect(finder.find_from_cache(42)).to eq([true, 4])
+ end
+
+ it 'retrieves a GitLab user ID for a GitHub Email address' do
+ email = 'kittens@example.com'
+
+ expect(finder)
+ .to receive(:cached_id_for_github_id)
+ .with(42)
+ .and_return([false, nil])
+
+ expect(finder)
+ .to receive(:cached_id_for_github_email)
+ .with(email)
+ .and_return([true, 4])
+
+ expect(finder.find_from_cache(42, email)).to eq([true, 4])
+ end
+
+ it 'does not query the cache for an Email address when none is given' do
+ expect(finder)
+ .to receive(:cached_id_for_github_id)
+ .with(42)
+ .and_return([false, nil])
+
+ expect(finder).not_to receive(:cached_id_for_github_id)
+
+ expect(finder.find_from_cache(42)).to eq([false])
+ end
+ end
+
+ describe '#find_id_from_database' do
+ let(:user) { create(:user) }
+
+ it 'returns the GitLab user ID for a GitHub user ID' do
+ user.identities.create!(provider: :github, extern_uid: 42)
+
+ expect(finder.find_id_from_database(42, user.email)).to eq(user.id)
+ end
+
+ it 'returns the GitLab user ID for a GitHub Email address' do
+ expect(finder.find_id_from_database(42, user.email)).to eq(user.id)
+ end
+ end
+
+ describe '#email_for_github_username' do
+ let(:email) { 'kittens@example.com' }
+
+ context 'when an Email address is cached' do
+ it 'reads the Email address from the cache' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:read)
+ .and_return(email)
+
+ expect(client).not_to receive(:user)
+ expect(finder.email_for_github_username('kittens')).to eq(email)
+ end
+ end
+
+ context 'when an Email address is not cached' do
+ let(:user) { double(:user, email: email) }
+
+ it 'retrieves the Email address from the GitHub API' do
+ expect(client).to receive(:user).with('kittens').and_return(user)
+ expect(finder.email_for_github_username('kittens')).to eq(email)
+ end
+
+ it 'caches the Email address when an Email address is available' do
+ expect(client).to receive(:user).with('kittens').and_return(user)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(an_instance_of(String), email)
+
+ finder.email_for_github_username('kittens')
+ end
+
+ it 'returns nil if the user does not exist' do
+ expect(client)
+ .to receive(:user)
+ .with('kittens')
+ .and_return(nil)
+
+ expect(Gitlab::GithubImport::Caching)
+ .not_to receive(:write)
+
+ expect(finder.email_for_github_username('kittens')).to be_nil
+ end
+ end
+ end
+
+ describe '#cached_id_for_github_id' do
+ let(:id) { 4 }
+
+ it 'reads a user ID from the cache' do
+ Gitlab::GithubImport::Caching
+ .write(described_class::ID_CACHE_KEY % id, 4)
+
+ expect(finder.cached_id_for_github_id(id)).to eq([true, 4])
+ end
+
+ it 'reads a non existing cache key' do
+ expect(finder.cached_id_for_github_id(id)).to eq([false, nil])
+ end
+ end
+
+ describe '#cached_id_for_github_email' do
+ let(:email) { 'kittens@example.com' }
+
+ it 'reads a user ID from the cache' do
+ Gitlab::GithubImport::Caching
+ .write(described_class::ID_FOR_EMAIL_CACHE_KEY % email, 4)
+
+ expect(finder.cached_id_for_github_email(email)).to eq([true, 4])
+ end
+
+ it 'reads a non existing cache key' do
+ expect(finder.cached_id_for_github_email(email)).to eq([false, nil])
+ end
+ end
+
+ describe '#id_for_github_id' do
+ let(:id) { 4 }
+
+ it 'queries and caches the user ID for a given GitHub ID' do
+ expect(finder).to receive(:query_id_for_github_id)
+ .with(id)
+ .and_return(42)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_CACHE_KEY % id, 42)
+
+ finder.id_for_github_id(id)
+ end
+
+ it 'caches a nil value if no ID could be found' do
+ expect(finder).to receive(:query_id_for_github_id)
+ .with(id)
+ .and_return(nil)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_CACHE_KEY % id, nil)
+
+ finder.id_for_github_id(id)
+ end
+ end
+
+ describe '#id_for_github_email' do
+ let(:email) { 'kittens@example.com' }
+
+ it 'queries and caches the user ID for a given Email address' do
+ expect(finder).to receive(:query_id_for_github_email)
+ .with(email)
+ .and_return(42)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_FOR_EMAIL_CACHE_KEY % email, 42)
+
+ finder.id_for_github_email(email)
+ end
+
+ it 'caches a nil value if no ID could be found' do
+ expect(finder).to receive(:query_id_for_github_email)
+ .with(email)
+ .and_return(nil)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_FOR_EMAIL_CACHE_KEY % email, nil)
+
+ finder.id_for_github_email(email)
+ end
+ end
+
+ describe '#query_id_for_github_id' do
+ it 'returns the ID of the user for the given GitHub user ID' do
+ user = create(:user)
+
+ user.identities.create!(provider: :github, extern_uid: '42')
+
+ expect(finder.query_id_for_github_id(42)).to eq(user.id)
+ end
+
+ it 'returns nil when no user ID could be found' do
+ expect(finder.query_id_for_github_id(42)).to be_nil
+ end
+ end
+
+ describe '#query_id_for_github_email' do
+ it 'returns the ID of the user for the given Email address' do
+ user = create(:user, email: 'kittens@example.com')
+
+ expect(finder.query_id_for_github_email(user.email)).to eq(user.id)
+ end
+
+ it 'returns nil if no user ID could be found' do
+ expect(finder.query_id_for_github_email('kittens@example.com')).to be_nil
+ end
+ end
+
+ describe '#read_id_from_cache' do
+ it 'reads an ID from the cache' do
+ Gitlab::GithubImport::Caching.write('foo', 10)
+
+ expect(finder.read_id_from_cache('foo')).to eq([true, 10])
+ end
+
+ it 'reads a cache key with an empty value' do
+ Gitlab::GithubImport::Caching.write('foo', nil)
+
+ expect(finder.read_id_from_cache('foo')).to eq([true, nil])
+ end
+
+ it 'reads a cache key that does not exist' do
+ expect(finder.read_id_from_cache('foo')).to eq([false, nil])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import_spec.rb b/spec/lib/gitlab/github_import_spec.rb
new file mode 100644
index 00000000000..51414800e8c
--- /dev/null
+++ b/spec/lib/gitlab/github_import_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport do
+ let(:project) { double(:project) }
+
+ describe '.new_client_for' do
+ it 'returns a new Client with a custom token' do
+ expect(described_class::Client)
+ .to receive(:new)
+ .with('123', parallel: true)
+
+ described_class.new_client_for(project, token: '123')
+ end
+
+ it 'returns a new Client with a token stored in the import data' do
+ import_data = double(:import_data, credentials: { user: '123' })
+
+ expect(project)
+ .to receive(:import_data)
+ .and_return(import_data)
+
+ expect(described_class::Client)
+ .to receive(:new)
+ .with('123', parallel: true)
+
+ described_class.new_client_for(project)
+ end
+ end
+
+ describe '.insert_and_return_id' do
+ let(:attributes) { { iid: 1, title: 'foo' } }
+ let(:project) { create(:project) }
+
+ context 'on PostgreSQL' do
+ it 'returns the ID returned by the query' do
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(Issue.table_name, [attributes], return_ids: true)
+ .and_return([10])
+
+ id = described_class.insert_and_return_id(attributes, project.issues)
+
+ expect(id).to eq(10)
+ end
+ end
+
+ context 'on MySQL' do
+ it 'uses a separate query to retrieve the ID' do
+ issue = create(:issue, project: project, iid: attributes[:iid])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(Issue.table_name, [attributes], return_ids: true)
+ .and_return([])
+
+ id = described_class.insert_and_return_id(attributes, project.issues)
+
+ expect(id).to eq(issue.id)
+ end
+ end
+ end
+
+ describe '.ghost_user_id', :clean_gitlab_redis_cache do
+ it 'returns the ID of the ghost user' do
+ expect(described_class.ghost_user_id).to eq(User.ghost.id)
+ end
+
+ it 'caches the ghost user ID' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .once
+ .and_call_original
+
+ 2.times do
+ described_class.ghost_user_id
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
index 30da56bec16..26529c4759d 100644
--- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -41,7 +41,8 @@ describe Gitlab::HookData::IssuableBuilder do
labels: [
[{ id: 1, title: 'foo' }],
[{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }]
- ]
+ ],
+ total_time_spent: [1, 2]
}
end
let(:data) { builder.build(user: user, changes: changes) }
@@ -53,6 +54,10 @@ describe Gitlab::HookData::IssuableBuilder do
labels: {
previous: [{ id: 1, title: 'foo' }],
current: [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }]
+ },
+ total_time_spent: {
+ previous: 1,
+ current: 2
}
}))
end
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
index 6c529cdd051..aeacd577d18 100644
--- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -11,7 +11,6 @@ describe Gitlab::HookData::IssueBuilder do
%w[
assignee_id
author_id
- branch_name
closed_at
confidential
created_at
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
index 92bf87bbad4..78475403f9e 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -26,7 +26,6 @@ describe Gitlab::HookData::MergeRequestBuilder do
merge_user_id
merge_when_pipeline_succeeds
milestone_id
- ref_fetched
source_branch
source_project_id
state
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 29baa70d5ae..bf1e97654e5 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -147,10 +147,6 @@ deploy_keys:
- user
- deploy_keys_projects
- projects
-cluster:
-- project
-- user
-- service
services:
- project
- service_hook
@@ -182,6 +178,8 @@ project:
- tags
- chat_services
- cluster
+- clusters
+- cluster_project
- creator
- group
- namespace
@@ -195,6 +193,7 @@ project:
- mattermost_slash_commands_service
- slack_slash_commands_service
- irker_service
+- packagist_service
- pivotaltracker_service
- prometheus_service
- hipchat_service
@@ -275,6 +274,7 @@ project:
- root_of_fork_network
- fork_network_member
- fork_network
+- custom_attributes
award_emoji:
- awardable
- user
@@ -286,3 +286,6 @@ timelogs:
- user
push_event_payload:
- event
+issue_assignees:
+- issue
+- assignee
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index dd0ce0dae41..cfb15ee7e8b 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -46,7 +46,7 @@ describe 'forked project import' do
end
it 'can access the MR' do
- project.merge_requests.first.ensure_ref_fetched
+ project.merge_requests.first.fetch_ref!
expect(project.repository.ref_exists?('refs/merge-requests/1/head')).to be_truthy
end
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 473ba40fae7..b793636c4d6 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::ImportExport::MergeRequestParser do
let(:parsed_merge_request) do
described_class.new(project,
- merge_request.diff_head_sha,
+ 'abcd',
merge_request,
merge_request.as_json).parse!
end
@@ -29,4 +29,14 @@ describe Gitlab::ImportExport::MergeRequestParser do
it 'has a target branch' do
expect(project.repository.branch_exists?(parsed_merge_request.target_branch)).to be true
end
+
+ it 'parses a MR that has no source branch' do
+ allow_any_instance_of(described_class).to receive(:branch_exists?).and_call_original
+ allow_any_instance_of(described_class).to receive(:branch_exists?).with(merge_request.source_branch).and_return(false)
+ allow_any_instance_of(described_class).to receive(:fork_merge_request?).and_return(true)
+ allow(Gitlab::GitalyClient).to receive(:migrate).and_call_original
+ allow(Gitlab::GitalyClient).to receive(:migrate).with(:fetch_ref).and_return([nil, 0])
+
+ expect(parsed_merge_request).to eq(merge_request)
+ end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 1115fb218d6..f7c90093bde 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -43,7 +43,7 @@
"issues": [
{
"id": 40,
- "title": "Voluptatem amet doloribus deleniti eos maxime repudiandae molestias.",
+ "title": "Voluptatem",
"assignee_id": 1,
"author_id": 22,
"project_id": 5,
@@ -60,6 +60,12 @@
"due_date": null,
"moved_to_id": null,
"test_ee_field": "test",
+ "issue_assignees": [
+ {
+ "user_id": 1,
+ "issue_id": 1
+ }
+ ],
"milestone": {
"id": 1,
"title": "test milestone",
@@ -7402,5 +7408,23 @@
"snippets_access_level": 20,
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
- }
+ },
+ "custom_attributes": [
+ {
+ "id": 1,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "project_id": 5,
+ "key": "foo",
+ "value": "foo"
+ },
+ {
+ "id": 2,
+ "created_at": "2017-10-19T15:37:21.904Z",
+ "updated_at": "2017-10-19T15:37:21.904Z",
+ "project_id": 5,
+ "key": "bar",
+ "value": "bar"
+ }
+ ]
}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 4301eee17dc..c2bda6f8821 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -63,6 +63,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
+ it 'has issue assignees' do
+ expect(Issue.where(title: 'Voluptatem').first.issue_assignees).not_to be_empty
+ end
+
it 'contains the merge access levels on a protected branch' do
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end
@@ -129,6 +133,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(@project.project_feature).not_to be_nil
end
+ it 'has custom attributes' do
+ expect(@project.custom_attributes.count).to eq(2)
+ end
+
it 'restores the correct service' do
expect(CustomIssueTrackerService.first).not_to be_nil
end
@@ -147,7 +155,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
it 'has no source if source/target differ' do
- expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
+ expect(MergeRequest.find_by_title('MR2').source_project_id).to be_nil
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index d9b86e1bf34..ee173afbd50 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -77,6 +77,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['issues'].first['notes']).not_to be_empty
end
+ it 'has issue assignees' do
+ expect(saved_project_json['issues'].first['issue_assignees']).not_to be_empty
+ end
+
it 'has author on issue comments' do
expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty
end
@@ -164,6 +168,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE)
end
+ it 'has custom attributes' do
+ expect(saved_project_json['custom_attributes'].count).to eq(2)
+ end
+
it 'does not complain about non UTF-8 characters in MR diffs' do
ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
@@ -275,6 +283,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:event, :created, target: milestone, project: project, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
+ create(:project_custom_attribute, project: project)
+ create(:project_custom_attribute, project: project)
+
project
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 121c0ed04ed..4e36af18aa7 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -313,30 +313,47 @@ Ci::PipelineSchedule:
- deleted_at
- created_at
- updated_at
-Gcp::Cluster:
+Clusters::Cluster:
- id
-- project_id
- user_id
-- service_id
- enabled
+- name
+- provider_type
+- platform_type
+- created_at
+- updated_at
+Clusters::Project:
+- id
+- project_id
+- cluster_id
+- created_at
+- updated_at
+Clusters::Providers::Gcp:
+- id
+- cluster_id
- status
- status_reason
-- project_namespace
+- gcp_project_id
+- zone
+- num_nodes
+- machine_type
+- operation_id
- endpoint
+- encrypted_access_token
+- encrypted_access_token_iv
+- created_at
+- updated_at
+Clusters::Platforms::Kubernetes:
+- id
+- cluster_id
+- api_url
- ca_cert
-- encrypted_kubernetes_token
-- encrypted_kubernetes_token_iv
+- namespace
- username
- encrypted_password
- encrypted_password_iv
-- gcp_project_id
-- gcp_cluster_zone
-- gcp_cluster_name
-- gcp_cluster_size
-- gcp_machine_type
-- gcp_operation_id
-- encrypted_gcp_token
-- encrypted_gcp_token_iv
+- encrypted_token
+- encrypted_token_iv
- created_at
- updated_at
DeployKey:
@@ -506,3 +523,13 @@ ProjectAutoDevops:
- project_id
- created_at
- updated_at
+IssueAssignee:
+- user_id
+- issue_id
+ProjectCustomAttribute:
+- id
+- created_at
+- updated_at
+- project_id
+- key
+- value
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index c5725f47453..f2fa315e3ec 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -56,14 +56,14 @@ describe Gitlab::ImportSources do
describe '.importer' do
import_sources = {
- 'github' => Github::Import,
+ 'github' => Gitlab::GithubImport::ParallelImporter,
'bitbucket' => Gitlab::BitbucketImport::Importer,
'gitlab' => Gitlab::GitlabImport::Importer,
'google_code' => Gitlab::GoogleCodeImport::Importer,
'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
- 'gitea' => Gitlab::GithubImport::Importer
+ 'gitea' => Gitlab::LegacyGithubImport::Importer
}
import_sources.each do |name, klass|
diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb
index 2455969a183..42635a68ee1 100644
--- a/spec/lib/gitlab/issuable_metadata_spec.rb
+++ b/spec/lib/gitlab/issuable_metadata_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Gitlab::IssuableMetadata do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
subject { Class.new { include Gitlab::IssuableMetadata }.new }
@@ -10,6 +10,10 @@ describe Gitlab::IssuableMetadata do
expect(subject.issuable_meta_data(Issue.none, 'Issue')).to eq({})
end
+ it 'raises an error when given a collection with no limit' do
+ expect { subject.issuable_meta_data(Issue.all, 'Issue') }.to raise_error(/must have a limit/)
+ end
+
context 'issues' do
let!(:issue) { create(:issue, author: user, project: project) }
let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) }
@@ -19,7 +23,7 @@ describe Gitlab::IssuableMetadata do
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do
- data = subject.issuable_meta_data(Issue.all, 'Issue')
+ data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue')
expect(data.count).to eq(2)
expect(data[issue.id].upvotes).to eq(1)
@@ -42,7 +46,7 @@ describe Gitlab::IssuableMetadata do
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
it 'aggregates stats on merge requests' do
- data = subject.issuable_meta_data(MergeRequest.all, 'MergeRequest')
+ data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest')
expect(data.count).to eq(2)
expect(data[merge_request.id].upvotes).to eq(1)
diff --git a/spec/lib/gitlab/kubernetes/helm_spec.rb b/spec/lib/gitlab/kubernetes/helm_spec.rb
new file mode 100644
index 00000000000..15f99b0401f
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm_spec.rb
@@ -0,0 +1,100 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Helm do
+ let(:client) { double('kubernetes client') }
+ let(:helm) { described_class.new(client) }
+ let(:namespace) { Gitlab::Kubernetes::Namespace.new(described_class::NAMESPACE, client) }
+ let(:install_helm) { true }
+ let(:chart) { 'stable/a_chart' }
+ let(:application_name) { 'app_name' }
+ let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm, chart) }
+ subject { helm }
+
+ before do
+ allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client).and_return(namespace)
+ end
+
+ describe '#initialize' do
+ it 'creates a namespace object' do
+ expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client)
+
+ subject
+ end
+ end
+
+ describe '#install' do
+ before do
+ allow(client).to receive(:create_pod).and_return(nil)
+ allow(namespace).to receive(:ensure_exists!).once
+ end
+
+ it 'ensures the namespace exists before creating the POD' do
+ expect(namespace).to receive(:ensure_exists!).once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.install(command)
+ end
+ end
+
+ describe '#installation_status' do
+ let(:phase) { Gitlab::Kubernetes::Pod::RUNNING }
+ let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation
+
+ it 'fetches POD phase from kubernetes cluster' do
+ expect(client).to receive(:get_pod).with(command.pod_name, described_class::NAMESPACE).once.and_return(pod)
+
+ expect(subject.installation_status(command.pod_name)).to eq(phase)
+ end
+ end
+
+ describe '#installation_log' do
+ let(:log) { 'some output' }
+ let(:response) { RestClient::Response.new(log) }
+
+ it 'fetches POD phase from kubernetes cluster' do
+ expect(client).to receive(:get_pod_log).with(command.pod_name, described_class::NAMESPACE).once.and_return(response)
+
+ expect(subject.installation_log(command.pod_name)).to eq(log)
+ end
+ end
+
+ describe '#delete_installation_pod!' do
+ it 'deletes the POD from kubernetes cluster' do
+ expect(client).to receive(:delete_pod).with(command.pod_name, described_class::NAMESPACE).once
+
+ subject.delete_installation_pod!(command.pod_name)
+ end
+ end
+
+ describe '#helm_init_command' do
+ subject { helm.send(:helm_init_command, command) }
+
+ context 'when command.install_helm is true' do
+ let(:install_helm) { true }
+
+ it { is_expected.to eq('helm init >/dev/null') }
+ end
+
+ context 'when command.install_helm is false' do
+ let(:install_helm) { false }
+
+ it { is_expected.to eq('helm init --client-only >/dev/null') }
+ end
+ end
+
+ describe '#helm_install_command' do
+ subject { helm.send(:helm_install_command, command) }
+
+ context 'when command.chart is nil' do
+ let(:chart) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when command.chart is set' do
+ let(:chart) { 'stable/a_chart' }
+
+ it { is_expected.to eq("helm install #{chart} --name #{application_name} --namespace #{namespace.name} >/dev/null")}
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb
new file mode 100644
index 00000000000..b3c987f9344
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Namespace do
+ let(:name) { 'a_namespace' }
+ let(:client) { double('kubernetes client') }
+ subject { described_class.new(name, client) }
+
+ it { expect(subject.name).to eq(name) }
+
+ describe '#exists?' do
+ context 'when namespace do not exits' do
+ let(:exception) { ::KubeException.new(404, "namespace #{name} not found", nil) }
+
+ it 'returns false' do
+ expect(client).to receive(:get_namespace).with(name).once.and_raise(exception)
+
+ expect(subject.exists?).to be_falsey
+ end
+ end
+
+ context 'when namespace exits' do
+ let(:namespace) { ::Kubeclient::Resource.new(kind: 'Namespace', metadata: { name: name }) } # partial representation
+
+ it 'returns true' do
+ expect(client).to receive(:get_namespace).with(name).once.and_return(namespace)
+
+ expect(subject.exists?).to be_truthy
+ end
+ end
+
+ context 'when cluster cannot be reached' do
+ let(:exception) { Errno::ECONNREFUSED.new }
+
+ it 'raises exception' do
+ expect(client).to receive(:get_namespace).with(name).once.and_raise(exception)
+
+ expect { subject.exists? }.to raise_error(exception)
+ end
+ end
+ end
+
+ describe '#create!' do
+ it 'creates a namespace' do
+ matcher = have_attributes(metadata: have_attributes(name: name))
+ expect(client).to receive(:create_namespace).with(matcher).once
+
+ expect { subject.create! }.not_to raise_error
+ end
+ end
+
+ describe '#ensure_exists!' do
+ it 'checks for existing namespace before creating' do
+ expect(subject).to receive(:exists?).once.ordered.and_return(false)
+ expect(subject).to receive(:create!).once.ordered
+
+ subject.ensure_exists!
+ end
+
+ it 'do not re-create an existing namespace' do
+ expect(subject).to receive(:exists?).once.and_return(true)
+ expect(subject).not_to receive(:create!)
+
+ subject.ensure_exists!
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb
index 01b6282af0c..9d57a46c12b 100644
--- a/spec/lib/gitlab/ldap/authentication_spec.rb
+++ b/spec/lib/gitlab/ldap/authentication_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Gitlab::LDAP::Authentication do
- let(:user) { create(:omniauth_user, extern_uid: dn) }
- let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
+ let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' }
+ let(:user) { create(:omniauth_user, extern_uid: Gitlab::LDAP::Person.normalize_dn(dn)) }
let(:login) { 'john' }
let(:password) { 'password' }
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 9a4705d1cee..260df6e4dae 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -44,23 +44,25 @@ describe Gitlab::LDAP::User do
end
describe '.find_by_uid_and_provider' do
+ let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' }
+
it 'retrieves the correct user' do
special_info = {
name: 'John Åström',
email: 'john@example.com',
nickname: 'jastrom'
}
- special_hash = OmniAuth::AuthHash.new(uid: 'CN=John Åström,CN=Users,DC=Example,DC=com', provider: 'ldapmain', info: special_info)
+ special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info)
special_chars_user = described_class.new(special_hash)
user = special_chars_user.save
- expect(described_class.find_by_uid_and_provider(special_hash.uid, special_hash.provider)).to eq user
+ expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user
end
end
describe 'find or create' do
it "finds the user if already existing" do
- create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
+ create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
expect { ldap_user.save }.not_to change { User.count }
end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb
index 426b43f8b51..48655851140 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::BranchFormatter do
+describe Gitlab::LegacyGithubImport::BranchFormatter do
let(:project) { create(:project, :repository) }
let(:commit) { create(:commit, project: project) }
let(:repo) { double }
diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb
new file mode 100644
index 00000000000..80b767abce0
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::Client do
+ let(:token) { '123456' }
+ let(:github_provider) { Settingslogic.new('app_id' => 'asd123', 'app_secret' => 'asd123', 'name' => 'github', 'args' => { 'client_options' => {} }) }
+
+ subject(:client) { described_class.new(token) }
+
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([github_provider])
+ end
+
+ it 'convert OAuth2 client options to symbols' do
+ client.client.options.keys.each do |key|
+ expect(key).to be_kind_of(Symbol)
+ end
+ end
+
+ it 'does not crash (e.g. Settingslogic::MissingSetting) when verify_ssl config is not present' do
+ expect { client.api }.not_to raise_error
+ end
+
+ context 'when config is missing' do
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ end
+
+ it 'is still possible to get an Octokit client' do
+ expect { client.api }.not_to raise_error
+ end
+
+ it 'is not be possible to get an OAuth2 client' do
+ expect { client.client }.to raise_error(Projects::ImportService::Error)
+ end
+ end
+
+ context 'allow SSL verification to be configurable on API' do
+ before do
+ github_provider['verify_ssl'] = false
+ end
+
+ it 'uses supplied value' do
+ expect(client.client.options[:connection_opts][:ssl]).to eq({ verify: false })
+ expect(client.api.connection_options[:ssl]).to eq({ verify: false })
+ end
+ end
+
+ describe '#api_endpoint' do
+ context 'when provider does not specity an API endpoint' do
+ it 'uses GitHub root API endpoint' do
+ expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ end
+ end
+
+ context 'when provider specify a custom API endpoint' do
+ before do
+ github_provider['args']['client_options']['site'] = 'https://github.company.com/'
+ end
+
+ it 'uses the custom API endpoint' do
+ expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options)
+ expect(client.api.api_endpoint).to eq 'https://github.company.com/'
+ end
+ end
+
+ context 'when given a host' do
+ subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') }
+
+ it 'builds a endpoint with the given host and the default API version' do
+ expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ end
+ end
+
+ context 'when given an API version' do
+ subject(:client) { described_class.new(token, api_version: 'v3') }
+
+ it 'does not use the API version without a host' do
+ expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ end
+ end
+
+ context 'when given a host and version' do
+ subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') }
+
+ it 'builds a endpoint with the given options' do
+ expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ end
+ end
+ end
+
+ it 'does not raise error when rate limit is disabled' do
+ stub_request(:get, /api.github.com/)
+ allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+
+ expect { client.issues {} }.not_to raise_error
+ end
+end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb
index 035ac8c7c1f..413654e108c 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::CommentFormatter do
+describe Gitlab::LegacyGithubImport::CommentFormatter do
let(:client) { double }
let(:project) { create(:project) }
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
index d570f34985b..20514486727 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-describe Gitlab::GithubImport::Importer do
- shared_examples 'Gitlab::GithubImport::Importer#execute' do
+describe Gitlab::LegacyGithubImport::Importer do
+ shared_examples 'Gitlab::LegacyGithubImport::Importer#execute' do
let(:expected_not_called) { [] }
before do
@@ -35,7 +35,7 @@ describe Gitlab::GithubImport::Importer do
end
end
- shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do
+ shared_examples 'Gitlab::LegacyGithubImport::Importer#execute an error occurs' do
before do
allow(project).to receive(:import_data).and_return(double.as_null_object)
@@ -178,7 +178,7 @@ describe Gitlab::GithubImport::Importer do
end
end
- shared_examples 'Gitlab::GithubImport unit-testing' do
+ shared_examples 'Gitlab::LegacyGithubImport unit-testing' do
describe '#clean_up_restored_branches' do
subject { described_class.new(project) }
@@ -188,7 +188,7 @@ describe Gitlab::GithubImport::Importer do
end
context 'when pull request stills open' do
- let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, pull_request) }
+ let(:gh_pull_request) { Gitlab::LegacyGithubImport::PullRequestFormatter.new(project, pull_request) }
it 'does not remove branches' do
expect(subject).not_to receive(:remove_branch)
@@ -197,7 +197,7 @@ describe Gitlab::GithubImport::Importer do
end
context 'when pull request is closed' do
- let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, closed_pull_request) }
+ let(:gh_pull_request) { Gitlab::LegacyGithubImport::PullRequestFormatter.new(project, closed_pull_request) }
it 'does remove branches' do
expect(subject).to receive(:remove_branch).at_least(2).times
@@ -262,14 +262,14 @@ describe Gitlab::GithubImport::Importer do
let(:repo_root) { 'https://github.com' }
subject { described_class.new(project) }
- it_behaves_like 'Gitlab::GithubImport::Importer#execute'
- it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
- it_behaves_like 'Gitlab::GithubImport unit-testing'
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute'
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute an error occurs'
+ it_behaves_like 'Gitlab::LegacyGithubImport unit-testing'
describe '#client' do
it 'instantiates a Client' do
allow(project).to receive(:import_data).and_return(double(credentials: credentials))
- expect(Gitlab::GithubImport::Client).to receive(:new).with(
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(
credentials[:user],
{}
)
@@ -288,16 +288,16 @@ describe Gitlab::GithubImport::Importer do
project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
end
- it_behaves_like 'Gitlab::GithubImport::Importer#execute' do
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute' do
let(:expected_not_called) { [:import_releases] }
end
- it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
- it_behaves_like 'Gitlab::GithubImport unit-testing'
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute an error occurs'
+ it_behaves_like 'Gitlab::LegacyGithubImport unit-testing'
describe '#client' do
it 'instantiates a Client' do
allow(project).to receive(:import_data).and_return(double(credentials: credentials))
- expect(Gitlab::GithubImport::Client).to receive(:new).with(
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(
credentials[:user],
{ host: "#{repo_root}:443/foo", api_version: 'v1' }
)
diff --git a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb
index 05294d227bd..3b5d8945344 100644
--- a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::IssuableFormatter do
+describe Gitlab::LegacyGithubImport::IssuableFormatter do
let(:raw_data) do
double(number: 42)
end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb
index 0fc56d92aa6..1a4d5dbfb70 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::IssueFormatter do
+describe Gitlab::LegacyGithubImport::IssueFormatter do
let(:client) { double }
let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
@@ -30,7 +30,7 @@ describe Gitlab::GithubImport::IssueFormatter do
allow(client).to receive(:user).and_return(octocat)
end
- shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
+ shared_examples 'Gitlab::LegacyGithubImport::IssueFormatter#attributes' do
context 'when issue is open' do
let(:raw_data) { double(base_data.merge(state: 'open')) }
@@ -135,7 +135,7 @@ describe Gitlab::GithubImport::IssueFormatter do
end
end
- shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do
+ shared_examples 'Gitlab::LegacyGithubImport::IssueFormatter#number' do
let(:raw_data) { double(base_data.merge(number: 1347)) }
it 'returns issue number' do
@@ -144,8 +144,8 @@ describe Gitlab::GithubImport::IssueFormatter do
end
context 'when importing a GitHub project' do
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#number'
end
context 'when importing a Gitea project' do
@@ -153,8 +153,8 @@ describe Gitlab::GithubImport::IssueFormatter do
project.update(import_type: 'gitea')
end
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#number'
end
describe '#has_comments?' do
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb
index 83fdd2cc415..0d1d04f1bf6 100644
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::LabelFormatter do
+describe Gitlab::LegacyGithubImport::LabelFormatter do
let(:project) { create(:project) }
let(:raw) { double(name: 'improvements', color: 'e6e6e6') }
diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb
index 683fa51b78e..1db4bbb568c 100644
--- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::MilestoneFormatter do
+describe Gitlab::LegacyGithubImport::MilestoneFormatter do
let(:project) { create(:project) }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -19,7 +19,7 @@ describe Gitlab::GithubImport::MilestoneFormatter do
subject(:formatter) { described_class.new(project, raw_data) }
- shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do
+ shared_examples 'Gitlab::LegacyGithubImport::MilestoneFormatter#attributes' do
let(:data) { base_data.merge(iid_attr => 1347) }
context 'when milestone is open' do
@@ -82,7 +82,7 @@ describe Gitlab::GithubImport::MilestoneFormatter do
end
context 'when importing a GitHub project' do
- it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::MilestoneFormatter#attributes'
end
context 'when importing a Gitea project' do
@@ -91,6 +91,6 @@ describe Gitlab::GithubImport::MilestoneFormatter do
project.update(import_type: 'gitea')
end
- it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::MilestoneFormatter#attributes'
end
end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
index 948e7469a18..737c9a624e0 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::ProjectCreator do
+describe Gitlab::LegacyGithubImport::ProjectCreator do
let(:user) { create(:user) }
let(:namespace) { create(:group, owner: user) }
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb
index 2e42f6239b7..267a41e3f32 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::PullRequestFormatter do
+describe Gitlab::LegacyGithubImport::PullRequestFormatter do
let(:client) { double }
let(:project) { create(:project, :repository) }
let(:source_sha) { create(:commit, project: project).id }
@@ -44,7 +44,7 @@ describe Gitlab::GithubImport::PullRequestFormatter do
allow(client).to receive(:user).and_return(octocat)
end
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#attributes' do
context 'when pull request is open' do
let(:raw_data) { double(base_data.merge(state: 'open')) }
@@ -189,7 +189,7 @@ describe Gitlab::GithubImport::PullRequestFormatter do
end
end
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#number' do
let(:raw_data) { double(base_data) }
it 'returns pull request number' do
@@ -197,7 +197,7 @@ describe Gitlab::GithubImport::PullRequestFormatter do
end
end
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#source_branch_name' do
context 'when source branch exists' do
let(:raw_data) { double(base_data) }
@@ -231,7 +231,7 @@ describe Gitlab::GithubImport::PullRequestFormatter do
end
end
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#target_branch_name' do
context 'when target branch exists' do
let(:raw_data) { double(base_data) }
@@ -250,10 +250,10 @@ describe Gitlab::GithubImport::PullRequestFormatter do
end
context 'when importing a GitHub project' do
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#number'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#source_branch_name'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#target_branch_name'
end
context 'when importing a Gitea project' do
@@ -261,10 +261,10 @@ describe Gitlab::GithubImport::PullRequestFormatter do
project.update(import_type: 'gitea')
end
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#number'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#source_branch_name'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#target_branch_name'
end
describe '#valid?' do
diff --git a/spec/lib/gitlab/github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
index 926bf725d6a..082e3b36dd0 100644
--- a/spec/lib/gitlab/github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::ReleaseFormatter do
+describe Gitlab::LegacyGithubImport::ReleaseFormatter do
let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
diff --git a/spec/lib/gitlab/github_import/user_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb
index 98e3a7c28b9..3cd096eb0ad 100644
--- a/spec/lib/gitlab/github_import/user_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::UserFormatter do
+describe Gitlab::LegacyGithubImport::UserFormatter do
let(:client) { double }
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb
index 2662cc20b32..7723533aee2 100644
--- a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::WikiFormatter do
+describe Gitlab::LegacyGithubImport::WikiFormatter do
let(:project) do
create(:project,
namespace: create(:namespace, path: 'gitlabhq'),
diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb
new file mode 100644
index 00000000000..17445fe6de5
--- /dev/null
+++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::BackgroundTransaction do
+ let(:test_worker_class) { double(:class, name: 'TestWorker') }
+
+ subject { described_class.new(test_worker_class) }
+
+ describe '#action' do
+ it 'returns transaction action name' do
+ expect(subject.action).to eq('TestWorker#perform')
+ end
+ end
+
+ describe '#label' do
+ it 'returns labels based on class name' do
+ expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 4b19ee19103..977bc250049 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::Metrics::Instrumentation do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
before do
@dummy = Class.new do
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index a247f03b2da..f1e9e414e0d 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::Metrics::MethodCall do
- let(:method_call) { described_class.new('Foo#bar', 'foo') }
+ let(:transaction) { double(:transaction, labels: {}) }
+ let(:method_call) { described_class.new('Foo#bar', :Foo, '#bar', transaction) }
describe '#measure' do
it 'measures the performance of the supplied block' do
@@ -11,6 +12,18 @@ describe Gitlab::Metrics::MethodCall do
expect(method_call.cpu_time).to be_a_kind_of(Numeric)
expect(method_call.call_count).to eq(1)
end
+
+ it 'observes the performance of the supplied block' do
+ expect(described_class.call_real_duration_histogram)
+ .to receive(:observe)
+ .with({ module: :Foo, method: '#bar' }, be_a_kind_of(Numeric))
+
+ expect(described_class.call_cpu_duration_histogram)
+ .to receive(:observe)
+ .with({ module: :Foo, method: '#bar' }, be_a_kind_of(Numeric))
+
+ method_call.measure { 'foo' }
+ end
end
describe '#to_metric' do
@@ -19,7 +32,7 @@ describe Gitlab::Metrics::MethodCall do
metric = method_call.to_metric
expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
- expect(metric.series).to eq('foo')
+ expect(metric.series).to eq('rails_method_calls')
expect(metric.values[:duration]).to be_a_kind_of(Numeric)
expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index ec415f2bd85..b84387204ee 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -18,34 +18,6 @@ describe Gitlab::Metrics::RackMiddleware do
expect(middleware.call(env)).to eq('yay')
end
- it 'tags a transaction with the name and action of a controller' do
- klass = double(:klass, name: 'TestController', content_type: 'text/html')
- controller = double(:controller, class: klass, action_name: 'show')
-
- env['action_controller.instance'] = controller
-
- allow(app).to receive(:call).with(env)
-
- expect(middleware).to receive(:tag_controller)
- .with(an_instance_of(Gitlab::Metrics::Transaction), env)
-
- middleware.call(env)
- end
-
- it 'tags a transaction with the method and path of the route in the grape endpoint' do
- route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
- endpoint = double(:endpoint, route: route)
-
- env['api.endpoint'] = endpoint
-
- allow(app).to receive(:call).with(env)
-
- expect(middleware).to receive(:tag_endpoint)
- .with(an_instance_of(Gitlab::Metrics::Transaction), env)
-
- middleware.call(env)
- end
-
it 'tracks any raised exceptions' do
expect(app).to receive(:call).with(env).and_raise(RuntimeError)
@@ -60,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do
let(:transaction) { middleware.transaction_from_env(env) }
it 'returns a Transaction' do
- expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
+ expect(transaction).to be_an_instance_of(Gitlab::Metrics::WebTransaction)
end
it 'stores the request method and URI in the transaction as values' do
@@ -84,58 +56,4 @@ describe Gitlab::Metrics::RackMiddleware do
end
end
end
-
- describe '#tag_controller' do
- let(:transaction) { middleware.transaction_from_env(env) }
- let(:content_type) { 'text/html' }
-
- before do
- klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
-
- env['action_controller.instance'] = controller
- end
-
- it 'tags a transaction with the name and action of a controller' do
- middleware.tag_controller(transaction, env)
-
- expect(transaction.action).to eq('TestController#show')
- end
-
- context 'when the response content type is not :html' do
- let(:content_type) { 'application/json' }
-
- it 'appends the mime type to the transaction action' do
- middleware.tag_controller(transaction, env)
-
- expect(transaction.action).to eq('TestController#show.json')
- end
- end
- end
-
- describe '#tag_endpoint' do
- let(:transaction) { middleware.transaction_from_env(env) }
-
- it 'tags a transaction with the method and path of the route in the grape endpount' do
- route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
- endpoint = double(:endpoint, route: route)
-
- env['api.endpoint'] = endpoint
-
- middleware.tag_endpoint(transaction, env)
-
- expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
- end
-
- it 'does not tag a transaction if route infos are missing' do
- endpoint = double(:endpoint)
- allow(endpoint).to receive(:route).and_raise
-
- env['api.endpoint'] = endpoint
-
- middleware.tag_endpoint(transaction, env)
-
- expect(transaction.action).to be_nil
- end
- end
end
diff --git a/spec/lib/gitlab/metrics/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
index 999a9536d82..667e4747897 100644
--- a/spec/lib/gitlab/metrics/influx_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Metrics::InfluxSampler do
+describe Gitlab::Metrics::Samplers::InfluxSampler do
let(:sampler) { described_class.new(5) }
after do
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
new file mode 100644
index 00000000000..53699327da1
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::RubySampler do
+ let(:sampler) { described_class.new(5) }
+
+ after do
+ Allocations.stop if Gitlab::Metrics.mri?
+ end
+
+ describe '#sample' do
+ it 'samples various statistics' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ expect(sampler).to receive(:sample_objects)
+ expect(sampler).to receive(:sample_gc)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the memory usage' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ .and_return(9000)
+
+ expect(sampler.metrics[:memory_usage]).to receive(:set)
+ .with({}, 9000)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the amount of open file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ .and_return(4)
+
+ expect(sampler.metrics[:file_descriptors]).to receive(:set)
+ .with({}, 4)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'clears any GC profiles' do
+ expect(GC::Profiler).to receive(:clear)
+
+ sampler.sample
+ end
+ end
+
+ describe '#sample_gc' do
+ it 'adds a metric containing garbage collection time statistics' do
+ expect(GC::Profiler).to receive(:total_time).and_return(0.24)
+
+ expect(sampler.metrics[:total_time]).to receive(:set)
+ .with({}, 240)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing garbage collection statistics' do
+ GC.stat.keys.each do |key|
+ expect(sampler.metrics[key]).to receive(:set).with({}, anything).and_call_original
+ end
+
+ sampler.sample
+ end
+ end
+
+ if Gitlab::Metrics.mri?
+ describe '#sample_objects' do
+ it 'adds a metric containing the amount of allocated objects' do
+ expect(sampler.metrics[:objects_total]).to receive(:set)
+ .with(include(class: anything), be > 0)
+ .at_least(:once)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'ignores classes without a name' do
+ expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
+
+ expect(sampler.metrics[:objects_total]).not_to receive(:set)
+ .with(include(class: 'object_counts'), anything)
+
+ sampler.sample
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index dc0d1f2e940..771b633a2b9 100644
--- a/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Metrics::UnicornSampler do
+describe Gitlab::Metrics::Samplers::UnicornSampler do
subject { described_class.new(1.second) }
describe '#sample' do
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index b576d7173f5..ae1d8b47fe9 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -8,8 +8,8 @@ describe Gitlab::Metrics::SidekiqMiddleware do
it 'tracks the transaction' do
worker = double(:worker, class: double(:class, name: 'TestWorker'))
- expect(Gitlab::Metrics::Transaction).to receive(:new)
- .with('TestWorker#perform')
+ expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
+ .with(worker.class)
.and_call_original
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
@@ -23,8 +23,8 @@ describe Gitlab::Metrics::SidekiqMiddleware do
it 'tracks the transaction (for messages without `enqueued_at`)' do
worker = double(:worker, class: double(:class, name: 'TestWorker'))
- expect(Gitlab::Metrics::Transaction).to receive(:new)
- .with('TestWorker#perform')
+ expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
+ .with(worker.class)
.and_call_original
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
index e7b595405a8..eca75a4fac1 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActionView do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
@@ -29,5 +30,13 @@ describe Gitlab::Metrics::Subscribers::ActionView do
subscriber.render_template(event)
end
+
+ it 'observes view rendering time' do
+ expect(subscriber.send(:metric_view_rendering_duration_seconds))
+ .to receive(:observe)
+ .with({ view: 'app/views/x.html.haml' }, 2.1)
+
+ subscriber.render_template(event)
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index ce6587e993f..9b3698fb4a8 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActiveRecord do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
let(:event) do
- double(:event, duration: 0.2,
+ double(:event, duration: 2,
payload: { sql: 'SELECT * FROM users WHERE id = 10' })
end
@@ -20,16 +21,24 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
describe 'with a current transaction' do
+ it 'observes sql_duration metric' do
+ expect(subscriber).to receive(:current_transaction)
+ .at_least(:once)
+ .and_return(transaction)
+ expect(subscriber.send(:metric_sql_duration_seconds)).to receive(:observe).with({}, 0.002)
+ subscriber.sql(event)
+ end
+
it 'increments the :sql_duration value' do
expect(subscriber).to receive(:current_transaction)
.at_least(:once)
.and_return(transaction)
expect(transaction).to receive(:increment)
- .with(:sql_duration, 0.2)
+ .with(:sql_duration, 2, false)
expect(transaction).to receive(:increment)
- .with(:sql_count, 1)
+ .with(:sql_count, 1, false)
subscriber.sql(event)
end
diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
index f04dc8dcc02..58e28592cf9 100644
--- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
@@ -1,15 +1,16 @@
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::RailsCache do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
let(:event) { double(:event, duration: 15.2) }
describe '#cache_read' do
it 'increments the cache_read duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_read, event.duration)
+ expect(subscriber).to receive(:observe)
+ .with(:read, event.duration)
subscriber.cache_read(event)
end
@@ -17,7 +18,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
context 'with hit event' do
@@ -25,9 +26,9 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'increments the cache_read_hit count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_hit_count, 1)
+ .with(:cache_read_hit_count, 1, false)
expect(transaction).to receive(:increment)
- .with(any_args).at_least(1) # Other calls
+ .with(any_args).at_least(1) # Other calls
subscriber.cache_read(event)
end
@@ -37,7 +38,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'does not increment cache read miss' do
expect(transaction).not_to receive(:increment)
- .with(:cache_read_hit_count, 1)
+ .with(:cache_read_hit_count, 1)
subscriber.cache_read(event)
end
@@ -49,9 +50,15 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'increments the cache_read_miss count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_miss_count, 1)
+ .with(:cache_read_miss_count, 1, false)
expect(transaction).to receive(:increment)
- .with(any_args).at_least(1) # Other calls
+ .with(any_args).at_least(1) # Other calls
+
+ subscriber.cache_read(event)
+ end
+
+ it 'increments the cache_read_miss total' do
+ expect(subscriber.send(:metric_cache_misses_total)).to receive(:increment).with({})
subscriber.cache_read(event)
end
@@ -61,7 +68,13 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'does not increment cache read miss' do
expect(transaction).not_to receive(:increment)
- .with(:cache_read_miss_count, 1)
+ .with(:cache_read_miss_count, 1)
+
+ subscriber.cache_read(event)
+ end
+
+ it 'does not increment cache_read_miss total' do
+ expect(subscriber.send(:metric_cache_misses_total)).not_to receive(:increment).with({})
subscriber.cache_read(event)
end
@@ -71,27 +84,27 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
end
describe '#cache_write' do
- it 'increments the cache_write duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_write, event.duration)
+ it 'observes write duration' do
+ expect(subscriber).to receive(:observe)
+ .with(:write, event.duration)
subscriber.cache_write(event)
end
end
describe '#cache_delete' do
- it 'increments the cache_delete duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_delete, event.duration)
+ it 'observes delete duration' do
+ expect(subscriber).to receive(:observe)
+ .with(:delete, event.duration)
subscriber.cache_delete(event)
end
end
describe '#cache_exist?' do
- it 'increments the cache_exists duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_exists, event.duration)
+ it 'observes the exists duration' do
+ expect(subscriber).to receive(:observe)
+ .with(:exists, event.duration)
subscriber.cache_exist?(event)
end
@@ -109,12 +122,12 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
it 'increments the cache_read_hit count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_hit_count, 1)
+ .with(:cache_read_hit_count, 1)
subscriber.cache_fetch_hit(event)
end
@@ -133,47 +146,61 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
it 'increments the cache_fetch_miss count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_miss_count, 1)
+ .with(:cache_read_miss_count, 1)
+
+ subscriber.cache_generate(event)
+ end
+
+ it 'increments the cache_read_miss total' do
+ expect(subscriber.send(:metric_cache_misses_total)).to receive(:increment).with({})
subscriber.cache_generate(event)
end
end
end
- describe '#increment' do
+ describe '#observe' do
context 'without a transaction' do
it 'returns' do
expect(transaction).not_to receive(:increment)
- subscriber.increment(:foo, 15.2)
+ subscriber.observe(:foo, 15.2)
end
end
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
it 'increments the total and specific cache duration' do
expect(transaction).to receive(:increment)
- .with(:cache_duration, event.duration)
+ .with(:cache_duration, event.duration, false)
expect(transaction).to receive(:increment)
- .with(:cache_count, 1)
+ .with(:cache_count, 1, false)
expect(transaction).to receive(:increment)
- .with(:cache_delete_duration, event.duration)
+ .with(:cache_delete_duration, event.duration, false)
expect(transaction).to receive(:increment)
- .with(:cache_delete_count, 1)
+ .with(:cache_delete_count, 1, false)
+
+ subscriber.observe(:delete, event.duration)
+ end
+
+ it 'observes cache metric' do
+ expect(subscriber.send(:metric_cache_operation_duration_seconds))
+ .to receive(:observe)
+ .with(transaction.labels.merge(operation: :delete), event.duration / 1000.0)
- subscriber.increment(:cache_delete, event.duration)
+ subscriber.observe(:delete, event.duration)
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 3779af81512..1d162f53a13 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
-describe Gitlab::Metrics::Transaction do
- let(:transaction) { described_class.new }
+describe Gitlab::Metrics::WebTransaction do
+ let(:env) { {} }
+ let(:transaction) { described_class.new(env) }
describe '#duration' do
it 'returns the duration of a transaction in seconds' do
@@ -48,7 +49,7 @@ describe Gitlab::Metrics::Transaction do
describe '#method_call_for' do
it 'returns a MethodCall' do
- method = transaction.method_call_for('Foo#bar')
+ method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
end
@@ -85,14 +86,6 @@ describe Gitlab::Metrics::Transaction do
end
end
- describe '#add_tag' do
- it 'adds a tag' do
- transaction.add_tag(:foo, 'bar')
-
- expect(transaction.tags).to eq({ foo: 'bar' })
- end
- end
-
describe '#finish' do
it 'tracks the transaction details and submits them to Sidekiq' do
expect(transaction).to receive(:track_self)
@@ -127,7 +120,7 @@ describe Gitlab::Metrics::Transaction do
end
it 'adds the action as a tag for every metric' do
- transaction.action = 'Foo#bar'
+ allow(transaction).to receive(:labels).and_return(controller: 'Foo', action: 'bar')
transaction.track_self
hash = {
@@ -144,7 +137,8 @@ describe Gitlab::Metrics::Transaction do
end
it 'does not add an action tag for events' do
- transaction.action = 'Foo#bar'
+ allow(transaction).to receive(:labels).and_return(controller: 'Foo', action: 'bar')
+
transaction.add_event(:meow)
hash = {
@@ -161,6 +155,61 @@ describe Gitlab::Metrics::Transaction do
end
end
+ describe '#labels' do
+ context 'when request goes to Grape endpoint' do
+ before do
+ route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+ end
+ it 'provides labels with the method and path of the route in the grape endpoint' do
+ expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive' })
+ expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
+ end
+
+ it 'does not provide labels if route infos are missing' do
+ endpoint = double(:endpoint)
+ allow(endpoint).to receive(:route).and_raise
+
+ env['api.endpoint'] = endpoint
+
+ expect(transaction.labels).to eq({})
+ expect(transaction.action).to be_nil
+ end
+ end
+
+ context 'when request goes to ActionController' do
+ let(:content_type) { 'text/html' }
+
+ before do
+ klass = double(:klass, name: 'TestController')
+ controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
+
+ env['action_controller.instance'] = controller
+ end
+
+ it 'tags a transaction with the name and action of a controller' do
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
+ expect(transaction.action).to eq('TestController#show')
+ end
+
+ context 'when the response content type is not :html' do
+ let(:content_type) { 'application/json' }
+
+ it 'appends the mime type to the transaction action' do
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
+ expect(transaction.action).to eq('TestController#show.json')
+ end
+ end
+ end
+
+ it 'returns no labels when no route information is present in env' do
+ expect(transaction.labels).to eq({})
+ expect(transaction.action).to eq(nil)
+ end
+ end
+
describe '#add_event' do
it 'adds a metric' do
transaction.add_event(:meow)
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 599b8807d8d..1619fbd88b1 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -115,7 +115,7 @@ describe Gitlab::Metrics do
end
context 'with a transaction' do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new({}) }
before do
allow(described_class).to receive(:current_transaction)
@@ -124,13 +124,13 @@ describe Gitlab::Metrics do
it 'adds a metric to the current transaction' do
expect(transaction).to receive(:increment)
- .with('foo_real_time', a_kind_of(Numeric))
+ .with('foo_real_time', a_kind_of(Numeric), false)
expect(transaction).to receive(:increment)
- .with('foo_cpu_time', a_kind_of(Numeric))
+ .with('foo_cpu_time', a_kind_of(Numeric), false)
expect(transaction).to receive(:increment)
- .with('foo_call_count', 1)
+ .with('foo_call_count', 1, false)
described_class.measure(:foo) { 10 }
end
@@ -143,31 +143,6 @@ describe Gitlab::Metrics do
end
end
- describe '.tag_transaction' do
- context 'without a transaction' do
- it 'does nothing' do
- expect_any_instance_of(Gitlab::Metrics::Transaction)
- .not_to receive(:add_tag)
-
- described_class.tag_transaction(:foo, 'bar')
- end
- end
-
- context 'with a transaction' do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
-
- it 'adds the tag to the transaction' do
- expect(described_class).to receive(:current_transaction)
- .and_return(transaction)
-
- expect(transaction).to receive(:add_tag)
- .with(:foo, 'bar')
-
- described_class.tag_transaction(:foo, 'bar')
- end
- end
- end
-
describe '.action=' do
context 'without a transaction' do
it 'does nothing' do
@@ -180,7 +155,7 @@ describe Gitlab::Metrics do
context 'with a transaction' do
it 'sets the action of a transaction' do
- trans = Gitlab::Metrics::Transaction.new
+ trans = Gitlab::Metrics::WebTransaction.new({})
expect(described_class).to receive(:current_transaction)
.and_return(trans)
@@ -210,7 +185,7 @@ describe Gitlab::Metrics do
context 'with a transaction' do
it 'adds an event' do
- transaction = Gitlab::Metrics::Transaction.new
+ transaction = Gitlab::Metrics::WebTransaction.new({})
expect(transaction).to receive(:add_event).with(:meow)
@@ -224,7 +199,7 @@ describe Gitlab::Metrics do
shared_examples 'prometheus metrics API' do
describe '#counter' do
- subject { described_class.counter(:couter, 'doc') }
+ subject { described_class.counter(:counter, 'doc') }
describe '#increment' do
it 'successfully calls #increment without arguments' do
@@ -280,7 +255,7 @@ describe Gitlab::Metrics do
it_behaves_like 'prometheus metrics API'
describe '#null_metric' do
- subject { described_class.provide_metric(:test) }
+ subject { described_class.send(:provide_metric, :test) }
it { is_expected.to be_a(Gitlab::Metrics::NullMetric) }
end
@@ -321,7 +296,7 @@ describe Gitlab::Metrics do
it_behaves_like 'prometheus metrics API'
describe '#null_metric' do
- subject { described_class.provide_metric(:test) }
+ subject { described_class.send(:provide_metric, :test) }
it { is_expected.to be_nil }
end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index cab662819ac..60a134be939 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -17,89 +17,123 @@ describe Gitlab::Middleware::Go do
describe 'when go-get=1' do
let(:current_user) { nil }
- context 'with simple 2-segment project path' do
- let!(:project) { create(:project, :private) }
+ shared_examples 'go-get=1' do |enabled_protocol:|
+ context 'with simple 2-segment project path' do
+ let!(:project) { create(:project, :private) }
- context 'with subpackages' do
- let(:path) { "#{project.full_path}/subpackage" }
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
- end
- end
-
- context 'without subpackages' do
- let(:path) { project.full_path }
-
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
end
- end
- end
- context 'with a nested project path' do
- let(:group) { create(:group, :nested) }
- let!(:project) { create(:project, :public, namespace: group) }
+ context 'without subpackages' do
+ let(:path) { project.full_path }
- shared_examples 'a nested project' do
- context 'when the project is public' do
it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ expect_response_with_path(go, enabled_protocol, project.full_path)
end
end
+ end
- context 'when the project is private' do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
+ context 'with a nested project path' do
+ let(:group) { create(:group, :nested) }
+ let!(:project) { create(:project, :public, namespace: group) }
- context 'with access to the project' do
- let(:current_user) { project.creator }
+ shared_examples 'a nested project' do
+ context 'when the project is public' do
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
+ end
+ context 'when the project is private' do
before do
- project.team.add_master(current_user)
+ project.update_attribute(:visibility_level, Project::PRIVATE)
end
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ context 'with access to the project' do
+ let(:current_user) { project.creator }
+
+ before do
+ project.team.add_master(current_user)
+ end
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
end
- end
- context 'without access to the project' do
- it 'returns the 2-segment group path' do
- expect_response_with_path(go, group.full_path)
+ context 'without access to the project' do
+ it 'returns the 2-segment group path' do
+ expect_response_with_path(go, enabled_protocol, group.full_path)
+ end
end
end
end
- end
- context 'with subpackages' do
- let(:path) { "#{project.full_path}/subpackage" }
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
- it_behaves_like 'a nested project'
+ it_behaves_like 'a nested project'
+ end
+
+ context 'with a subpackage that is not a valid project path' do
+ let(:path) { "#{project.full_path}/---subpackage" }
+
+ it_behaves_like 'a nested project'
+ end
+
+ context 'without subpackages' do
+ let(:path) { project.full_path }
+
+ it_behaves_like 'a nested project'
+ end
end
- context 'with a subpackage that is not a valid project path' do
- let(:path) { "#{project.full_path}/---subpackage" }
+ context 'with a bogus path' do
+ let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
- it_behaves_like 'a nested project'
+ it 'skips go-import generation' do
+ expect(app).to receive(:call).and_return('no-go')
+
+ go
+ end
+ end
+ end
+
+ context 'with SSH disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: 'http')
end
- context 'without subpackages' do
- let(:path) { project.full_path }
+ include_examples 'go-get=1', enabled_protocol: :http
+ end
- it_behaves_like 'a nested project'
+ context 'with HTTP disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
end
+
+ include_examples 'go-get=1', enabled_protocol: :ssh
end
- context 'with a bogus path' do
- let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
+ context 'with nothing disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: nil)
+ end
- it 'skips go-import generation' do
- expect(app).to receive(:call).and_return('no-go')
+ include_examples 'go-get=1', enabled_protocol: nil
+ end
- go
+ context 'with nothing disabled (blank string)' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: '')
end
+
+ include_examples 'go-get=1', enabled_protocol: nil
end
end
@@ -113,10 +147,16 @@ describe Gitlab::Middleware::Go do
middleware.call(env)
end
- def expect_response_with_path(response, path)
+ def expect_response_with_path(response, protocol, path)
+ repository_url = case protocol
+ when :ssh
+ "ssh://git@#{Gitlab.config.gitlab.host}/#{path}.git"
+ when :http, nil
+ "http://#{Gitlab.config.gitlab.host}/#{path}.git"
+ end
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git" /></head></html>}
+ expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /></head></html>}
expect(response[2].body).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
index 88107536c9e..14f2c3cb86f 100644
--- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Middleware::RailsQueueDuration do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
let(:env) { {} }
- let(:transaction) { double(:transaction) }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
before do
expect(app).to receive(:call).with(env).and_return('yay')
@@ -30,6 +30,16 @@ describe Gitlab::Middleware::RailsQueueDuration do
expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
expect(middleware.call(env)).to eq('yay')
end
+
+ it 'observes rails queue duration metrics and calls the app when the header is present' do
+ env['HTTP_GITLAB_WORKHORSE_PROXY_START'] = '2000000000'
+
+ expect(middleware.send(:metric_rails_queue_duration_seconds)).to receive(:observe).with(transaction.labels, 1)
+
+ Timecop.freeze(Time.at(3)) do
+ expect(middleware.call(env)).to eq('yay')
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 742a792a1af..07ba11b93a3 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -83,15 +83,24 @@ describe Gitlab::Middleware::ReadOnly do
expect(subject).to disallow_request
end
- context 'whitelisted requests' do
- it 'expects DELETE request to logout to be allowed' do
- response = request.delete('/users/sign_out')
+ it 'expects POST of new file that looks like an LFS batch url to be disallowed' do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
+ response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch')
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
- end
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+ it 'returns last_vistited_url for disallowed request' do
+ response = request.post('/test_request')
+
+ expect(response.location).to eq 'http://localhost/'
+ end
+
+ context 'whitelisted requests' do
it 'expects a POST internal request to be allowed' do
+ expect(Rails.application.routes).not_to receive(:recognize_path)
+
response = request.post("/api/#{API::API.version}/internal")
expect(response).not_to be_a_redirect
@@ -99,11 +108,32 @@ describe Gitlab::Middleware::ReadOnly do
end
it 'expects a POST LFS request to batch URL to be allowed' do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
response = request.post('/root/rouge.git/info/lfs/objects/batch')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
+
+ it 'expects a POST request to git-upload-pack URL to be allowed' do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
+ response = request.post('/root/rouge.git/git-upload-pack')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+
+ it 'expects requests to sidekiq admin to be allowed' do
+ response = request.post('/admin/sidekiq')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+
+ response = request.get('/admin/sidekiq')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index db26e16e3b2..2f19fb7312d 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::OAuth::User do
let(:oauth_user) { described_class.new(auth_hash) }
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
- let(:dn) { 'uid=user1,ou=People,dc=example' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'my-provider' }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
let(:info_hash) do
@@ -662,4 +662,13 @@ describe Gitlab::OAuth::User do
end
end
end
+
+ describe '.find_by_uid_and_provider' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ it 'normalizes extern_uid' do
+ allow(oauth_user.auth_hash).to receive(:uid).and_return('MY-UID')
+ expect(oauth_user.find_user).to eql gl_user
+ end
+ end
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index f1f188cbfb5..0ae90069b7f 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -45,21 +45,16 @@ describe Gitlab::PathRegex do
Found new routes that could cause conflicts with existing namespaced routes
for groups or projects.
- Add <#{missing_words.join(', ')}> to `Gitlab::PathRegex::#{constant_name}
- to make sure no projects or namespaces can be created with those paths.
-
- To rename any existing records with those paths you can use the
- `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
- migration helper.
-
- Make sure to make a note of the renamed records in the release blog post.
+ Nest <#{missing_words.join(', ')}> in a route containing `-`, that way
+ we know there will be no conflicts with groups or projects created with those
+ paths.
MISSING
end
if additional_words.any?
message += <<-ADDITIONAL
- Why are <#{additional_words.join(', ')}> in `#{constant_name}`?
+ Is <#{additional_words.join(', ')}> in `#{constant_name}` required?
If they are really required, update these specs to reflect that.
ADDITIONAL
@@ -68,14 +63,27 @@ describe Gitlab::PathRegex do
message
end
- let(:all_routes) do
+ let(:all_non_legacy_routes) do
route_set = Rails.application.routes
routes_collection = route_set.routes
routes_array = routes_collection.routes
- routes_array.map { |route| route.path.spec.to_s }
+
+ non_legacy_routes = routes_array.reject do |route|
+ route.name.to_s =~ /legacy_(\w*)_redirect/
+ end
+
+ non_deprecated_redirect_routes = non_legacy_routes.reject do |route|
+ app = route.app
+ # `app.app` is either another app, or `self`. We want to find the final app.
+ app = app.app while app.try(:app) && app.app != app
+
+ app.is_a?(ActionDispatch::Routing::PathRedirect) && app.block.include?('/-/')
+ end
+
+ non_deprecated_redirect_routes.map { |route| route.path.spec.to_s }
end
- let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
+ let(:routes_without_format) { all_non_legacy_routes.map { |path| without_format(path) } }
# Routes not starting with `/:` or `/*`
# all routes not starting with a param
@@ -144,16 +152,7 @@ describe Gitlab::PathRegex do
let(:paths_after_group_id) do
group_routes.map do |route|
route.gsub(STARTING_WITH_GROUP, '').split('/').first
- end.uniq + ee_paths_after_group_id
- end
-
- let(:ee_paths_after_group_id) do
- %w(analytics
- ldap
- ldap_group_links
- notification_setting
- audit_events
- pipeline_quota hooks)
+ end.uniq
end
describe 'TOP_LEVEL_ROUTES' do
@@ -212,8 +211,6 @@ describe Gitlab::PathRegex do
it 'accepts group routes' do
expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('labels/')
end
it 'is not case sensitive' do
@@ -245,8 +242,6 @@ describe Gitlab::PathRegex do
it 'accepts group routes' do
expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('labels/')
end
end
@@ -267,8 +262,6 @@ describe Gitlab::PathRegex do
it 'accepts group routes' do
expect(subject).to match('activity/more/')
- expect(subject).to match('group_members/more/')
- expect(subject).to match('labels/more/')
end
end
end
@@ -290,9 +283,7 @@ describe Gitlab::PathRegex do
end
it 'rejects group routes' do
- expect(subject).not_to match('root/activity/')
- expect(subject).not_to match('root/group_members/')
- expect(subject).not_to match('root/labels/')
+ expect(subject).not_to match('root/-/')
end
end
@@ -312,9 +303,7 @@ describe Gitlab::PathRegex do
end
it 'rejects group routes' do
- expect(subject).not_to match('root/activity/more/')
- expect(subject).not_to match('root/group_members/more/')
- expect(subject).not_to match('root/labels/more/')
+ expect(subject).not_to match('root/-/')
end
end
end
@@ -347,9 +336,7 @@ describe Gitlab::PathRegex do
end
it 'accepts group routes' do
- expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('labels/')
+ expect(subject).to match('analytics/')
end
it 'is not case sensitive' do
@@ -380,9 +367,7 @@ describe Gitlab::PathRegex do
end
it 'accepts group routes' do
- expect(subject).to match('root/activity/')
- expect(subject).to match('root/group_members/')
- expect(subject).to match('root/labels/')
+ expect(subject).to match('root/analytics/')
end
it 'is not case sensitive' do
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 1c23fb5f285..1765980e977 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Saml::User do
let(:saml_user) { described_class.new(auth_hash) }
let(:gl_user) { saml_user.gl_user }
let(:uid) { 'my-uid' }
- let(:dn) { 'uid=user1,ou=People,dc=example' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'saml' }
let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) }
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
new file mode 100644
index 00000000000..8fdbbacd04d
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::MemoryKiller do
+ subject { described_class.new }
+ let(:pid) { 999 }
+
+ let(:worker) { double(:worker, class: 'TestWorker') }
+ let(:job) { { 'jid' => 123 } }
+ let(:queue) { 'test_queue' }
+
+ def run
+ thread = subject.call(worker, job, queue) { nil }
+ thread&.join
+ end
+
+ before do
+ allow(subject).to receive(:get_rss).and_return(10.kilobytes)
+ allow(subject).to receive(:pid).and_return(pid)
+ end
+
+ context 'when MAX_RSS is set to 0' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 0)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 5.kilobytes)
+ end
+
+ it 'sends the STP, TERM and KILL signals at expected times' do
+ expect(subject).to receive(:sleep).with(15 * 60).ordered
+ expect(Process).to receive(:kill).with('SIGSTP', pid).ordered
+
+ expect(subject).to receive(:sleep).with(30).ordered
+ expect(Process).to receive(:kill).with('SIGTERM', pid).ordered
+
+ expect(subject).to receive(:sleep).with(10).ordered
+ expect(Process).to receive(:kill).with('SIGKILL', pid).ordered
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is not exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 15.kilobytes)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index f18823b61ef..d9b3c2350b1 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -20,6 +20,22 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
end
+ it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
+ expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for alternative version of 127.0.0.1 (0x7f.1)' do
+ expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for alternative version of 127.0.0.1 (2130706433)' do
+ expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for alternative version of 127.0.0.1 (127.000.000.001)' do
+ expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true
+ end
+
it 'returns true for a non-alphanumeric hostname' do
stub_resolv
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a7b65e94706..a4c1113ae37 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -60,9 +60,9 @@ describe Gitlab::UsageData do
deploy_keys
deployments
environments
- gcp_clusters
- gcp_clusters_enabled
- gcp_clusters_disabled
+ clusters
+ clusters_enabled
+ clusters_disabled
in_review_folder
groups
issues
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
new file mode 100644
index 00000000000..4a104ab6d97
--- /dev/null
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::Utils::StrongMemoize do
+ let(:klass) do
+ struct = Struct.new(:value) do
+ def method_name
+ strong_memoize(:method_name) do
+ trace << value
+ value
+ end
+ end
+
+ def trace
+ @trace ||= []
+ end
+ end
+
+ struct.include(described_class)
+ struct
+ end
+
+ subject(:object) { klass.new(value) }
+
+ shared_examples 'caching the value' do
+ it 'only calls the block once' do
+ value0 = object.method_name
+ value1 = object.method_name
+
+ expect(value0).to eq(value)
+ expect(value1).to eq(value)
+ expect(object.trace).to contain_exactly(value)
+ end
+
+ it 'returns and defines the instance variable for the exact value' do
+ returned_value = object.method_name
+ memoized_value = object.instance_variable_get(:@method_name)
+
+ expect(returned_value).to eql(value)
+ expect(memoized_value).to eql(value)
+ end
+ end
+
+ describe '#strong_memoize' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value #{value}" do
+ let(:value) { value }
+
+ it_behaves_like 'caching the value'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 80bf7986ee0..249c77dc636 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -268,7 +268,8 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: false
}
end
@@ -282,7 +283,8 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "wiki-#{project.id}",
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: false
}
end
@@ -324,6 +326,12 @@ describe Gitlab::Workhorse do
expect(subject).to include(gitaly_params)
end
+
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+
+ it { is_expected.to include(ShowAllRefs: true) }
+ end
end
context "when git_receive_pack action is passed" do
@@ -336,6 +344,12 @@ describe Gitlab::Workhorse do
let(:action) { 'info_refs' }
it { expect(subject).to include(gitaly_params) }
+
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+
+ it { is_expected.to include(ShowAllRefs: true) }
+ end
end
context 'when action passed is not supported by Gitaly' do
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index acc5bd1da35..fac23dce44d 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -69,7 +69,7 @@ describe GoogleApi::CloudPlatform::Client do
let(:cluster_name) { 'test-cluster' }
let(:cluster_size) { 1 }
- let(:machine_type) { 'n1-standard-4' }
+ let(:machine_type) { 'n1-standard-2' }
let(:operation) { double }
before do
diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
index b4b83b70d1c..a0fb86345f3 100644
--- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
+++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
@@ -39,14 +39,6 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
it { is_expected.to eq(expected_result) }
end
-
- it 'skips GitLab read-only instances' do
- stub_user
- stub_home_dir
- allow(Gitlab::Database).to receive(:read_only?).and_return(true)
-
- is_expected.to be_truthy
- end
end
describe '#check?' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index c832cee965b..f942a22b6d1 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -783,7 +783,25 @@ describe Notify do
shared_examples 'an email for a note on a diff discussion' do |model|
let(:note) { create(model, author: note_author) }
- it "includes diffs with character-level highlighting" do
+ context 'when note is on image' do
+ before do
+ allow_any_instance_of(DiffDiscussion).to receive(:on_image?).and_return(true)
+ end
+
+ it 'does not include diffs with character-level highlighting' do
+ is_expected.not_to have_body_text '<span class="p">}</span></span>'
+ end
+
+ it 'ends the intro with a dot' do
+ is_expected.to have_body_text "#{note.diff_file.file_path}</a>."
+ end
+ end
+
+ it 'ends the intro with a colon' do
+ is_expected.to have_body_text "#{note.diff_file.file_path}</a>:"
+ end
+
+ it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text '<span class="p">}</span></span>'
end
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
new file mode 100644
index 00000000000..9f41534441b
--- /dev/null
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb')
+
+describe MigrateGcpClustersToNewClustersArchitectures, :migration do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { create(:kubernetes_service, project: project) }
+
+ context 'when cluster is being created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 2 } # creating
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "''" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { 'NULL' }
+ let(:ca_cert) { 'NULL' }
+ let(:encrypted_kubernetes_token) { 'NULL' }
+ let(:encrypted_kubernetes_token_iv) { 'NULL' }
+ let(:username) { 'NULL' }
+ let(:encrypted_password) { 'NULL' }
+ let(:encrypted_password_iv) { 'NULL' }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { 'NULL' }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(gcp_cluster_name.delete!("'"))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to be_nil
+ expect(cluster.provider_gcp.endpoint).to be_nil
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to be_nil
+ expect(cluster.platform_kubernetes.ca_cert).to be_nil
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to be_nil
+ end
+ end
+
+ context 'when cluster has been created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 3 } # created
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "'general error'" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { "'111.111.111.111'" }
+ let(:ca_cert) { "'ca_cert'" }
+ let(:encrypted_kubernetes_token) { "'encrypted_kubernetes_token'" }
+ let(:encrypted_kubernetes_token_iv) { "'encrypted_kubernetes_token_iv'" }
+ let(:username) { "'username'" }
+ let(:encrypted_password) { "'encrypted_password'" }
+ let(:encrypted_password_iv) { "'encrypted_password_iv'" }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { "'gcp_operation_id'" }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(tr(gcp_cluster_name))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to eq(tr(gcp_operation_id))
+ expect(cluster.provider_gcp.endpoint).to eq(tr(endpoint))
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to eq('https://' + tr(endpoint))
+ expect(cluster.platform_kubernetes.ca_cert).to eq(tr(ca_cert))
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to eq(tr(username))
+ expect(cluster.platform_kubernetes.encrypted_password).to eq(tr(encrypted_password))
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to eq(tr(encrypted_password_iv))
+ expect(cluster.platform_kubernetes.encrypted_token).to eq(tr(encrypted_kubernetes_token))
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to eq(tr(encrypted_kubernetes_token_iv))
+ end
+ end
+
+ def tr(s)
+ s.delete("'")
+ end
+end
diff --git a/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb
new file mode 100644
index 00000000000..b4834705011
--- /dev/null
+++ b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20171012125712_migrate_user_authentication_token_to_personal_access_token.rb')
+
+describe MigrateUserAuthenticationTokenToPersonalAccessToken, :migration do
+ let(:users) { table(:users) }
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+
+ let!(:user) { users.create!(id: 1, email: 'user@example.com', authentication_token: 'user-token', admin: false) }
+ let!(:admin) { users.create!(id: 2, email: 'admin@example.com', authentication_token: 'admin-token', admin: true) }
+
+ it 'migrates private tokens to Personal Access Tokens' do
+ migrate!
+
+ expect(personal_access_tokens.count).to eq(2)
+
+ user_token = personal_access_tokens.find_by(user_id: user.id)
+ admin_token = personal_access_tokens.find_by(user_id: admin.id)
+
+ expect(user_token.token).to eq('user-token')
+ expect(admin_token.token).to eq('admin-token')
+
+ expect(user_token.scopes).to eq(%w[api].to_yaml)
+ expect(admin_token.scopes).to eq(%w[api sudo].to_yaml)
+ end
+end
diff --git a/spec/migrations/remove_empty_fork_networks_spec.rb b/spec/migrations/remove_empty_fork_networks_spec.rb
new file mode 100644
index 00000000000..cf6ae5cda74
--- /dev/null
+++ b/spec/migrations/remove_empty_fork_networks_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171114104051_remove_empty_fork_networks.rb')
+
+describe RemoveEmptyForkNetworks, :migration do
+ let!(:fork_networks) { table(:fork_networks) }
+
+ let(:deleted_project) { create(:project) }
+ let!(:empty_network) { create(:fork_network, id: 1, root_project_id: deleted_project.id) }
+ let!(:other_network) { create(:fork_network, id: 2, root_project_id: create(:project).id) }
+
+ before do
+ deleted_project.destroy!
+ end
+
+ it 'deletes only the fork network without members' do
+ expect(fork_networks.count).to eq(2)
+
+ migrate!
+
+ expect(fork_networks.find_by(id: empty_network.id)).to be_nil
+ expect(fork_networks.find_by(id: other_network.id)).not_to be_nil
+ expect(fork_networks.count).to eq(1)
+ end
+end
diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
index f95bd6e3511..76afb6c19cf 100644
--- a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
+++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
@@ -2,19 +2,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170703130158_schedule_merge_request_diff_migrations')
describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do
- matcher :be_scheduled_migration do |time, *expected|
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration, expected] &&
- job['at'].to_i == time.to_i
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
let(:projects) { table(:projects) }
@@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes.from_now, 1, 1)
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 2, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes.from_now, 4, 4)
+ expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, 4, 4)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
index 4ab1bb67058..cf323973384 100644
--- a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
+++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
@@ -2,19 +2,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170926150348_schedule_merge_request_diff_migrations_take_two')
describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do
- matcher :be_scheduled_migration do |time, *expected|
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration, expected] &&
- job['at'].to_i == time.to_i
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
let(:projects) { table(:projects) }
@@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 1, 1)
- expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes.from_now, 2, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes.from_now, 4, 4)
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes, 4, 4)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb
new file mode 100644
index 00000000000..158d0bc02ed
--- /dev/null
+++ b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations')
+
+describe ScheduleMergeRequestLatestMergeRequestDiffIdMigrations, :migration, :sidekiq do
+ let(:projects_table) { table(:projects) }
+ let(:merge_requests_table) { table(:merge_requests) }
+ let(:merge_request_diffs_table) { table(:merge_request_diffs) }
+
+ let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') }
+
+ let!(:merge_request_1) { create_mr!('mr_1', diffs: 1) }
+ let!(:merge_request_2) { create_mr!('mr_2', diffs: 2) }
+ let!(:merge_request_migrated) { create_mr!('merge_request_migrated', diffs: 3) }
+ let!(:merge_request_4) { create_mr!('mr_4', diffs: 3) }
+
+ def create_mr!(name, diffs: 0)
+ merge_request =
+ merge_requests_table.create!(target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: name,
+ title: name)
+
+ diffs.times do
+ merge_request_diffs_table.create!(merge_request_id: merge_request.id)
+ end
+
+ merge_request
+ end
+
+ def diffs_for(merge_request)
+ merge_request_diffs_table.where(merge_request_id: merge_request.id)
+ end
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ diff_id = diffs_for(merge_request_migrated).minimum(:id)
+ merge_request_migrated.update!(latest_merge_request_diff_id: diff_id)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, merge_request_1.id, merge_request_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, merge_request_2.id, merge_request_2.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, merge_request_4.id, merge_request_4.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'schedules background migrations' do
+ Sidekiq::Testing.inline! do
+ expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 3
+
+ migrate!
+
+ expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 0
+ end
+ end
+end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 47342f98283..81e35e6c931 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -16,6 +16,23 @@ describe Blob do
end
end
+ describe '.lazy' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit_by(oid: 'e63f41fe459e62e1228fcef60d7189127aeba95a') }
+
+ it 'fetches all blobs when the first is accessed' do
+ changelog = described_class.lazy(project, commit.id, 'CHANGELOG')
+ contributing = described_class.lazy(project, commit.id, 'CONTRIBUTING.md')
+
+ expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original
+ expect(Gitlab::Git::Blob).not_to receive(:find)
+
+ # Access property so the values are loaded
+ changelog.id
+ contributing.id
+ end
+ end
+
describe '#data' do
context 'using a binary blob' do
it 'returns the data as-is' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 41ecdb604f1..1795ee8e9a4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -270,6 +270,23 @@ describe Ci::Build do
end
end
+ describe '#triggered_by?' do
+ subject { build.triggered_by?(user) }
+
+ context 'when user is owner' do
+ let(:build) { create(:ci_build, pipeline: pipeline, user: user) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user is not owner' do
+ let(:another_user) { create(:user) }
+ let(:build) { create(:ci_build, pipeline: pipeline, user: another_user) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '#detailed_status' do
it 'returns a detailed status' do
expect(build.detailed_status(user))
@@ -1271,6 +1288,7 @@ describe Ci::Build do
{ key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 2c9e7013b77..3a19a0753e2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -625,38 +625,29 @@ describe Ci::Pipeline, :mailer do
shared_context 'with some outdated pipelines' do
before do
- create_pipeline(:canceled, 'ref', 'A')
- create_pipeline(:success, 'ref', 'A')
- create_pipeline(:failed, 'ref', 'B')
- create_pipeline(:skipped, 'feature', 'C')
+ create_pipeline(:canceled, 'ref', 'A', project)
+ create_pipeline(:success, 'ref', 'A', project)
+ create_pipeline(:failed, 'ref', 'B', project)
+ create_pipeline(:skipped, 'feature', 'C', project)
end
- def create_pipeline(status, ref, sha)
- create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
+ def create_pipeline(status, ref, sha, project)
+ create(
+ :ci_empty_pipeline,
+ status: status,
+ ref: ref,
+ sha: sha,
+ project: project
+ )
end
end
- describe '.latest' do
+ describe '.newest_first' do
include_context 'with some outdated pipelines'
- context 'when no ref is specified' do
- let(:pipelines) { described_class.latest.all }
-
- it 'returns the latest pipeline for the same ref and different sha' do
- expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
- expect(pipelines.map(&:status))
- .to contain_exactly('success', 'failed', 'skipped')
- end
- end
-
- context 'when ref is specified' do
- let(:pipelines) { described_class.latest('ref').all }
-
- it 'returns the latest pipeline for ref and different sha' do
- expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
- expect(pipelines.map(&:status))
- .to contain_exactly('success', 'failed')
- end
+ it 'returns the pipelines from new to old' do
+ expect(described_class.newest_first.pluck(:status))
+ .to eq(%w[skipped failed success canceled])
end
end
@@ -664,20 +655,14 @@ describe Ci::Pipeline, :mailer do
include_context 'with some outdated pipelines'
context 'when no ref is specified' do
- let(:latest_status) { described_class.latest_status }
-
- it 'returns the latest status for the same ref and different sha' do
- expect(latest_status).to eq(described_class.latest.status)
- expect(latest_status).to eq('failed')
+ it 'returns the status of the latest pipeline' do
+ expect(described_class.latest_status).to eq('skipped')
end
end
context 'when ref is specified' do
- let(:latest_status) { described_class.latest_status('ref') }
-
- it 'returns the latest status for ref and different sha' do
- expect(latest_status).to eq(described_class.latest_status('ref'))
- expect(latest_status).to eq('failed')
+ it 'returns the status of the latest pipeline for the given ref' do
+ expect(described_class.latest_status('ref')).to eq('failed')
end
end
end
@@ -686,7 +671,7 @@ describe Ci::Pipeline, :mailer do
include_context 'with some outdated pipelines'
let!(:latest_successful_pipeline) do
- create_pipeline(:success, 'ref', 'D')
+ create_pipeline(:success, 'ref', 'D', project)
end
it 'returns the latest successful pipeline' do
@@ -698,8 +683,13 @@ describe Ci::Pipeline, :mailer do
describe '.latest_successful_for_refs' do
include_context 'with some outdated pipelines'
- let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') }
- let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') }
+ let!(:latest_successful_pipeline1) do
+ create_pipeline(:success, 'ref1', 'D', project)
+ end
+
+ let!(:latest_successful_pipeline2) do
+ create_pipeline(:success, 'ref2', 'D', project)
+ end
it 'returns the latest successful pipeline for both refs' do
refs = %w(ref1 ref2 ref3)
@@ -708,6 +698,62 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.latest_status_per_commit' do
+ let(:project) { create(:project) }
+
+ before do
+ pairs = [
+ %w[success ref1 123],
+ %w[manual master 123],
+ %w[failed ref 456]
+ ]
+
+ pairs.each do |(status, ref, sha)|
+ create(
+ :ci_empty_pipeline,
+ status: status,
+ ref: ref,
+ sha: sha,
+ project: project
+ )
+ end
+ end
+
+ context 'without a ref' do
+ it 'returns a Hash containing the latest status per commit for all refs' do
+ expect(described_class.latest_status_per_commit(%w[123 456]))
+ .to eq({ '123' => 'manual', '456' => 'failed' })
+ end
+
+ it 'only includes the status of the given commit SHAs' do
+ expect(described_class.latest_status_per_commit(%w[123]))
+ .to eq({ '123' => 'manual' })
+ end
+
+ context 'when there are two pipelines for a ref and SHA' do
+ it 'returns the status of the latest pipeline' do
+ create(
+ :ci_empty_pipeline,
+ status: 'failed',
+ ref: 'master',
+ sha: '123',
+ project: project
+ )
+
+ expect(described_class.latest_status_per_commit(%w[123]))
+ .to eq({ '123' => 'failed' })
+ end
+ end
+ end
+
+ context 'with a ref' do
+ it 'only includes the pipelines for the given ref' do
+ expect(described_class.latest_status_per_commit(%w[123 456], 'master'))
+ .to eq({ '123' => 'manual' })
+ end
+ end
+ end
+
describe '.internal_sources' do
subject { described_class.internal_sources }
@@ -1456,6 +1502,10 @@ describe Ci::Pipeline, :mailer do
create(:ci_build, :success, :artifacts, pipeline: pipeline)
end
+ it 'returns an Array' do
+ expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array)
+ end
+
it 'returns the latest builds' do
expect(pipeline.latest_builds_with_artifacts).to eq([build])
end
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
new file mode 100644
index 00000000000..f8855079842
--- /dev/null
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -0,0 +1,102 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Helm do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:cluster) }
+
+ describe '#name' do
+ it 'is .application_name' do
+ expect(subject.name).to eq(described_class.application_name)
+ end
+
+ it 'is recorded in Clusters::Cluster::APPLICATIONS' do
+ expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
+ end
+ end
+
+ describe '#version' do
+ it 'defaults to Gitlab::Kubernetes::Helm::HELM_VERSION' do
+ expect(subject.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
+ end
+ end
+
+ describe '#status' do
+ let(:cluster) { create(:cluster) }
+
+ subject { described_class.new(cluster: cluster) }
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+
+ context 'when platform kubernetes is defined' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it 'defaults to :installable' do
+ expect(subject.status_name).to be(:installable)
+ end
+ end
+ end
+
+ describe '#install_command' do
+ it 'has all the needed information' do
+ expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true, chart: nil)
+ end
+ end
+
+ describe 'status state machine' do
+ describe '#make_installing' do
+ subject { create(:cluster_applications_helm, :scheduled) }
+
+ it 'is installing' do
+ subject.make_installing!
+
+ expect(subject).to be_installing
+ end
+ end
+
+ describe '#make_installed' do
+ subject { create(:cluster_applications_helm, :installing) }
+
+ it 'is installed' do
+ subject.make_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ describe '#make_errored' do
+ subject { create(:cluster_applications_helm, :installing) }
+ let(:reason) { 'some errors' }
+
+ it 'is errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
+
+ describe '#make_scheduled' do
+ subject { create(:cluster_applications_helm, :installable) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+
+ describe 'when was errored' do
+ subject { create(:cluster_applications_helm, :errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
new file mode 100644
index 00000000000..b83472e1944
--- /dev/null
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -0,0 +1,108 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Ingress do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:cluster) }
+
+ describe '#name' do
+ it 'is .application_name' do
+ expect(subject.name).to eq(described_class.application_name)
+ end
+
+ it 'is recorded in Clusters::Cluster::APPLICATIONS' do
+ expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
+ end
+ end
+
+ describe '#status' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { described_class.new(cluster: cluster) }
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+
+ context 'when application helm is scheduled' do
+ before do
+ create(:cluster_applications_helm, :scheduled, cluster: cluster)
+ end
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+ end
+
+ context 'when application helm is installed' do
+ before do
+ create(:cluster_applications_helm, :installed, cluster: cluster)
+ end
+
+ it 'defaults to :installable' do
+ expect(subject.status_name).to be(:installable)
+ end
+ end
+ end
+
+ describe '#install_command' do
+ it 'has all the needed information' do
+ expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false, chart: subject.chart)
+ end
+ end
+
+ describe 'status state machine' do
+ describe '#make_installing' do
+ subject { create(:cluster_applications_ingress, :scheduled) }
+
+ it 'is installing' do
+ subject.make_installing!
+
+ expect(subject).to be_installing
+ end
+ end
+
+ describe '#make_installed' do
+ subject { create(:cluster_applications_ingress, :installing) }
+
+ it 'is installed' do
+ subject.make_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ describe '#make_errored' do
+ subject { create(:cluster_applications_ingress, :installing) }
+ let(:reason) { 'some errors' }
+
+ it 'is errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
+
+ describe '#make_scheduled' do
+ subject { create(:cluster_applications_ingress, :installable) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+
+ describe 'when was errored' do
+ subject { create(:cluster_applications_ingress, :errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
new file mode 100644
index 00000000000..b91a5e7a272
--- /dev/null
+++ b/spec/models/clusters/cluster_spec.rb
@@ -0,0 +1,202 @@
+require 'spec_helper'
+
+describe Clusters::Cluster do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:projects) }
+ it { is_expected.to have_one(:provider_gcp) }
+ it { is_expected.to have_one(:platform_kubernetes) }
+ it { is_expected.to delegate_method(:status).to(:provider) }
+ it { is_expected.to delegate_method(:status_reason).to(:provider) }
+ it { is_expected.to delegate_method(:status_name).to(:provider) }
+ it { is_expected.to delegate_method(:on_creation?).to(:provider) }
+ it { is_expected.to delegate_method(:update_kubernetes_integration!).to(:platform) }
+ it { is_expected.to respond_to :project }
+
+ describe '.enabled' do
+ subject { described_class.enabled }
+
+ let!(:cluster) { create(:cluster, enabled: true) }
+
+ before do
+ create(:cluster, enabled: false)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '.disabled' do
+ subject { described_class.disabled }
+
+ let!(:cluster) { create(:cluster, enabled: false) }
+
+ before do
+ create(:cluster, enabled: true)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe 'validation' do
+ subject { cluster.valid? }
+
+ context 'when validates name' do
+ context 'when provided by user' do
+ let!(:cluster) { build(:cluster, :provided_by_user, name: name) }
+
+ context 'when name is empty' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is nil' do
+ let(:name) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when provided by gcp' do
+ let!(:cluster) { build(:cluster, :provided_by_gcp, name: name) }
+
+ context 'when name is shorter than 1' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is longer than 63' do
+ let(:name) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name includes invalid character' do
+ let(:name) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when record is persisted' do
+ let(:name) { 'cluster-name-1' }
+
+ before do
+ cluster.save!
+ end
+
+ context 'when name is changed' do
+ before do
+ cluster.name = 'new-cluster-name'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is same' do
+ before do
+ cluster.name = name
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+ end
+
+ context 'when validates restrict_modification' do
+ context 'when creation is on going' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_falsey }
+ end
+
+ context 'when creation is done' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_truthy }
+ end
+ end
+ end
+
+ describe '#provider' do
+ subject { cluster.provider }
+
+ context 'when provider is gcp' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it 'returns a provider' do
+ is_expected.to eq(cluster.provider_gcp)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s)
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#platform' do
+ subject { cluster.platform }
+
+ context 'when platform is kubernetes' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it 'returns a platform' do
+ is_expected.to eq(cluster.platform_kubernetes)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Platforms.to_s)
+ end
+ end
+ end
+
+ describe '#first_project' do
+ subject { cluster.first_project }
+
+ context 'when cluster belongs to a project' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { Clusters::Project.find_by_cluster_id(cluster.id).project }
+
+ it { is_expected.to eq(project) }
+ end
+
+ context 'when cluster does not belong to projects' do
+ let(:cluster) { create(:cluster) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#applications' do
+ set(:cluster) { create(:cluster) }
+
+ subject { cluster.applications }
+
+ context 'when none of applications are created' do
+ it 'returns a list of a new objects' do
+ is_expected.not_to be_empty
+ end
+ end
+
+ context 'when applications are created' do
+ let!(:helm) { create(:cluster_applications_helm, cluster: cluster) }
+ let!(:ingress) { create(:cluster_applications_ingress, cluster: cluster) }
+
+ it 'returns a list of created applications' do
+ is_expected.to contain_exactly(helm, ingress)
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
new file mode 100644
index 00000000000..ed76be703a5
--- /dev/null
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -0,0 +1,188 @@
+require 'spec_helper'
+
+describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to respond_to :ca_pem }
+
+ describe 'before_validation' do
+ context 'when namespace includes upper case' do
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+ let(:namespace) { 'ABC' }
+
+ it 'converts to lower case' do
+ expect(kubernetes.namespace).to eq('abc')
+ end
+ end
+ end
+
+ describe 'validation' do
+ subject { kubernetes.valid? }
+
+ context 'when validates namespace' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is blank' do
+ let(:namespace) { '' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when namespace is longer than 63' do
+ let(:namespace) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace includes invalid character' do
+ let(:namespace) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace is vaild' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates api_url' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.api_url = api_url
+ end
+
+ context 'when api_url is invalid url' do
+ let(:api_url) { '!!!!!!' }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is valid url' do
+ let(:api_url) { 'https://111.111.111.111' }
+
+ it { expect(kubernetes.save).to be_truthy }
+ end
+ end
+
+ context 'when validates token' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.token = token
+ end
+
+ context 'when token is nil' do
+ let(:token) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+ end
+ end
+
+ describe 'after_save from Clusters::Cluster' do
+ context 'when platform_kubernetes is being cerated' do
+ let(:enabled) { true }
+ let(:project) { create(:project) }
+ let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, enabled: enabled, projects: [project]) }
+ let(:platform) { build(:cluster_platform_kubernetes, :configured) }
+ let(:provider) { build(:cluster_provider_gcp) }
+ let(:kubernetes_service) { project.kubernetes_service }
+
+ it 'updates KubernetesService' do
+ cluster.save!
+
+ expect(kubernetes_service.active).to eq(enabled)
+ expect(kubernetes_service.api_url).to eq(platform.api_url)
+ expect(kubernetes_service.namespace).to eq(platform.namespace)
+ expect(kubernetes_service.ca_pem).to eq(platform.ca_cert)
+ end
+ end
+
+ context 'when platform_kubernetes has been created' do
+ let(:enabled) { false }
+ let!(:project) { create(:project) }
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:platform) { cluster.platform }
+ let(:kubernetes_service) { project.kubernetes_service }
+
+ it 'updates KubernetesService' do
+ cluster.update(enabled: enabled)
+
+ expect(kubernetes_service.active).to eq(enabled)
+ end
+ end
+
+ context 'when kubernetes_service has been configured without cluster integration' do
+ let!(:project) { create(:project) }
+ let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, projects: [project]) }
+ let(:platform) { build(:cluster_platform_kubernetes, :configured, api_url: 'https://111.111.111.111') }
+ let(:provider) { build(:cluster_provider_gcp) }
+
+ before do
+ create(:kubernetes_service, project: project)
+ end
+
+ it 'raises an error' do
+ expect { cluster.save! }.to raise_error('Kubernetes service already configured')
+ end
+ end
+ end
+
+ describe '#actual_namespace' do
+ subject { kubernetes.actual_namespace }
+
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is present' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to eq(namespace) }
+ end
+
+ context 'when namespace is not present' do
+ let(:namespace) { nil }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+ end
+
+ describe '.namespace_for_project' do
+ subject { described_class.namespace_for_project(project) }
+
+ let(:project) { create(:project) }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ describe '#default_namespace' do
+ subject { kubernetes.default_namespace }
+
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
+
+ context 'when cluster belongs to a project' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ context 'when cluster belongs to nothing' do
+ let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb
new file mode 100644
index 00000000000..7d75d6ab345
--- /dev/null
+++ b/spec/models/clusters/project_spec.rb
@@ -0,0 +1,6 @@
+require 'spec_helper'
+
+describe Clusters::Project do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to belong_to(:project) }
+end
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
new file mode 100644
index 00000000000..b38b5e6bcad
--- /dev/null
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -0,0 +1,183 @@
+require 'spec_helper'
+
+describe Clusters::Providers::Gcp do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:zone) }
+
+ describe 'default_value_for' do
+ let(:gcp) { build(:cluster_provider_gcp) }
+
+ it "has default value" do
+ expect(gcp.zone).to eq('us-central1-a')
+ expect(gcp.num_nodes).to eq(3)
+ expect(gcp.machine_type).to eq('n1-standard-2')
+ end
+ end
+
+ describe 'validation' do
+ subject { gcp.valid? }
+
+ context 'when validates gcp_project_id' do
+ let(:gcp) { build(:cluster_provider_gcp, gcp_project_id: gcp_project_id) }
+
+ context 'when gcp_project_id is shorter than 1' do
+ let(:gcp_project_id) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is longer than 63' do
+ let(:gcp_project_id) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id includes invalid character' do
+ let(:gcp_project_id) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is valid' do
+ let(:gcp_project_id) { 'gcp-project-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates num_nodes' do
+ let(:gcp) { build(:cluster_provider_gcp, num_nodes: num_nodes) }
+
+ context 'when num_nodes is string' do
+ let(:num_nodes) { 'A3' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is nil' do
+ let(:num_nodes) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is smaller than 1' do
+ let(:num_nodes) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is valid' do
+ let(:num_nodes) { 3 }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe '#state_machine' do
+ context 'when any => [:created]' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+
+ before do
+ gcp.make_created
+ end
+
+ it 'nullify access_token and operation_id' do
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp).to be_created
+ end
+ end
+
+ context 'when any => [:creating]' do
+ let(:gcp) { build(:cluster_provider_gcp) }
+
+ context 'when operation_id is present' do
+ let(:operation_id) { 'operation-xxx' }
+
+ before do
+ gcp.make_creating(operation_id)
+ end
+
+ it 'sets operation_id' do
+ expect(gcp.operation_id).to eq(operation_id)
+ expect(gcp).to be_creating
+ end
+ end
+
+ context 'when operation_id is nil' do
+ let(:operation_id) { nil }
+
+ it 'raises an error' do
+ expect { gcp.make_creating(operation_id) }
+ .to raise_error('operation_id is required')
+ end
+ end
+ end
+
+ context 'when any => [:errored]' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+ let(:status_reason) { 'err msg' }
+
+ it 'nullify access_token and operation_id' do
+ gcp.make_errored(status_reason)
+
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp.status_reason).to eq(status_reason)
+ expect(gcp).to be_errored
+ end
+
+ context 'when status_reason is nil' do
+ let(:gcp) { build(:cluster_provider_gcp, :errored) }
+
+ it 'does not set status_reason' do
+ gcp.make_errored(nil)
+
+ expect(gcp.status_reason).not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#on_creation?' do
+ subject { gcp.on_creation? }
+
+ context 'when status is creating' do
+ let(:gcp) { create(:cluster_provider_gcp, :creating) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is created' do
+ let(:gcp) { create(:cluster_provider_gcp, :created) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#api_client' do
+ subject { gcp.api_client }
+
+ context 'when status is creating' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+
+ it 'returns Cloud Platform API clinet' do
+ expect(subject).to be_an_instance_of(GoogleApi::CloudPlatform::Client)
+ expect(subject.access_token).to eq(gcp.access_token)
+ end
+ end
+
+ context 'when status is created' do
+ let(:gcp) { build(:cluster_provider_gcp, :created) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when status is errored' do
+ let(:gcp) { build(:cluster_provider_gcp, :errored) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
new file mode 100644
index 00000000000..066fe7d154e
--- /dev/null
+++ b/spec/models/commit_collection_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe CommitCollection do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
+
+ describe '#each' do
+ it 'yields every commit' do
+ collection = described_class.new(project, [commit])
+
+ expect { |b| collection.each(&b) }.to yield_with_args(commit)
+ end
+ end
+
+ describe '#with_pipeline_status' do
+ it 'sets the pipeline status for every commit so no additional queries are necessary' do
+ create(
+ :ci_empty_pipeline,
+ ref: 'master',
+ sha: commit.id,
+ status: 'success',
+ project: project
+ )
+
+ collection = described_class.new(project, [commit])
+ collection.with_pipeline_status
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ expect(commit.status).to eq('success')
+ end
+
+ expect(recorder.count).to be_zero
+ end
+ end
+
+ describe '#respond_to_missing?' do
+ it 'returns true when the underlying Array responds to the message' do
+ collection = described_class.new(project, [])
+
+ expect(collection.respond_to?(:last)).to eq(true)
+ end
+
+ it 'returns false when the underlying Array does not respond to the message' do
+ collection = described_class.new(project, [])
+
+ expect(collection.respond_to?(:foo)).to eq(false)
+ end
+ end
+
+ describe '#method_missing' do
+ it 'delegates undefined methods to the underlying Array' do
+ collection = described_class.new(project, [commit])
+
+ expect(collection.length).to eq(1)
+ expect(collection.last).to eq(commit)
+ expect(collection).not_to be_empty
+ end
+ end
+end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index e3cfa149e3a..d18a5c9dfa6 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -351,12 +351,19 @@ eos
end
it 'gives compound status from latest pipelines if ref is nil' do
- expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
- expect(commit.status(nil)).to eq('failed')
+ expect(commit.status(nil)).to eq(pipeline_from_fix.status)
end
end
end
+ describe '#set_status_for_ref' do
+ it 'sets the status for a given reference' do
+ commit.set_status_for_ref('master', 'failed')
+
+ expect(commit.status('master')).to eq('failed')
+ end
+ end
+
describe '#participants' do
let(:user1) { build(:user) }
let(:user2) { build(:user) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 858ec831200..c536dab2681 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe CommitStatus do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
- let(:pipeline) do
+ set(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit.id)
end
@@ -464,4 +464,73 @@ describe CommitStatus do
it { is_expected.to be_script_failure }
end
end
+
+ describe 'ensure stage assignment' do
+ context 'when commit status has a stage_id assigned' do
+ let!(:stage) do
+ create(:ci_stage_entity, project: project, pipeline: pipeline)
+ end
+
+ let(:commit_status) do
+ create(:commit_status, stage_id: stage.id, name: 'rspec', stage: 'test')
+ end
+
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).to eq stage.id
+ end
+ end
+
+ context 'when commit status does not have a stage_id assigned' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', status: :success)
+ end
+
+ let(:stage) { Ci::Stage.first }
+
+ it 'creates a new stage' do
+ expect { commit_status }.to change { Ci::Stage.count }.by(1)
+
+ expect(stage.name).to eq 'test'
+ expect(stage.project).to eq commit_status.project
+ expect(stage.pipeline).to eq commit_status.pipeline
+ expect(stage.status).to eq commit_status.status
+ expect(commit_status.stage_id).to eq stage.id
+ end
+ end
+
+ context 'when commit status does not have stage but it exists' do
+ let!(:stage) do
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ name: 'test')
+ end
+
+ let(:commit_status) do
+ create(:commit_status, project: project,
+ pipeline: pipeline,
+ name: 'rspec',
+ stage: 'test',
+ status: :success)
+ end
+
+ it 'uses existing stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+
+ expect(commit_status.stage_id).to eq stage.id
+ expect(stage.reload.status).to eq commit_status.status
+ end
+ end
+
+ context 'when commit status is being imported' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', importing: true)
+ end
+
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).not_to be_present
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
new file mode 100644
index 00000000000..cbdc438be0b
--- /dev/null
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Avatarable do
+ subject { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+
+ let(:gitlab_host) { "https://gitlab.example.com" }
+ let(:relative_url_root) { "/gitlab" }
+ let(:asset_host) { "https://gitlab-assets.example.com" }
+
+ before do
+ stub_config_setting(base_url: gitlab_host)
+ stub_config_setting(relative_url_root: relative_url_root)
+ end
+
+ describe '#avatar_path' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:has_asset_host, :visibility_level, :only_path, :avatar_path) do
+ true | Project::PRIVATE | true | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::INTERNAL | true | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::PUBLIC | true | [subject.avatar.url]
+ true | Project::PUBLIC | false | [asset_host, subject.avatar.url]
+ false | Project::PRIVATE | true | [relative_url_root, subject.avatar.url]
+ false | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ false | Project::INTERNAL | true | [relative_url_root, subject.avatar.url]
+ false | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ false | Project::PUBLIC | true | [relative_url_root, subject.avatar.url]
+ false | Project::PUBLIC | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ end
+
+ with_them do
+ before do
+ allow(ActionController::Base).to receive(:asset_host).and_return(has_asset_host ? asset_host : nil)
+ subject.visibility_level = visibility_level
+ end
+
+ it 'returns the expected avatar path' do
+ expect(subject.avatar_path(only_path: only_path)).to eq(avatar_path.join)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb
index dba9fe43327..b70f2331a0e 100644
--- a/spec/models/concerns/ignorable_column_spec.rb
+++ b/spec/models/concerns/ignorable_column_spec.rb
@@ -5,7 +5,11 @@ describe IgnorableColumn do
Class.new do
def self.columns
# This method does not have access to "double"
- [Struct.new(:name).new('id'), Struct.new(:name).new('title')]
+ [
+ Struct.new(:name).new('id'),
+ Struct.new(:name).new('title'),
+ Struct.new(:name).new('date')
+ ]
end
end
end
@@ -18,7 +22,7 @@ describe IgnorableColumn do
describe '.columns' do
it 'returns the columns, excluding the ignored ones' do
- model.ignore_column(:title)
+ model.ignore_column(:title, :date)
expect(model.columns.map(&:name)).to eq(%w(id))
end
@@ -30,9 +34,9 @@ describe IgnorableColumn do
end
it 'returns the names of the ignored columns' do
- model.ignore_column(:title)
+ model.ignore_column(:title, :date)
- expect(model.ignored_columns).to eq(Set.new(%w(title)))
+ expect(model.ignored_columns).to eq(Set.new(%w(title date)))
end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index ba57301a3c9..4dfbb14952e 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -265,25 +265,44 @@ describe Issuable do
end
describe '#to_hook_data' do
+ let(:builder) { double }
+
context 'labels are updated' do
let(:labels) { create_list(:label, 2) }
before do
issue.update(labels: [labels[1]])
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(issue).and_return(builder)
end
it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
- builder = double
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]]
+ ))
+ issue.to_hook_data(user, old_labels: [labels[0]])
+ end
+ end
+
+ context 'total_time_spent is updated' do
+ before do
+ issue.spend_time(duration: 2, user: user, spent_at: Time.now)
+ issue.save
expect(Gitlab::HookData::IssuableBuilder)
.to receive(:new).with(issue).and_return(builder)
+ end
+
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]]
+ 'total_time_spent' => [1, 2]
))
- issue.to_hook_data(user, old_labels: [labels[0]])
+ issue.to_hook_data(user, old_total_time_spent: 1)
end
end
@@ -292,13 +311,11 @@ describe Issuable do
before do
issue.assignees << user << user2
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(issue).and_return(builder)
end
it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
- builder = double
-
- expect(Gitlab::HookData::IssuableBuilder)
- .to receive(:new).with(issue).and_return(builder)
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
@@ -316,13 +333,11 @@ describe Issuable do
before do
merge_request.update(assignee: user)
merge_request.update(assignee: user2)
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(merge_request).and_return(builder)
end
it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
- builder = double
-
- expect(Gitlab::HookData::IssuableBuilder)
- .to receive(:new).with(merge_request).and_return(builder)
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 66353935427..9048da0c73d 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -186,4 +186,21 @@ describe Milestone, 'Milestoneish' do
expect(milestone.elapsed_days).to eq(2)
end
end
+
+ describe '#total_issue_time_spent' do
+ it 'calculates total issue time spent' do
+ closed_issue_1.spend_time(duration: 300, user: author)
+ closed_issue_1.save!
+ closed_issue_2.spend_time(duration: 600, user: assignee)
+ closed_issue_2.save!
+
+ expect(milestone.total_issue_time_spent).to eq(900)
+ end
+ end
+
+ describe '#human_total_issue_time_spent' do
+ it 'returns nil if no time has been spent' do
+ expect(milestone.human_total_issue_time_spent).to be_nil
+ end
+ end
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index ab8773b7ede..3106207811a 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -134,6 +134,7 @@ describe Group, 'Routable' do
context 'with RequestStore active', :request_store do
it 'does not load the route table more than once' do
+ group.expires_full_path_cache
expect(group).to receive(:uncached_full_path).once.and_call_original
3.times { group.full_path }
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index 28ff8158e0e..45dfb136aea 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -6,6 +6,12 @@ describe Subscribable, 'Subscribable' do
let(:user_1) { create(:user) }
describe '#subscribed?' do
+ context 'without user' do
+ it 'returns false' do
+ expect(resource.subscribed?(nil, project)).to be_falsey
+ end
+ end
+
context 'without project' do
it 'returns false when no subscription exists' do
expect(resource.subscribed?(user_1)).to be_falsey
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 882afeccfc6..dfb83578fce 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -12,7 +12,7 @@ shared_examples 'TokenAuthenticatable' do
end
describe User, 'TokenAuthenticatable' do
- let(:token_field) { :authentication_token }
+ let(:token_field) { :rss_token }
it_behaves_like 'TokenAuthenticatable'
describe 'ensures authentication token' do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index da972d2d86a..8389d5c5430 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -283,6 +283,12 @@ describe DiffNote do
expect(diff_line).to be nil
expect(subject).to be_valid
end
+
+ it "does not update the position" do
+ expect(subject).not_to receive(:update_position)
+
+ subject.save
+ end
end
it "returns true for on_image?" do
diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb
index b26de3f3b97..c90b32c5d77 100644
--- a/spec/models/diff_viewer/base_spec.rb
+++ b/spec/models/diff_viewer/base_spec.rb
@@ -32,10 +32,8 @@ describe DiffViewer::Base do
end
context 'when the binaryness does not match' do
- before do
- allow(diff_file.old_blob).to receive(:binary?).and_return(false)
- allow(diff_file.new_blob).to receive(:binary?).and_return(false)
- end
+ let(:commit) { project.commit_by(oid: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('Gemfile.zip') }
it 'returns false' do
expect(viewer_class.can_render?(diff_file)).to be_falsey
@@ -60,8 +58,7 @@ describe DiffViewer::Base do
context 'when the binaryness does not match' do
before do
- allow(diff_file.old_blob).to receive(:binary?).and_return(true)
- allow(diff_file.new_blob).to receive(:binary?).and_return(true)
+ allow_any_instance_of(Blob).to receive(:binary?).and_return(true)
end
it 'returns false' do
@@ -77,12 +74,12 @@ describe DiffViewer::Base do
end
context 'when the file was renamed and only the old blob is supported' do
- let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:commit) { project.commit_by(oid: '2f63565e7aac07bcdadb654e253078b727143ec4') }
let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
before do
allow(diff_file).to receive(:renamed_file?).and_return(true)
- allow(diff_file.new_blob).to receive(:extension).and_return('jpeg')
+ viewer_class.extensions = %w(notjpg)
end
it 'returns false' do
@@ -94,8 +91,7 @@ describe DiffViewer::Base do
describe '#collapsed?' do
context 'when the combined blob size is larger than the collapse limit' do
before do
- allow(diff_file.old_blob).to receive(:raw_size).and_return(512.kilobytes)
- allow(diff_file.new_blob).to receive(:raw_size).and_return(513.kilobytes)
+ allow(diff_file).to receive(:raw_size).and_return(1025.kilobytes)
end
it 'returns true' do
@@ -113,8 +109,7 @@ describe DiffViewer::Base do
describe '#too_large?' do
context 'when the combined blob size is larger than the size limit' do
before do
- allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes)
- allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes)
+ allow(diff_file).to receive(:raw_size).and_return(6.megabytes)
end
it 'returns true' do
@@ -132,8 +127,7 @@ describe DiffViewer::Base do
describe '#render_error' do
context 'when the combined blob size is larger than the size limit' do
before do
- allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes)
- allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes)
+ allow(diff_file).to receive(:raw_size).and_return(6.megabytes)
end
it 'returns :too_large' do
diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb
index 92e613f92de..98a8f6d4cc9 100644
--- a/spec/models/diff_viewer/server_side_spec.rb
+++ b/spec/models/diff_viewer/server_side_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe DiffViewer::ServerSide do
- let(:project) { create(:project, :repository) }
- let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
- let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') }
+ set(:project) { create(:project, :repository) }
+ let(:commit) { project.commit_by(oid: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+ let!(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') }
let(:viewer_class) do
Class.new(DiffViewer::Base) do
@@ -15,8 +15,7 @@ describe DiffViewer::ServerSide do
describe '#prepare!' do
it 'loads all diff file data' do
- expect(diff_file.old_blob).to receive(:load_all_data!)
- expect(diff_file.new_blob).to receive(:load_all_data!)
+ expect(Blob).to receive(:lazy).at_least(:twice)
subject.prepare!
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index e1be23541e8..1ce1d595c60 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -18,7 +18,6 @@ describe Environment do
it { is_expected.to validate_length_of(:slug).is_at_most(24) }
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
- it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
describe '.order_by_last_deployed_at' do
let(:project) { create(:project, :repository) }
@@ -547,6 +546,15 @@ describe Environment do
expect(environment.slug).to eq(original_slug)
end
+
+ it "regenerates the slug if nil" do
+ environment = build(:environment, slug: nil)
+
+ new_slug = environment.slug
+
+ expect(new_slug).not_to be_nil
+ expect(environment.slug).to eq(new_slug)
+ end
end
describe '#generate_slug' do
@@ -583,6 +591,12 @@ describe Environment do
it 'returns a path that uses the slug and does not have spaces' do
expect(environment.ref_path).to start_with('refs/environments/staging-review-1-')
end
+
+ it "doesn't change when the slug is nil initially" do
+ environment.slug = nil
+
+ expect(environment.ref_path).to eq(environment.ref_path)
+ end
end
describe '#external_url_for' do
diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb
index 532ca1fca8c..25bf596fddc 100644
--- a/spec/models/fork_network_member_spec.rb
+++ b/spec/models/fork_network_member_spec.rb
@@ -5,4 +5,22 @@ describe ForkNetworkMember do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:fork_network) }
end
+
+ describe 'destroying a ForkNetworkMember' do
+ let(:fork_network_member) { create(:fork_network_member) }
+ let(:fork_network) { fork_network_member.fork_network }
+
+ it 'removes the fork network if it was the last member' do
+ fork_network.fork_network_members.destroy_all
+
+ expect(ForkNetwork.count).to eq(0)
+ end
+
+ it 'does not destroy the fork network if there are members left' do
+ fork_network_member.destroy!
+
+ # The root of the fork network is left
+ expect(ForkNetwork.count).to eq(1)
+ end
+ end
end
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
index 605ccd6db06..a43baf1820a 100644
--- a/spec/models/fork_network_spec.rb
+++ b/spec/models/fork_network_spec.rb
@@ -24,6 +24,16 @@ describe ForkNetwork do
end
end
+ describe '#merge_requests' do
+ it 'finds merge requests within the fork network' do
+ project = create(:project)
+ forked_project = fork_project(project)
+ merge_request = create(:merge_request, source_project: forked_project, target_project: project)
+
+ expect(project.fork_network.merge_requests).to include(merge_request)
+ end
+ end
+
context 'for a deleted project' do
it 'keeps the fork network' do
project = create(:project, :public)
diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb
deleted file mode 100644
index 8f39fff6394..00000000000
--- a/spec/models/gcp/cluster_spec.rb
+++ /dev/null
@@ -1,264 +0,0 @@
-require 'spec_helper'
-
-describe Gcp::Cluster do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:service) }
-
- it { is_expected.to validate_presence_of(:gcp_cluster_zone) }
-
- describe '.enabled' do
- subject { described_class.enabled }
-
- let!(:cluster) { create(:gcp_cluster, enabled: true) }
-
- before do
- create(:gcp_cluster, enabled: false)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
- describe '.disabled' do
- subject { described_class.disabled }
-
- let!(:cluster) { create(:gcp_cluster, enabled: false) }
-
- before do
- create(:gcp_cluster, enabled: true)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
- describe '#default_value_for' do
- let(:cluster) { described_class.new }
-
- it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') }
- it { expect(cluster.gcp_cluster_size).to eq(3) }
- it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') }
- end
-
- describe '#validates' do
- subject { cluster.valid? }
-
- context 'when validates gcp_project_id' do
- let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) }
-
- context 'when valid' do
- let(:gcp_project_id) { 'gcp-project-12345' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_project_id) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_project_id) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_project_id) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_name' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) }
-
- context 'when valid' do
- let(:gcp_cluster_name) { 'test-cluster' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_cluster_name) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_cluster_name) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_cluster_name) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_size' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) }
-
- context 'when valid' do
- let(:gcp_cluster_size) { 1 }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when zero' do
- let(:gcp_cluster_size) { 0 }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates project_namespace' do
- let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) }
-
- context 'when valid' do
- let(:project_namespace) { 'default-namespace' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:project_namespace) { '' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when too long' do
- let(:project_namespace) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:project_namespace) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates restrict_modification' do
- let(:cluster) { create(:gcp_cluster) }
-
- before do
- cluster.make_creating!
- end
-
- context 'when created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when creating' do
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#state_machine' do
- let(:cluster) { build(:gcp_cluster) }
-
- context 'when transits to created state' do
- before do
- cluster.gcp_token = 'tmp'
- cluster.gcp_operation_id = 'tmp'
- cluster.make_created!
- end
-
- it 'nullify gcp_token and gcp_operation_id' do
- expect(cluster.gcp_token).to be_nil
- expect(cluster.gcp_operation_id).to be_nil
- expect(cluster).to be_created
- end
- end
-
- context 'when transits to errored state' do
- let(:reason) { 'something wrong' }
-
- before do
- cluster.make_errored!(reason)
- end
-
- it 'sets status_reason' do
- expect(cluster.status_reason).to eq(reason)
- expect(cluster).to be_errored
- end
- end
- end
-
- describe '#project_namespace_placeholder' do
- subject { cluster.project_namespace_placeholder }
-
- let(:cluster) { create(:gcp_cluster) }
-
- it 'returns a placeholder' do
- is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}")
- end
- end
-
- describe '#on_creation?' do
- subject { cluster.on_creation? }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#api_url' do
- subject { cluster.api_url }
-
- let(:cluster) { create(:gcp_cluster, :created_on_gke) }
- let(:api_url) { 'https://' + cluster.endpoint }
-
- it { is_expected.to eq(api_url) }
- end
-
- describe '#restrict_modification' do
- subject { cluster.restrict_modification }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_falsey }
-
- it 'sets error' do
- is_expected.to be_falsey
- expect(cluster.errors).not_to be_empty
- end
- end
- end
-end
diff --git a/spec/models/group_custom_attribute_spec.rb b/spec/models/group_custom_attribute_spec.rb
new file mode 100644
index 00000000000..7ecb2022567
--- /dev/null
+++ b/spec/models/group_custom_attribute_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe GroupCustomAttribute do
+ describe 'assocations' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ subject { build :group_custom_attribute }
+
+ it { is_expected.to validate_presence_of(:group) }
+ it { is_expected.to validate_presence_of(:key) }
+ it { is_expected.to validate_presence_of(:value) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id) }
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index f36d6eeb327..5e82a2988ce 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -17,6 +17,7 @@ describe Group do
it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_one(:chat_team) }
+ it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -64,12 +65,6 @@ describe Group do
expect(group).not_to be_valid
end
-
- it 'rejects reserved group paths' do
- group = build(:group, path: 'activity', parent: create(:group))
-
- expect(group).not_to be_valid
- end
end
describe '#visibility_level_allowed_by_parent' do
@@ -252,8 +247,6 @@ describe Group do
describe '#avatar_url' do
let!(:group) { create(:group, :access_requestable, :with_avatar) }
let(:user) { create(:user) }
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/-/system/group/avatar/#{group.id}/dk.png" }
context 'when avatar file is uploaded' do
before do
@@ -261,12 +254,8 @@ describe Group do
end
it 'shows correct avatar url' do
- expect(group.avatar_url).to eq(avatar_path)
- expect(group.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(group.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(group.avatar_url).to eq(group.avatar.url)
+ expect(group.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, group.avatar.url].join)
end
end
end
@@ -488,6 +477,47 @@ describe Group do
end
end
+ describe '#path_changed_hook' do
+ let(:system_hook_service) { SystemHooksService.new }
+
+ context 'for a new group' do
+ let(:group) { build(:group) }
+
+ before do
+ expect(group).to receive(:system_hook_service).and_return(system_hook_service)
+ end
+
+ it 'does not trigger system hook' do
+ expect(system_hook_service).to receive(:execute_hooks_for).with(group, :create)
+
+ group.save!
+ end
+ end
+
+ context 'for an existing group' do
+ let(:group) { create(:group, path: 'old-path') }
+
+ context 'when the path is changed' do
+ let(:new_path) { 'very-new-path' }
+
+ it 'triggers the rename system hook' do
+ expect(group).to receive(:system_hook_service).and_return(system_hook_service)
+ expect(system_hook_service).to receive(:execute_hooks_for).with(group, :rename)
+
+ group.update_attributes!(path: new_path)
+ end
+ end
+
+ context 'when the path is not changed' do
+ it 'does not trigger system hook' do
+ expect(group).not_to receive(:system_hook_service)
+
+ group.update_attributes!(name: 'new name')
+ end
+ end
+ end
+ end
+
describe '#secret_variables_for' do
let(:project) { create(:project, group: group) }
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index 4ca6556d0f4..a45a6088831 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-RSpec.describe Identity do
+describe Identity do
describe 'relations' do
it { is_expected.to belong_to(:user) }
end
@@ -22,4 +22,26 @@ RSpec.describe Identity do
expect(other_identity.ldap?).to be_falsey
end
end
+
+ describe '.with_extern_uid' do
+ context 'LDAP identity' do
+ let!(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com') }
+
+ it 'finds the identity when the DN is formatted differently' do
+ identity = described_class.with_extern_uid('ldapmain', 'uid=John Smith, ou=People, dc=example, dc=com').first
+
+ expect(identity).to eq(ldap_identity)
+ end
+ end
+
+ context 'any other provider' do
+ let!(:test_entity) { create(:identity, provider: 'test_provider', extern_uid: 'test_uid') }
+
+ it 'the extern_uid lookup is case insensitive' do
+ identity = described_class.with_extern_uid('test_provider', 'TEST_UID').first
+
+ expect(identity).to eq(test_entity)
+ end
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index bb5033c1628..5f901262598 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -765,22 +765,4 @@ describe Issue do
expect(described_class.public_only).to eq([public_issue])
end
end
-
- describe '#update_project_counter_caches?' do
- it 'returns true when the state changes' do
- subject.state = 'closed'
-
- expect(subject.update_project_counter_caches?).to eq(true)
- end
-
- it 'returns true when the confidential flag changes' do
- subject.confidential = true
-
- expect(subject.update_project_counter_caches?).to eq(true)
- end
-
- it 'returns false when the state or confidential flag did not change' do
- expect(subject.update_project_counter_caches?).to eq(false)
- end
- end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 81c2057e175..4cd9e3f4f1d 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -166,4 +166,27 @@ describe Key, :mailer do
expect(key.public_key.key_text).to eq(valid_key)
end
end
+
+ describe '#refresh_user_cache', :use_clean_rails_memory_store_caching do
+ context 'when the key belongs to a user' do
+ it 'refreshes the keys count cache for the user' do
+ expect_any_instance_of(Users::KeysCountService)
+ .to receive(:refresh_cache)
+ .and_call_original
+
+ key = create(:personal_key)
+
+ expect(Users::KeysCountService.new(key.user).count).to eq(1)
+ end
+ end
+
+ context 'when the key does not belong to a user' do
+ it 'does nothing' do
+ expect_any_instance_of(Users::KeysCountService)
+ .not_to receive(:refresh_cache)
+
+ create(:key)
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 9d4a0ecf8c0..7709cf43200 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -2,14 +2,93 @@ require 'rails_helper'
describe MergeRequestDiffCommit do
let(:merge_request) { create(:merge_request) }
- subject { merge_request.commits.first }
+ let(:project) { merge_request.project }
describe '#to_hash' do
+ subject { merge_request.commits.first }
+
it 'returns the same results as Commit#to_hash, except for parent_ids' do
- commit_from_repo = merge_request.project.repository.commit(subject.sha)
+ commit_from_repo = project.repository.commit(subject.sha)
commit_from_repo_hash = commit_from_repo.to_hash.merge(parent_ids: [])
expect(subject.to_hash).to eq(commit_from_repo_hash)
end
end
+
+ describe '.create_bulk' do
+ let(:sha_attribute) { Gitlab::Database::ShaAttribute.new }
+ let(:merge_request_diff_id) { merge_request.merge_request_diff.id }
+ let(:commits) do
+ [
+ project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e'),
+ project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ ]
+ end
+ let(:rows) do
+ [
+ {
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "authored_date": "2014-02-27T10:01:38.000+01:00".to_time,
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T10:01:38.000+01:00".to_time,
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 0,
+ "sha": sha_attribute.type_cast_for_database('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ },
+ {
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "authored_date": "2014-02-27T09:57:31.000+01:00".to_time,
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:57:31.000+01:00".to_time,
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 1,
+ "sha": sha_attribute.type_cast_for_database('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ }
+ ]
+ end
+
+ subject { described_class.create_bulk(merge_request_diff_id, commits) }
+
+ it 'inserts the commits into the database en masse' do
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with(described_class.table_name, rows)
+
+ subject
+ end
+
+ context 'with dates larger than the DB limit' do
+ let(:commits) do
+ # This commit's date is "Sun Aug 17 07:12:55 292278994 +0000"
+ [project.commit('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')]
+ end
+ let(:timestamp) { Time.at((1 << 31) - 1) }
+ let(:rows) do
+ [{
+ "message": "Weird commit date\n",
+ "authored_date": timestamp,
+ "author_name": "Alejandro Rodríguez",
+ "author_email": "alejorro70@gmail.com",
+ "committed_date": timestamp,
+ "committer_name": "Alejandro Rodríguez",
+ "committer_email": "alejorro70@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 0,
+ "sha": sha_attribute.type_cast_for_database('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')
+ }]
+ end
+
+ it 'uses a sanitized date' do
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with(described_class.table_name, rows)
+
+ subject
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 476a2697605..d250ad50713 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1755,39 +1755,12 @@ describe MergeRequest do
end
end
- describe '#fetch_ref' do
- it 'sets "ref_fetched" flag to true' do
- subject.update!(ref_fetched: nil)
+ describe '#fetch_ref!' do
+ it 'fetches the ref correctly' do
+ expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
- subject.fetch_ref
-
- expect(subject.reload.ref_fetched).to be_truthy
- end
- end
-
- describe '#ref_fetched?' do
- it 'does not perform git operation when value is cached' do
- subject.ref_fetched = true
-
- expect_any_instance_of(Repository).not_to receive(:ref_exists?)
- expect(subject.ref_fetched?).to be_truthy
- end
-
- it 'caches the value when ref exists but value is not cached' do
- subject.update!(ref_fetched: nil)
- allow_any_instance_of(Repository).to receive(:ref_exists?)
- .and_return(true)
-
- expect(subject.ref_fetched?).to be_truthy
- expect(subject.reload.ref_fetched).to be_truthy
- end
-
- it 'returns false when ref does not exist' do
- subject.update!(ref_fetched: nil)
- allow_any_instance_of(Repository).to receive(:ref_exists?)
- .and_return(false)
-
- expect(subject.ref_fetched?).to be_falsey
+ subject.fetch_ref!
+ expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
end
end
@@ -1799,16 +1772,4 @@ describe MergeRequest do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
-
- describe '#update_project_counter_caches?' do
- it 'returns true when the state changes' do
- subject.state = 'closed'
-
- expect(subject.update_project_counter_caches?).to eq(true)
- end
-
- it 'returns false when the state did not change' do
- expect(subject.update_project_counter_caches?).to eq(false)
- end
- end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 13e37fffa4e..47f4a792e5c 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -11,7 +11,7 @@ describe Milestone do
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(milestone).not_to be_valid
- expect(milestone.errors[:start_date]).to include("Can't be greater than due date")
+ expect(milestone.errors[:due_date]).to include("must be greater than start date")
end
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1ecb50586c7..6e7e8c4c570 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -231,6 +231,37 @@ describe Note do
end
end
+ describe '#cross_reference?' do
+ it 'falsey for user-generated notes' do
+ note = create(:note, system: false)
+
+ expect(note.cross_reference?).to be_falsy
+ end
+
+ context 'when the note might contain cross references' do
+ SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.each do |type|
+ let(:note) { create(:note, :system) }
+ let!(:metadata) { create(:system_note_metadata, note: note, action: type) }
+
+ it 'delegates to the cross-reference regex' do
+ expect(note).to receive(:matches_cross_reference_regex?).and_return(false)
+
+ note.cross_reference?
+ end
+ end
+ end
+
+ context 'when the note cannot contain cross references' do
+ let(:commit_note) { build(:note, note: 'mentioned in 1312312313 something else.', system: true) }
+ let(:label_note) { build(:note, note: 'added ~2323232323', system: true) }
+
+ it 'scan for a `mentioned in` prefix' do
+ expect(commit_note.cross_reference?).to be_truthy
+ expect(label_note.cross_reference?).to be_falsy
+ end
+ end
+ end
+
describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ')
diff --git a/spec/models/project_custom_attribute_spec.rb b/spec/models/project_custom_attribute_spec.rb
new file mode 100644
index 00000000000..669de5506bc
--- /dev/null
+++ b/spec/models/project_custom_attribute_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe ProjectCustomAttribute do
+ describe 'assocations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ subject { build :project_custom_attribute }
+
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:key) }
+ it { is_expected.to validate_presence_of(:value) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
+ end
+end
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index d37726dc3f1..f7a35fdc88a 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -66,6 +66,19 @@ describe ChatMessage::IssueMessage do
expect(subject.attachments).to be_empty
end
end
+
+ context 'reopen' do
+ before do
+ args[:object_attributes][:action] = 'reopen'
+ args[:object_attributes][:state] = 'opened'
+ end
+
+ it 'returns a message regarding reopening of issues' do
+ expect(subject.pretext)
+ .to eq('[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> opened by Test User (test.user)')
+ expect(subject.attachments).to be_empty
+ end
+ end
end
context 'with markdown' do
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index 5e8e880985e..fabcb142858 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -46,6 +46,7 @@ describe FlowdockService do
@sample_data[:commits].each do |commit|
# One request to Flowdock per new commit
next if commit[:id] == @sample_data[:before]
+
expect(WebMock).to have_requested(:post, @api_url).with(
body: /#{commit[:id]}.*#{project.path}/
).once
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 00de536a18b..1c629155e1e 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -7,7 +7,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:project) { build_stubbed(:kubernetes_project) }
let(:service) { project.kubernetes_service }
- describe "Associations" do
+ describe 'Associations' do
it { is_expected.to belong_to :project }
end
@@ -145,7 +145,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
before do
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
end
context 'with path prefix in api_url' do
@@ -153,7 +153,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it 'tests with the prefix' do
service.api_url = 'https://kubernetes.example.com/prefix'
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb
new file mode 100644
index 00000000000..6acee311700
--- /dev/null
+++ b/spec/models/project_services/packagist_service_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe PackagistService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ let(:project) { create(:project) }
+
+ let(:packagist_server) { 'https://packagist.example.com' }
+ let(:packagist_username) { 'theUser' }
+ let(:packagist_token) { 'verySecret' }
+ let(:packagist_hook_url) do
+ "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}"
+ end
+
+ let(:packagist_params) do
+ {
+ active: true,
+ project: project,
+ properties: {
+ username: packagist_username,
+ token: packagist_token,
+ server: packagist_server
+ }
+ }
+ end
+
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+ let(:packagist_service) { described_class.create(packagist_params) }
+
+ before do
+ stub_request(:post, packagist_hook_url)
+ end
+
+ it 'calls Packagist API' do
+ packagist_service.execute(push_sample_data)
+
+ expect(a_request(:post, packagist_hook_url)).to have_been_made.once
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 74eba7e33f6..f7f19d464d1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -24,6 +24,7 @@ describe Project do
it { is_expected.to have_one(:slack_service) }
it { is_expected.to have_one(:microsoft_teams_service) }
it { is_expected.to have_one(:mattermost_service) }
+ it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
it { is_expected.to have_one(:asana_service) }
it { is_expected.to have_many(:boards) }
@@ -78,6 +79,7 @@ describe Project do
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_one(:cluster) }
+ it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
context 'after initialized' do
it "has a project_feature" do
@@ -275,6 +277,12 @@ describe Project do
expect(project).to be_valid
end
+
+ it 'allows a path ending in a period' do
+ project = build(:project, path: 'foo.')
+
+ expect(project).to be_valid
+ end
end
end
@@ -875,20 +883,14 @@ describe Project do
context 'when avatar file is uploaded' do
let(:project) { create(:project, :public, :with_avatar) }
- let(:avatar_path) { "/uploads/-/system/project/avatar/#{project.id}/dk.png" }
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
it 'shows correct url' do
- expect(project.avatar_url).to eq(avatar_path)
- expect(project.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(project.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(project.avatar_url).to eq(project.avatar.url)
+ expect(project.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, project.avatar.url].join)
end
end
- context 'When avatar file in git' do
+ context 'when avatar file in git' do
before do
allow(project).to receive(:avatar_in_git) { true }
end
@@ -1922,6 +1924,38 @@ describe Project do
expect(forked_project.in_fork_network_of?(other_project)).to be_falsy
end
end
+
+ describe '#fork_source' do
+ let!(:second_fork) { fork_project(forked_project) }
+
+ it 'returns the direct source if it exists' do
+ expect(second_fork.fork_source).to eq(forked_project)
+ end
+
+ it 'returns the root of the fork network when the directs source was deleted' do
+ forked_project.destroy
+
+ expect(second_fork.fork_source).to eq(project)
+ end
+ end
+
+ describe '#lfs_storage_project' do
+ it 'returns self for non-forks' do
+ expect(project.lfs_storage_project).to eq project
+ end
+
+ it 'returns the fork network root for forks' do
+ second_fork = fork_project(forked_project)
+
+ expect(second_fork.lfs_storage_project).to eq project
+ end
+
+ it 'returns self when fork_source is nil' do
+ expect(forked_project).to receive(:fork_source).and_return(nil)
+
+ expect(forked_project.lfs_storage_project).to eq forked_project
+ end
+ end
end
describe '#pushes_since_gc' do
@@ -2452,6 +2486,7 @@ describe Project do
context 'legacy storage' do
let(:project) { create(:project, :repository) }
let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:project_storage) { project.send(:storage) }
before do
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
@@ -2493,7 +2528,7 @@ describe Project do
describe '#hashed_storage?' do
it 'returns false' do
- expect(project.hashed_storage?).to be_falsey
+ expect(project.hashed_storage?(:repository)).to be_falsey
end
end
@@ -2546,6 +2581,30 @@ describe Project do
it { expect { subject }.to raise_error(StandardError) }
end
+
+ context 'gitlab pages' do
+ before do
+ expect(project_storage).to receive(:rename_repo) { true }
+ end
+
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+
+ context 'attachments' do
+ before do
+ expect(project_storage).to receive(:rename_repo) { true }
+ end
+
+ it 'moves uploads folder to new location' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
end
describe '#pages_path' do
@@ -2605,8 +2664,14 @@ describe Project do
end
describe '#hashed_storage?' do
- it 'returns true' do
- expect(project.hashed_storage?).to be_truthy
+ it 'returns true if rolled out' do
+ expect(project.hashed_storage?(:attachments)).to be_truthy
+ end
+
+ it 'returns false when not rolled out yet' do
+ project.storage_version = 1
+
+ expect(project.hashed_storage?(:attachments)).to be_falsey
end
end
@@ -2649,10 +2714,6 @@ describe Project do
.to receive(:execute_hooks_for)
.with(project, :rename)
- expect_any_instance_of(Gitlab::UploadsTransfer)
- .to receive(:rename_project)
- .with('foo', project.path, project.namespace.full_path)
-
expect(project).to receive(:expire_caches_before_rename)
expect(project).to receive(:expires_full_path_cache)
@@ -2673,6 +2734,32 @@ describe Project do
it { expect { subject }.to raise_error(StandardError) }
end
+
+ context 'gitlab pages' do
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+
+ context 'attachments' do
+ it 'keeps uploads folder location unchanged' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).not_to receive(:rename_project)
+
+ project.rename_repo
+ end
+
+ context 'when not rolled out' do
+ let(:project) { create(:project, :repository, storage_version: 1) }
+
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+ end
end
describe '#pages_path' do
@@ -2941,4 +3028,77 @@ describe Project do
end
end
end
+
+ describe '#after_import' do
+ let(:project) { build(:project) }
+
+ it 'runs the correct hooks' do
+ expect(project.repository).to receive(:after_import)
+ expect(project).to receive(:import_finish)
+ expect(project).to receive(:update_project_counter_caches)
+ expect(project).to receive(:remove_import_jid)
+
+ project.after_import
+ end
+ end
+
+ describe '#update_project_counter_caches' do
+ let(:project) { create(:project) }
+
+ it 'updates all project counter caches' do
+ expect_any_instance_of(Projects::OpenIssuesCountService)
+ .to receive(:refresh_cache)
+ .and_call_original
+
+ expect_any_instance_of(Projects::OpenMergeRequestsCountService)
+ .to receive(:refresh_cache)
+ .and_call_original
+
+ project.update_project_counter_caches
+ end
+ end
+
+ describe '#remove_import_jid', :clean_gitlab_redis_cache do
+ let(:project) { }
+
+ context 'without an import JID' do
+ it 'does nothing' do
+ project = create(:project)
+
+ expect(Gitlab::SidekiqStatus)
+ .not_to receive(:unset)
+
+ project.remove_import_jid
+ end
+ end
+
+ context 'with an import JID' do
+ it 'unsets the import JID' do
+ project = create(:project, import_jid: '123')
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:unset)
+ .with('123')
+ .and_call_original
+
+ project.remove_import_jid
+
+ expect(project.import_jid).to be_nil
+ end
+ end
+ end
+
+ describe '#wiki_repository_exists?' do
+ it 'returns true when the wiki repository exists' do
+ project = create(:project, :wiki_repo)
+
+ expect(project.wiki_repository_exists?).to eq(true)
+ end
+
+ it 'returns false when the wiki repository does not exist' do
+ project = create(:project)
+
+ expect(project.wiki_repository_exists?).to eq(false)
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index f10d9383ae2..929086305ba 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -10,6 +10,10 @@ describe ProjectWiki do
subject { project_wiki }
+ it { is_expected.to delegate_method(:empty?).to :pages }
+ it { is_expected.to delegate_method(:repository_storage_path).to :project }
+ it { is_expected.to delegate_method(:hashed_storage?).to :project }
+
describe "#path_with_namespace" do
it "returns the project path with namespace with the .wiki extension" do
expect(subject.path_with_namespace).to eq(project.full_path + '.wiki')
@@ -117,65 +121,74 @@ describe ProjectWiki do
end
describe "#find_page" do
- before do
- create_page("index page", "This is an awesome Gollum Wiki")
- end
+ shared_examples 'finding a wiki page' do
+ before do
+ create_page("index page", "This is an awesome Gollum Wiki")
+ end
- after do
- destroy_page(subject.pages.first.page)
- end
+ after do
+ destroy_page(subject.pages.first.page)
+ end
- it "returns the latest version of the page if it exists" do
- page = subject.find_page("index page")
- expect(page.title).to eq("index page")
- end
+ it "returns the latest version of the page if it exists" do
+ page = subject.find_page("index page")
+ expect(page.title).to eq("index page")
+ end
- it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existant")).to eq(nil)
+ it "returns nil if the page does not exist" do
+ expect(subject.find_page("non-existant")).to eq(nil)
+ end
+
+ it "can find a page by slug" do
+ page = subject.find_page("index-page")
+ expect(page.title).to eq("index page")
+ end
+
+ it "returns a WikiPage instance" do
+ page = subject.find_page("index page")
+ expect(page).to be_a WikiPage
+ end
end
- it "can find a page by slug" do
- page = subject.find_page("index-page")
- expect(page.title).to eq("index page")
+ context 'when Gitaly wiki_find_page is enabled' do
+ it_behaves_like 'finding a wiki page'
end
- it "returns a WikiPage instance" do
- page = subject.find_page("index page")
- expect(page).to be_a WikiPage
+ context 'when Gitaly wiki_find_page is disabled', :skip_gitaly_mock do
+ it_behaves_like 'finding a wiki page'
end
end
describe '#find_file' do
- before do
- file = Gollum::File.new(subject.wiki)
- allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('image.jpg', 'master')
- .and_return(file)
- allow_any_instance_of(Gollum::File)
- .to receive(:mime_type)
- .and_return('image/jpeg')
- allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('non-existant', 'master')
- .and_return(nil)
- end
+ shared_examples 'finding a wiki file' do
+ before do
+ file = File.open(Rails.root.join('spec', 'fixtures', 'dk.png'))
+ subject.wiki # Make sure the wiki repo exists
- after do
- allow_any_instance_of(Gollum::Wiki).to receive(:file).and_call_original
- allow_any_instance_of(Gollum::File).to receive(:mime_type).and_call_original
- end
+ BareRepoOperations.new(subject.repository.path_to_repo).commit_file(file, 'image.png')
+ end
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.jpg')
- expect(file.mime_type).to eq('image/jpeg')
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
+ expect(file.mime_type).to eq('image/png')
+ end
+
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existant')).to eq(nil)
+ end
+
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
+ expect(file).to be_a Gitlab::Git::WikiFile
+ end
end
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existant')).to eq(nil)
+ context 'when Gitaly wiki_find_file is enabled' do
+ it_behaves_like 'finding a wiki file'
end
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.jpg')
- expect(file).to be_a Gitlab::Git::WikiFile
+ context 'when Gitaly wiki_find_file is disabled', :skip_gitaly_mock do
+ it_behaves_like 'finding a wiki file'
end
end
@@ -265,23 +278,33 @@ describe ProjectWiki do
end
describe "#delete_page" do
- before do
- create_page("index", "some content")
- @page = subject.wiki.page(title: "index")
- end
+ shared_examples 'deleting a wiki page' do
+ before do
+ create_page("index", "some content")
+ @page = subject.wiki.page(title: "index")
+ end
- it "deletes the page" do
- subject.delete_page(@page)
- expect(subject.pages.count).to eq(0)
- end
+ it "deletes the page" do
+ subject.delete_page(@page)
+ expect(subject.pages.count).to eq(0)
+ end
- it 'updates project activity' do
- subject.delete_page(@page)
+ it 'updates project activity' do
+ subject.delete_page(@page)
- project.reload
+ project.reload
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ end
+ end
+
+ context 'when Gitaly wiki_delete_page is enabled' do
+ it_behaves_like 'deleting a wiki page'
+ end
+
+ context 'when Gitaly wiki_delete_page is disabled', :skip_gitaly_mock do
+ it_behaves_like 'deleting a wiki page'
end
end
@@ -343,6 +366,6 @@ describe ProjectWiki do
end
def destroy_page(page)
- subject.delete_page(page, commit_details)
+ subject.delete_page(page, "test commit")
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index d7c07676911..e9e6abb0d5f 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1166,6 +1166,31 @@ describe Repository do
end
end
+ describe '#branch_exists?' do
+ it 'uses branch_names' do
+ allow(repository).to receive(:branch_names).and_return(['foobar'])
+
+ expect(repository.branch_exists?('foobar')).to eq(true)
+ expect(repository.branch_exists?('master')).to eq(false)
+ end
+ end
+
+ describe '#branch_names', :use_clean_rails_memory_store_caching do
+ let(:fake_branch_names) { ['foobar'] }
+
+ it 'gets cached across Repository instances' do
+ allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names)
+
+ expect(repository.branch_names).to eq(fake_branch_names)
+
+ fresh_repository = Project.find(project.id).repository
+ expect(fresh_repository.object_id).not_to eq(repository.object_id)
+
+ expect(fresh_repository.raw_repository).not_to receive(:branch_names)
+ expect(fresh_repository.branch_names).to eq(fake_branch_names)
+ end
+ end
+
describe '#update_autocrlf_option' do
describe 'when autocrlf is not already set to :input' do
before do
@@ -2298,4 +2323,24 @@ describe Repository do
project.commit_by(oid: '1' * 40)
end
end
+
+ describe '#raw_repository' do
+ subject { repository.raw_repository }
+
+ it 'returns a Gitlab::Git::Repository representation of the repository' do
+ expect(subject).to be_a(Gitlab::Git::Repository)
+ expect(subject.relative_path).to eq(project.disk_path + '.git')
+ expect(subject.gl_repository).to eq("project-#{project.id}")
+ end
+
+ context 'with a wiki repository' do
+ let(:repository) { project.wiki.repository }
+
+ it 'creates a Gitlab::Git::Repository with the proper attributes' do
+ expect(subject).to be_a(Gitlab::Git::Repository)
+ expect(subject.relative_path).to eq(project.disk_path + '.wiki.git')
+ expect(subject.gl_repository).to eq("wiki-#{project.id}")
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 1c3c9068f12..86647ddf6ce 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -346,7 +346,6 @@ describe User do
describe "Respond to" do
it { is_expected.to respond_to(:admin?) }
it { is_expected.to respond_to(:name) }
- it { is_expected.to respond_to(:private_token) }
it { is_expected.to respond_to(:external?) }
end
@@ -526,14 +525,6 @@ describe User do
end
end
- describe 'authentication token' do
- it "has authentication token" do
- user = create(:user)
-
- expect(user.authentication_token).not_to be_blank
- end
- end
-
describe 'ensure incoming email token' do
it 'has incoming email token' do
user = create(:user)
@@ -651,16 +642,40 @@ describe User do
end
describe 'groups' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
before do
- @user = create :user
- @group = create :group
- @group.add_owner(@user)
+ group.add_owner(user)
end
- it { expect(@user.several_namespaces?).to be_truthy }
- it { expect(@user.authorized_groups).to eq([@group]) }
- it { expect(@user.owned_groups).to eq([@group]) }
- it { expect(@user.namespaces).to match_array([@user.namespace, @group]) }
+ it { expect(user.several_namespaces?).to be_truthy }
+ it { expect(user.authorized_groups).to eq([group]) }
+ it { expect(user.owned_groups).to eq([group]) }
+ it { expect(user.namespaces).to contain_exactly(user.namespace, group) }
+ it { expect(user.manageable_namespaces).to contain_exactly(user.namespace, group) }
+
+ context 'with child groups', :nested_groups do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ describe '#manageable_namespaces' do
+ it 'includes all the namespaces the user can manage' do
+ expect(user.manageable_namespaces).to contain_exactly(user.namespace, group, subgroup)
+ end
+ end
+
+ describe '#manageable_groups' do
+ it 'includes all the namespaces the user can manage' do
+ expect(user.manageable_groups).to contain_exactly(group, subgroup)
+ end
+
+ it 'does not include duplicates if a membership was added for the subgroup' do
+ subgroup.add_owner(user)
+
+ expect(user.manageable_groups).to contain_exactly(group, subgroup)
+ end
+ end
+ end
end
describe 'group multiple owners' do
@@ -797,21 +812,23 @@ describe User do
end
it "creates external user by default" do
- user = build(:user)
+ user = create(:user)
expect(user.external).to be_truthy
+ expect(user.can_create_group).to be_falsey
+ expect(user.projects_limit).to be 0
end
describe 'with default overrides' do
it "creates a non-external user" do
- user = build(:user, external: false)
+ user = create(:user, external: false)
expect(user.external).to be_falsey
end
end
end
- describe '#require_ssh_key?' do
+ describe '#require_ssh_key?', :use_clean_rails_memory_store_caching do
protocol_and_expectation = {
'http' => false,
'ssh' => true,
@@ -826,6 +843,12 @@ describe User do
expect(user.require_ssh_key?).to eq(expected)
end
end
+
+ it 'returns false when the user has 1 or more SSH keys' do
+ key = create(:personal_key)
+
+ expect(key.user.require_ssh_key?).to eq(false)
+ end
end
end
@@ -848,6 +871,19 @@ describe User do
end
end
+ describe '.by_any_email' do
+ it 'returns an ActiveRecord::Relation' do
+ expect(described_class.by_any_email('foo@example.com'))
+ .to be_a_kind_of(ActiveRecord::Relation)
+ end
+
+ it 'returns a relation of users' do
+ user = create(:user)
+
+ expect(described_class.by_any_email(user.email)).to eq([user])
+ end
+ end
+
describe '.search' do
let!(:user) { create(:user, name: 'user', username: 'usern', email: 'email@gmail.com') }
let!(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@gmail.com') }
@@ -1143,16 +1179,9 @@ describe User do
let(:user) { create(:user, :with_avatar) }
context 'when avatar file is uploaded' do
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/-/system/user/avatar/#{user.id}/dk.png" }
-
it 'shows correct avatar url' do
- expect(user.avatar_url).to eq(avatar_path)
- expect(user.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(user.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(user.avatar_url).to eq(user.avatar.url)
+ expect(user.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, user.avatar.url].join)
end
end
end
@@ -2226,6 +2255,42 @@ describe User do
end
end
+ describe '#username_changed_hook' do
+ context 'for a new user' do
+ let(:user) { build(:user) }
+
+ it 'does not trigger system hook' do
+ expect(user).not_to receive(:system_hook_service)
+
+ user.save!
+ end
+ end
+
+ context 'for an existing user' do
+ let(:user) { create(:user, username: 'old-username') }
+
+ context 'when the username is changed' do
+ let(:new_username) { 'very-new-name' }
+
+ it 'triggers the rename system hook' do
+ system_hook_service = SystemHooksService.new
+ expect(system_hook_service).to receive(:execute_hooks_for).with(user, :rename)
+ expect(user).to receive(:system_hook_service).and_return(system_hook_service)
+
+ user.update_attributes!(username: new_username)
+ end
+ end
+
+ context 'when the username is not changed' do
+ it 'does not trigger system hook' do
+ expect(user).not_to receive(:system_hook_service)
+
+ user.update_attributes!(email: 'asdf@asdf.com')
+ end
+ end
+ end
+ end
+
describe '#sync_attribute?' do
let(:user) { described_class.new }
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 1f14d06997e..ea75434e399 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -373,7 +373,7 @@ describe WikiPage do
end
it 'returns commit sha' do
- expect(@page.last_commit_sha).to eq @page.commit.sha
+ expect(@page.last_commit_sha).to eq @page.last_version.sha
end
it 'is changed after page updated' do
@@ -402,7 +402,7 @@ describe WikiPage do
def destroy_page(title)
page = wiki.wiki.page(title: title)
- wiki.delete_page(page, commit_details)
+ wiki.delete_page(page, "test commit")
end
def get_slugs(page_or_dir)
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 8e1bc3d1543..298a9d16425 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -150,5 +150,82 @@ describe Ci::BuildPolicy do
end
end
end
+
+ describe 'rules for erase build' do
+ let(:project) { create(:project, :repository) }
+ let(:build) { create(:ci_build, pipeline: pipeline, ref: 'some-ref', user: owner) }
+
+ context 'when a developer erases a build' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when developers can push to the branch' do
+ before do
+ create(:protected_branch, :developers_can_push,
+ name: build.ref, project: project)
+ end
+
+ context 'when the build was created by the developer' do
+ let(:owner) { user }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+
+ context 'when no one can push or merge to the branch' do
+ let(:owner) { user }
+
+ before do
+ create(:protected_branch, :no_one_can_push, :no_one_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+
+ context 'when a master erases a build' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'when masters can push to the branch' do
+ before do
+ create(:protected_branch, :masters_can_push,
+ name: build.ref, project: project)
+ end
+
+ context 'when the build was created by the master' do
+ let(:owner) { user }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+ end
+
+ context 'when no one can push or merge to the branch' do
+ let(:owner) { user }
+
+ before do
+ create(:protected_branch, :no_one_can_push, :no_one_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+ end
end
end
diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index e213aa3d557..4207f42b07f 100644
--- a/spec/policies/gcp/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-describe Gcp::ClusterPolicy, :models do
- set(:project) { create(:project) }
- set(:cluster) { create(:gcp_cluster, project: project) }
+describe Clusters::ClusterPolicy, :models do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { cluster.project }
let(:user) { create(:user) }
let(:policy) { described_class.new(user, cluster) }
diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 8d86dc31582..48d4f3671c5 100644
--- a/spec/presenters/gcp/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
-describe Gcp::ClusterPresenter do
- let(:project) { create(:project) }
- let(:cluster) { create(:gcp_cluster, project: project) }
+describe Clusters::ClusterPresenter do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
subject(:presenter) do
described_class.new(cluster)
@@ -22,14 +21,14 @@ describe Gcp::ClusterPresenter do
end
it 'forwards missing methods to cluster' do
- expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone)
+ expect(presenter.status).to eq(cluster.status)
end
end
describe '#gke_cluster_url' do
subject { described_class.new(cluster).gke_cluster_url }
- it { is_expected.to include(cluster.gcp_cluster_zone) }
- it { is_expected.to include(cluster.gcp_cluster_name) }
+ it { is_expected.to include(cluster.provider.zone) }
+ it { is_expected.to include(cluster.name) }
end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index de7ce848a31..308134eba72 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -25,7 +25,7 @@ describe 'doorkeeper access' do
end
end
- describe "authorization by private token" do
+ describe "authorization by OAuth token" do
it "returns authentication success" do
get api("/user", user)
expect(response).to have_gitlab_http_status(200)
@@ -39,20 +39,20 @@ describe 'doorkeeper access' do
end
describe "when user is blocked" do
- it "returns authentication error" do
+ it "returns authorization error" do
user.block
get api("/user"), access_token: token.token
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
describe "when user is ldap_blocked" do
- it "returns authentication error" do
+ it "returns authorization error" do
user.ldap_block
get api("/user"), access_token: token.token
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 8ce9fcc80bf..04a658cd6c3 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -427,6 +427,142 @@ describe API::Groups do
end
end
+ describe 'GET /groups/:id/subgroups', :nested_groups do
+ let!(:subgroup1) { create(:group, parent: group1) }
+ let!(:subgroup2) { create(:group, :private, parent: group1) }
+ let!(:subgroup3) { create(:group, :private, parent: group2) }
+
+ context 'when unauthenticated' do
+ it 'returns only public subgroups' do
+ get api("/groups/#{group1.id}/subgroups")
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(subgroup1.id)
+ expect(json_response.first['parent_id']).to eq(group1.id)
+ end
+
+ it 'returns 404 for a private group' do
+ get api("/groups/#{group2.id}/subgroups")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated as user' do
+ context 'when user is not member of a public group' do
+ it 'returns no subgroups for the public group' do
+ get api("/groups/#{group1.id}/subgroups", user2)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ context 'when using all_available in request' do
+ it 'returns public subgroups' do
+ get api("/groups/#{group1.id}/subgroups", user2), all_available: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response[0]['id']).to eq(subgroup1.id)
+ expect(json_response[0]['parent_id']).to eq(group1.id)
+ end
+ end
+ end
+
+ context 'when user is not member of a private group' do
+ it 'returns 404 for the private group' do
+ get api("/groups/#{group2.id}/subgroups", user1)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when user is member of public group' do
+ before do
+ group1.add_guest(user2)
+ end
+
+ it 'returns private subgroups' do
+ get api("/groups/#{group1.id}/subgroups", user2)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ private_subgroups = json_response.select { |group| group['visibility'] == 'private' }
+ expect(private_subgroups.length).to eq(1)
+ expect(private_subgroups.first['id']).to eq(subgroup2.id)
+ expect(private_subgroups.first['parent_id']).to eq(group1.id)
+ end
+
+ context 'when using statistics in request' do
+ it 'does not include statistics' do
+ get api("/groups/#{group1.id}/subgroups", user2), statistics: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
+ end
+
+ context 'when user is member of private group' do
+ before do
+ group2.add_guest(user1)
+ end
+
+ it 'returns subgroups' do
+ get api("/groups/#{group2.id}/subgroups", user1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(subgroup3.id)
+ expect(json_response.first['parent_id']).to eq(group2.id)
+ end
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'returns private subgroups of a public group' do
+ get api("/groups/#{group1.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns subgroups of a private group' do
+ get api("/groups/#{group2.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+
+ it 'does not include statistics by default' do
+ get api("/groups/#{group1.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it 'includes statistics if requested' do
+ get api("/groups/#{group1.id}/subgroups", admin), statistics: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include('statistics')
+ end
+ end
+ end
+
describe "POST /groups" do
context "when authenticated as user without group permissions" do
it "does not create group" do
@@ -618,4 +754,14 @@ describe API::Groups do
end
end
end
+
+ it_behaves_like 'custom attributes endpoints', 'groups' do
+ let(:attributable) { group1 }
+ let(:other_attributable) { group2 }
+ let(:user) { user1 }
+
+ before do
+ group2.add_owner(user1)
+ end
+ end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 9f3b5a809d7..0462f494e15 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -11,7 +11,6 @@ describe API::Helpers do
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
- let(:params) { {} }
let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) }
let(:env) do
{
@@ -19,60 +18,35 @@ describe API::Helpers do
'rack.session' => {
_csrf_token: csrf_token
},
- 'REQUEST_METHOD' => 'GET'
+ 'REQUEST_METHOD' => 'GET',
+ 'CONTENT_TYPE' => 'text/plain;charset=utf-8'
}
end
let(:header) { }
+ let(:request) { Grape::Request.new(env)}
+ let(:params) { request.params }
before do
allow_any_instance_of(self.class).to receive(:options).and_return({})
end
- def set_env(user_or_token, identifier)
- clear_env
- clear_param
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
- env[API::Helpers::SUDO_HEADER] = identifier.to_s
- end
-
- def set_param(user_or_token, identifier)
- clear_env
- clear_param
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
- params[API::Helpers::SUDO_PARAM] = identifier.to_s
- end
-
- def clear_env
- env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
- env.delete(API::Helpers::SUDO_HEADER)
- end
-
- def clear_param
- params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
- params.delete(API::Helpers::SUDO_PARAM)
- end
-
def warden_authenticate_returns(value)
warden = double("warden", authenticate: value)
env['warden'] = warden
end
- def doorkeeper_guard_returns(value)
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value }
- end
-
def error!(message, status, header)
raise Exception.new("#{status} - #{message}")
end
+ def set_param(key, value)
+ request.update_param(key, value)
+ end
+
describe ".current_user" do
subject { current_user }
describe "Warden authentication", :allow_forgery_protection do
- before do
- doorkeeper_guard_returns false
- end
-
context "with invalid credentials" do
context "GET request" do
before do
@@ -160,300 +134,53 @@ describe API::Helpers do
end
end
- describe "when authenticating using a user's private token" do
- it "returns a 401 response for an invalid token" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
-
- expect { current_user }.to raise_error /401/
- end
-
- it "returns a 401 response for a user without access" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
-
- expect { current_user }.to raise_error /401/
- end
-
- it 'returns a 401 response for a user who is blocked' do
- user.block!
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
-
- expect { current_user }.to raise_error /401/
- end
-
- it "leaves user as is when sudo not specified" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
-
- expect(current_user).to eq(user)
-
- clear_env
-
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
-
- expect(current_user).to eq(user)
- end
- end
-
describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) }
- before do
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
- end
-
it "returns a 401 response for an invalid token" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/
end
- it "returns a 401 response for a user without access" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ it "returns a 403 response for a user without access" do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error /403/
end
- it 'returns a 401 response for a user who is blocked' do
+ it 'returns a 403 response for a user who is blocked' do
user.block!
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error /403/
end
- it "leaves user as is when sudo not specified" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect(current_user).to eq(user)
- clear_env
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
-
+ it "sets current_user" do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
end
it "does not allow tokens without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
+ expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError
end
it 'does not allow revoked tokens' do
personal_access_token.revoke!
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error API::APIGuard::RevokedError
+ expect { current_user }.to raise_error Gitlab::Auth::RevokedError
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error API::APIGuard::ExpiredError
- end
- end
-
- context 'sudo usage' do
- context 'with admin' do
- context 'with header' do
- context 'with id' do
- it 'changes current_user to sudo' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- end
-
- it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_env(admin, admin.id)
-
- expect(current_user).to eq(admin)
- end
-
- it 'throws an error when user cannot be found' do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
-
- set_env(admin, id)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with username' do
- it 'changes current_user to sudo' do
- set_env(admin, user.username)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_env(admin, admin.username)
-
- expect(current_user).to eq(admin)
- end
-
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
-
- set_env(admin, username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
-
- context 'with param' do
- context 'with id' do
- it 'changes current_user to sudo' do
- set_param(admin, user.id)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_param(admin, admin.id)
-
- expect(current_user).to eq(admin)
- end
-
- it 'handles sudo to oneself using string' do
- set_env(admin, user.id.to_s)
-
- expect(current_user).to eq(user)
- end
-
- it 'throws an error when user cannot be found' do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
-
- set_param(admin, id)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with username' do
- it 'changes current_user to sudo' do
- set_param(admin, user.username)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_param(admin, admin.username)
-
- expect(current_user).to eq(admin)
- end
-
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
-
- set_param(admin, username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
-
- context 'when user is blocked' do
- before do
- user.block!
- end
-
- it 'changes current_user to sudo' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- end
- end
- end
-
- context 'with regular user' do
- context 'with env' do
- it 'changes current_user to sudo when admin and user id' do
- set_env(user, admin.id)
-
- expect { current_user }.to raise_error(Exception)
- end
-
- it 'changes current_user to sudo when admin and user username' do
- set_env(user, admin.username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with params' do
- it 'changes current_user to sudo when admin and user id' do
- set_param(user, admin.id)
-
- expect { current_user }.to raise_error(Exception)
- end
-
- it 'changes current_user to sudo when admin and user username' do
- set_param(user, admin.username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
- end
- end
-
- describe '.sudo?' do
- context 'when no sudo env or param is passed' do
- before do
- doorkeeper_guard_returns(nil)
- end
-
- it 'returns false' do
- expect(sudo?).to be_falsy
- end
- end
-
- context 'when sudo env or param is passed', 'user is not an admin' do
- before do
- set_env(user, '123')
- end
-
- it 'returns an 403 Forbidden' do
- expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
- end
- end
-
- context 'when sudo env or param is passed', 'user is admin' do
- context 'personal access token is used' do
- before do
- personal_access_token = create(:personal_access_token, user: admin)
- set_env(personal_access_token.token, user.id)
- end
-
- it 'returns an 403 Forbidden' do
- expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
- end
- end
-
- context 'private access token is used' do
- before do
- set_env(admin.private_token, user.id)
- end
-
- it 'returns true' do
- expect(sudo?).to be_truthy
- end
+ expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
end
end
end
@@ -582,4 +309,147 @@ describe API::Helpers do
end
end
end
+
+ context 'sudo' do
+ shared_examples 'successful sudo' do
+ it 'sets current_user' do
+ expect(current_user).to eq(user)
+ end
+
+ it 'sets sudo?' do
+ expect(sudo?).to be_truthy
+ end
+ end
+
+ shared_examples 'sudo' do
+ context 'when admin' do
+ before do
+ token.user = admin
+ token.save!
+ end
+
+ context 'when token has sudo scope' do
+ before do
+ token.scopes = %w[sudo]
+ token.save!
+ end
+
+ context 'when user exists' do
+ context 'when using header' do
+ context 'when providing username' do
+ before do
+ env[API::Helpers::SUDO_HEADER] = user.username
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+
+ context 'when providing user ID' do
+ before do
+ env[API::Helpers::SUDO_HEADER] = user.id.to_s
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+ end
+
+ context 'when using param' do
+ context 'when providing username' do
+ before do
+ set_param(API::Helpers::SUDO_PARAM, user.username)
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+
+ context 'when providing user ID' do
+ before do
+ set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+ end
+ end
+
+ context 'when user does not exist' do
+ before do
+ set_param(API::Helpers::SUDO_PARAM, 'nonexistent')
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /User with ID or username 'nonexistent' Not Found/
+ end
+ end
+ end
+
+ context 'when token does not have sudo scope' do
+ before do
+ token.scopes = %w[api]
+ token.save!
+
+ set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError
+ end
+ end
+ end
+
+ context 'when not admin' do
+ before do
+ token.user = user
+ token.save!
+
+ set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /Must be admin to use sudo/
+ end
+ end
+ end
+
+ context 'using an OAuth token' do
+ let(:token) { create(:oauth_access_token) }
+
+ before do
+ env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
+ end
+
+ it_behaves_like 'sudo'
+ end
+
+ context 'using a personal access token' do
+ let(:token) { create(:personal_access_token) }
+
+ context 'passed as param' do
+ before do
+ set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, token.token)
+ end
+
+ it_behaves_like 'sudo'
+ end
+
+ context 'passed as header' do
+ before do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = token.token
+ end
+
+ it_behaves_like 'sudo'
+ end
+ end
+
+ context 'using warden authentication' do
+ before do
+ warden_authenticate_returns admin
+ env[API::Helpers::SUDO_HEADER] = user.username
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /Must be authenticated using an OAuth or Personal Access Token to use sudo/
+ end
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index d919899282d..34ecdd1e164 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -203,18 +203,44 @@ describe API::Internal do
end
context 'with env passed as a JSON' do
- it 'sets env in RequestStore' do
- expect(Gitlab::Git::Env).to receive(:set).with({
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- })
+ context 'when relative path envs are not set' do
+ it 'sets env in RequestStore' do
+ expect(Gitlab::Git::Env).to receive(:set).with({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ })
+
+ push(key, project.wiki, env: {
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
+ }.to_json)
- push(key, project.wiki, env: {
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
- }.to_json)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
- expect(response).to have_gitlab_http_status(200)
+ context 'when relative path envs are set' do
+ it 'sets env in RequestStore' do
+ obj_dir_relative = './objects'
+ alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2']
+ repo_path = project.wiki.repository.path_to_repo
+
+ expect(Gitlab::Git::Env).to receive(:set).with({
+ 'GIT_OBJECT_DIRECTORY' => File.join(repo_path, obj_dir_relative),
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => alt_obj_dirs_relative.map { |d| File.join(repo_path, d) },
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative,
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative
+ })
+
+ push(key, project.wiki, env: {
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
+ GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
+ }.to_json)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 3b7b9c889e7..2a83213e87a 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -165,7 +165,17 @@ describe API::Jobs do
context 'authorized user' do
it 'returns specific job data' do
expect(response).to have_gitlab_http_status(200)
- expect(json_response['name']).to eq('test')
+ expect(json_response['id']).to eq(job.id)
+ expect(json_response['status']).to eq(job.status)
+ expect(json_response['stage']).to eq(job.stage)
+ expect(json_response['name']).to eq(job.name)
+ expect(json_response['ref']).to eq(job.ref)
+ expect(json_response['tag']).to eq(job.tag)
+ expect(json_response['coverage']).to eq(job.coverage)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(job.created_at)
+ expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at)
+ expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at)
+ expect(json_response['duration']).to eq(job.duration)
end
it 'returns pipeline data' do
@@ -490,7 +500,11 @@ describe API::Jobs do
end
describe 'POST /projects/:id/jobs/:job_id/erase' do
+ let(:role) { :master }
+
before do
+ project.team << [user, role]
+
post api("/projects/#{project.id}/jobs/#{job.id}/erase", user)
end
@@ -519,6 +533,23 @@ describe API::Jobs do
expect(response).to have_gitlab_http_status(403)
end
end
+
+ context 'when a developer erases a build' do
+ let(:role) { :developer }
+ let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, user: owner) }
+
+ context 'when the build was created by the developer' do
+ let(:owner) { user }
+
+ it { expect(response).to have_gitlab_http_status(201) }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(response).to have_gitlab_http_status(403) }
+ end
+ end
end
describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 28b1404a4f7..35c6b3bb2fb 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -435,17 +435,7 @@ describe API::MergeRequests do
expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['should_close_merge_request']).to be_falsy
expect(json_response['force_close_merge_request']).to be_falsy
- end
-
- it "returns merge_request" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(merge_request.title)
- expect(json_response['iid']).to eq(merge_request.iid)
- expect(json_response['work_in_progress']).to eq(false)
- expect(json_response['merge_status']).to eq('can_be_merged')
- expect(json_response['should_close_merge_request']).to be_falsy
- expect(json_response['force_close_merge_request']).to be_falsy
+ expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
end
it "returns a 404 error if merge_request_iid not found" do
@@ -462,12 +452,32 @@ describe API::MergeRequests do
context 'Work in Progress' do
let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
- it "returns merge_request" do
+ it "returns merge request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
+
expect(response).to have_gitlab_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
end
end
+
+ context 'when a merge request has more than the changes limit' do
+ it "returns a string indicating that more changes were made" do
+ stub_const('Commit::DIFF_HARD_LIMIT_FILES', 5)
+
+ merge_request_overflow = create(:merge_request, :simple,
+ author: user,
+ assignee: user,
+ source_project: project,
+ source_branch: 'expand-collapse-files',
+ target_project: project,
+ target_branch: 'master')
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request_overflow.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['changes_count']).to eq('5+')
+ end
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
@@ -618,13 +628,11 @@ describe API::MergeRequests do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:forked_project) { fork_project(project, user2) }
+ let!(:forked_project) { fork_project(project, user2, repository: true) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
forked_project.add_reporter(user2)
-
- allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
@@ -1061,6 +1069,30 @@ describe API::MergeRequests do
end
end
+ describe 'POST :id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
+ before do
+ ::MergeRequests::MergeWhenPipelineSucceedsService.new(merge_request.target_project, user).execute(merge_request)
+ end
+
+ it 'removes the merge_when_pipeline_succeeds status' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/cancel_merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post api("/projects/#{project.id}/merge_requests/123/merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the merge request id is used instead of iid' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
describe 'Time tracking' do
let(:issuable) { merge_request }
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index d13b3a958c9..d412b045e9f 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
describe API::PagesDomains do
set(:project) { create(:project) }
set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
@@ -23,12 +24,49 @@ describe API::PagesDomains do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
end
+ describe 'GET /pages/domains' do
+ context 'when pages is disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api('/pages/domains', admin) }
+ end
+ end
+
+ context 'when pages is enabled' do
+ context 'when authenticated as an admin' do
+ it 'returns paginated all pages domains' do
+ get api('/pages/domains', admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.last).to have_key('domain')
+ expect(json_response.last).to have_key('certificate_expiration')
+ expect(json_response.last['certificate_expiration']['expired']).to be true
+ expect(json_response.first).not_to have_key('certificate_expiration')
+ end
+ end
+
+ context 'when authenticated as a non-member' do
+ it_behaves_like '403 response' do
+ let(:request) { get api('/pages/domains', user) }
+ end
+ end
+ end
+ end
+
describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do
it 'returns paginated pages domains' do
get api(route, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domains')
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
@@ -99,6 +137,7 @@ describe API::PagesDomains do
get api(route_domain, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain.domain)
expect(json_response['url']).to eq(pages_domain.url)
expect(json_response['certificate']).to be_nil
@@ -108,6 +147,7 @@ describe API::PagesDomains do
get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain_secure.domain)
expect(json_response['url']).to eq(pages_domain_secure.url)
expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
@@ -118,6 +158,7 @@ describe API::PagesDomains do
get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['certificate']['expired']).to be true
end
end
@@ -187,6 +228,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params[:domain])
expect(pages_domain.certificate).to be_nil
expect(pages_domain.key).to be_nil
@@ -197,6 +239,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params_secure[:domain])
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
@@ -270,6 +313,7 @@ describe API::PagesDomains do
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to be_nil
expect(pages_domain_secure.key).to be_nil
end
@@ -279,6 +323,7 @@ describe API::PagesDomains do
pages_domain.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
end
@@ -288,6 +333,7 @@ describe API::PagesDomains do
pages_domain_expired.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_expired.certificate).to eq(params_secure[:certificate])
expect(pages_domain_expired.key).to eq(params_secure[:key])
end
@@ -297,6 +343,7 @@ describe API::PagesDomains do
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate])
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index e095ba2af5d..a41345da05b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -50,6 +50,12 @@ describe API::Projects do
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
end
+
+ it 'returns the proper security headers' do
+ get api('/projects', current_user), filter
+
+ expect(response).to include_security_headers
+ end
end
shared_examples_for 'projects response without N + 1 queries' do
@@ -431,6 +437,7 @@ describe API::Projects do
project.each_pair do |k, v|
next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
@@ -637,6 +644,7 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(201)
project.each_pair do |k, v|
next if %i[has_external_issue_tracker path].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
end
@@ -1856,4 +1864,9 @@ describe API::Projects do
end
end
end
+
+ it_behaves_like 'custom attributes endpoints', 'projects' do
+ let(:attributable) { project }
+ let(:other_attributable) { project2 }
+ end
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index dfe48e45d49..ba697e2b305 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -175,4 +175,25 @@ describe API::Services do
end
end
end
+
+ describe 'Mattermost service' do
+ let(:service_name) { 'mattermost' }
+ let(:params) do
+ { webhook: 'https://hook.example.com', username: 'username' }
+ end
+
+ before do
+ project.create_mattermost_service(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'accepts a username for update' do
+ put api("/projects/#{project.id}/services/mattermost", user), params.merge(username: 'new_username')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['properties']['username']).to eq('new_username')
+ end
+ end
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
deleted file mode 100644
index 83d09878813..00000000000
--- a/spec/requests/api/session_spec.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'spec_helper'
-
-describe API::Session do
- let(:user) { create(:user) }
-
- describe "POST /session" do
- context "when valid password" do
- it "returns private token" do
- post api("/session"), email: user.email, password: '12345678'
- expect(response).to have_gitlab_http_status(201)
-
- expect(json_response['email']).to eq(user.email)
- expect(json_response['private_token']).to eq(user.private_token)
- expect(json_response['is_admin']).to eq(user.admin?)
- expect(json_response['can_create_project']).to eq(user.can_create_project?)
- expect(json_response['can_create_group']).to eq(user.can_create_group?)
- end
-
- context 'with 2FA enabled' do
- it 'rejects sign in attempts' do
- user = create(:user, :two_factor)
-
- post api('/session'), email: user.email, password: user.password
-
- expect(response).to have_gitlab_http_status(401)
- expect(response.body).to include('You have 2FA enabled.')
- end
- end
- end
-
- context 'when email has case-typo and password is valid' do
- it 'returns private token' do
- post api('/session'), email: user.email.upcase, password: '12345678'
- expect(response.status).to eq 201
-
- expect(json_response['email']).to eq user.email
- expect(json_response['private_token']).to eq user.private_token
- expect(json_response['is_admin']).to eq user.admin?
- expect(json_response['can_create_project']).to eq user.can_create_project?
- expect(json_response['can_create_group']).to eq user.can_create_group?
- end
- end
-
- context 'when login has case-typo and password is valid' do
- it 'returns private token' do
- post api('/session'), login: user.username.upcase, password: '12345678'
- expect(response.status).to eq 201
-
- expect(json_response['email']).to eq user.email
- expect(json_response['private_token']).to eq user.private_token
- expect(json_response['is_admin']).to eq user.admin?
- expect(json_response['can_create_project']).to eq user.can_create_project?
- expect(json_response['can_create_group']).to eq user.can_create_group?
- end
- end
-
- context "when invalid password" do
- it "returns authentication error" do
- post api("/session"), email: user.email, password: '123'
- expect(response).to have_gitlab_http_status(401)
-
- expect(json_response['email']).to be_nil
- expect(json_response['private_token']).to be_nil
- end
- end
-
- context "when empty password" do
- it "returns authentication error with email" do
- post api("/session"), email: user.email
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- it "returns authentication error with username" do
- post api("/session"), email: user.username
-
- expect(response).to have_gitlab_http_status(400)
- end
- end
-
- context "when empty name" do
- it "returns authentication error" do
- post api("/session"), password: user.password
-
- expect(response).to have_gitlab_http_status(400)
- end
- end
-
- context "when user is blocked" do
- it "returns authentication error" do
- user.block
- post api("/session"), email: user.username, password: user.password
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when user is ldap_blocked" do
- it "returns authentication error" do
- user.ldap_block
- post api("/session"), email: user.username, password: user.password
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 4737f034f21..2428e63e149 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -127,8 +127,8 @@ describe API::Users do
context "when admin" do
context 'when sudo is defined' do
it 'does not return 500' do
- admin_personal_access_token = create(:personal_access_token, user: admin).token
- get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin)
+ admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo])
+ get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token)
expect(response).to have_gitlab_http_status(:success)
end
@@ -510,6 +510,14 @@ describe API::Users do
expect(user.reload.notification_email).to eq('new@email.com')
end
+ it 'skips reconfirmation when requested' do
+ put api("/users/#{user.id}", admin), { skip_reconfirmation: true }
+
+ user.reload
+
+ expect(user.confirmed_at).to be_present
+ end
+
it 'updates user with his own username' do
put api("/users/#{user.id}", admin), username: user.username
@@ -1097,14 +1105,6 @@ describe API::Users do
end
end
- context 'with private token' do
- it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{user.private_token}&sudo=123")
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
it 'returns current user without private token when sudo not defined' do
get api("/user", user)
@@ -1139,24 +1139,6 @@ describe API::Users do
expect(json_response['id']).to eq(admin.id)
end
end
-
- context 'with private token' do
- it 'returns sudoed user with private token when sudo defined' do
- get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/user/login')
- expect(json_response['id']).to eq(user.id)
- end
-
- it 'returns initial current user without private token but with is_admin when sudo not defined' do
- get api("/user?private_token=#{admin.private_token}")
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/user/admin')
- expect(json_response['id']).to eq(admin.id)
- end
- end
end
context 'with unauthenticated user' do
@@ -1906,7 +1888,8 @@ describe API::Users do
end
end
- include_examples 'custom attributes endpoints', 'users' do
+ it_behaves_like 'custom attributes endpoints', 'users' do
let(:attributable) { user }
+ let(:other_attributable) { admin }
end
end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 3f58b7ef384..a73bb456b52 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -408,6 +408,8 @@ describe API::V3::Builds do
describe 'POST /projects/:id/builds/:build_id/erase' do
before do
+ project.add_master(user)
+
post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user)
end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index 26251b95680..2e2b9449429 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -314,13 +314,11 @@ describe API::MergeRequests do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:forked_project) { fork_project(project, user2) }
+ let!(:forked_project) { fork_project(project, user2, repository: true) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
forked_project.add_reporter(user2)
-
- allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index f62ad747c73..27288b98d1c 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -404,6 +404,7 @@ describe API::V3::Projects do
project.each_pair do |k, v|
next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
@@ -547,6 +548,7 @@ describe API::V3::Projects do
expect(response).to have_gitlab_http_status(201)
project.each_pair do |k, v|
next if %i[has_external_issue_tracker path].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 52e93e157f1..c597623bc4d 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -654,6 +654,20 @@ describe 'Git LFS API and storage' do
}
end
+ shared_examples 'pushes new LFS objects' do
+ let(:sample_size) { 150.megabytes }
+ let(:sample_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+
+ it 'responds with upload hypermedia link' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first['oid']).to eq(sample_oid)
+ expect(json_response['objects'].first['size']).to eq(sample_size)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
+ end
+ end
+
describe 'when request is authenticated' do
describe 'when user has project push access' do
let(:authorization) { authorize_user }
@@ -684,27 +698,7 @@ describe 'Git LFS API and storage' do
end
context 'when pushing a lfs object that does not exist' do
- let(:body) do
- {
- 'operation' => 'upload',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078 }
- ]
- }
- end
-
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'responds with upload hypermedia link' do
- expect(json_response['objects']).to be_kind_of(Array)
- expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(json_response['objects'].first['size']).to eq(1575078)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
- expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
- end
+ it_behaves_like 'pushes new LFS objects'
end
context 'when pushing one new and one existing lfs object' do
@@ -785,6 +779,17 @@ describe 'Git LFS API and storage' do
end
end
end
+
+ context 'when deploy key has project push access' do
+ let(:key) { create(:deploy_key, can_push: true) }
+ let(:authorization) { authorize_deploy_key }
+
+ let(:update_user_permissions) do
+ project.deploy_keys << key
+ end
+
+ it_behaves_like 'pushes new LFS objects'
+ end
end
context 'when user is not authenticated' do
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 0b1f8ce6f6d..1a5ad9b04e4 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -107,6 +107,15 @@ describe 'OpenID Connect requests' do
end
end
+ # These 2 calls shouldn't actually throw, they should be handled as an
+ # unauthorized request, so we should be able to check the response.
+ #
+ # This was not possible due to an issue with Warden:
+ # https://github.com/hassox/warden/pull/162
+ #
+ # When the patch gets merged and we update Warden, these specs will need to
+ # updated to check the response instead of a raised exception.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/40218
context 'when user is blocked' do
it 'returns authentication error' do
access_grant
@@ -114,7 +123,7 @@ describe 'OpenID Connect requests' do
expect do
request_access_token
- end.to throw_symbol :warden
+ end.to raise_error UncaughtThrowError
end
end
@@ -125,7 +134,7 @@ describe 'OpenID Connect requests' do
expect do
request_access_token
- end.to throw_symbol :warden
+ end.to raise_error UncaughtThrowError
end
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
new file mode 100644
index 00000000000..0fec14d0cce
--- /dev/null
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -0,0 +1,362 @@
+require 'spec_helper'
+
+describe 'Rack Attack global throttles' do
+ let(:settings) { Gitlab::CurrentSettings.current_application_settings }
+
+ # Start with really high limits and override them with low limits to ensure
+ # the right settings are being exercised
+ let(:settings_to_set) do
+ {
+ throttle_unauthenticated_requests_per_period: 100,
+ throttle_unauthenticated_period_in_seconds: 1,
+ throttle_authenticated_api_requests_per_period: 100,
+ throttle_authenticated_api_period_in_seconds: 1,
+ throttle_authenticated_web_requests_per_period: 100,
+ throttle_authenticated_web_period_in_seconds: 1
+ }
+ end
+
+ let(:requests_per_period) { 1 }
+ let(:period_in_seconds) { 10000 }
+ let(:period) { period_in_seconds.seconds }
+
+ let(:url_that_does_not_require_authentication) { '/users/sign_in' }
+ let(:url_that_requires_authentication) { '/dashboard/snippets' }
+ let(:api_partial_url) { '/todos' }
+
+ around do |example|
+ # Instead of test environment's :null_store so the throttles can increment
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
+
+ # Make time-dependent tests deterministic
+ Timecop.freeze { example.run }
+
+ Rack::Attack.cache.store = Rails.cache
+ end
+
+ # Requires let variables:
+ # * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web")
+ # * get_args
+ # * other_user_get_args
+ shared_examples_for 'rate-limited token-authenticated requests' do
+ before do
+ # Set low limits
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get(*get_args) }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ get(*other_user_get_args)
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ describe 'unauthenticated requests' do
+ before do
+ # Set low limits
+ settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:throttle_unauthenticated_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get url_that_does_not_require_authentication }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_does_not_require_authentication }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_does_not_require_authentication }
+ end
+ end
+
+ it 'counts requests from different IPs separately' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ # would be over limit for the same IP
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:throttle_unauthenticated_enabled] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ describe 'API requests authenticated with personal access token', :api do
+ let(:user) { create(:user) }
+ let(:token) { create(:personal_access_token, user: user) }
+ let(:other_user) { create(:user) }
+ let(:other_user_token) { create(:personal_access_token, user: other_user) }
+ let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+
+ context 'with the token in the query string' do
+ let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
+ let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'with the token in the headers' do
+ let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
+ let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+ end
+
+ describe 'API requests authenticated with OAuth token', :api do
+ let(:user) { create(:user) }
+ let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
+ let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") }
+ let(:other_user) { create(:user) }
+ let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) }
+ let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") }
+ let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+
+ context 'with the token in the query string' do
+ let(:get_args) { [api(api_partial_url, oauth_access_token: token)] }
+ let(:other_user_get_args) { [api(api_partial_url, oauth_access_token: other_user_token)] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'with the token in the headers' do
+ let(:get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
+ let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+ end
+
+ describe '"web" (non-API) requests authenticated with RSS token' do
+ let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
+ let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
+
+ context 'with the token in the query string' do
+ let(:get_args) { [rss_url(user), nil] }
+ let(:other_user_get_args) { [rss_url(other_user), nil] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+ end
+
+ describe 'web requests authenticated with regular login' do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+
+ # Set low limits
+ settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:throttle_authenticated_web_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get url_that_requires_authentication }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ login_as(create(:user))
+
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:throttle_authenticated_web_enabled] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ def api_get_args_with_token_headers(partial_url, token_headers)
+ ["/api/#{API::API.version}#{partial_url}", nil, token_headers]
+ end
+
+ def rss_url(user)
+ "/dashboard/projects.atom?rss_token=#{user.rss_token}"
+ end
+
+ def private_token_headers(user)
+ { 'HTTP_PRIVATE_TOKEN' => user.private_token }
+ end
+
+ def personal_access_token_headers(personal_access_token)
+ { 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
+ end
+
+ def oauth_token_headers(oauth_access_token)
+ { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
+ end
+
+ def expect_rejection(&block)
+ yield
+
+ expect(response).to have_http_status(429)
+ end
+end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
new file mode 100644
index 00000000000..71788028cbf
--- /dev/null
+++ b/spec/routing/group_routing_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe "Groups", "routing" do
+ let(:group_path) { 'complex.group-namegit' }
+ let!(:group) { create(:group, path: group_path) }
+
+ it "to #show" do
+ expect(get("/groups/#{group_path}")).to route_to('groups#show', id: group_path)
+ end
+
+ it "also supports nested groups" do
+ nested_group = create(:group, parent: group)
+ expect(get("/#{group_path}/#{nested_group.path}")).to route_to('groups#show', id: "#{group_path}/#{nested_group.path}")
+ end
+
+ it "also display group#show on the short path" do
+ expect(get("/#{group_path}")).to route_to('groups#show', id: group_path)
+ end
+
+ it "to #activity" do
+ expect(get("/groups/#{group_path}/-/activity")).to route_to('groups#activity', id: group_path)
+ end
+
+ it "to #issues" do
+ expect(get("/groups/#{group_path}/-/issues")).to route_to('groups#issues', id: group_path)
+ end
+
+ it "to #members" do
+ expect(get("/groups/#{group_path}/-/group_members")).to route_to('groups/group_members#index', group_id: group_path)
+ end
+
+ it "to #labels" do
+ expect(get("/groups/#{group_path}/-/labels")).to route_to('groups/labels#index', group_id: group_path)
+ end
+
+ it "to #milestones" do
+ expect(get("/groups/#{group_path}/-/milestones")).to route_to('groups/milestones#index', group_id: group_path)
+ end
+
+ describe 'legacy redirection' do
+ describe 'labels' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels", "/groups/complex.group-namegit/-/labels" do
+ let(:resource) { create(:group, parent: group, path: 'labels') }
+ end
+
+ context 'when requesting JSON' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels.json", "/groups/complex.group-namegit/-/labels.json" do
+ let(:resource) { create(:group, parent: group, path: 'labels') }
+ end
+ end
+ end
+
+ describe 'group_members' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/group_members", "/groups/complex.group-namegit/-/group_members" do
+ let(:resource) { create(:group, parent: group, path: 'group_members') }
+ end
+ end
+
+ describe 'avatar' do
+ it 'routes to the avatars controller' do
+ expect(delete("/groups/#{group_path}/-/avatar"))
+ .to route_to(group_id: group_path,
+ controller: 'groups/avatars',
+ action: 'destroy')
+ end
+ end
+
+ describe 'milestones' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones", "/groups/complex.group-namegit/-/milestones" do
+ let(:resource) { create(:group, parent: group, path: 'milestones') }
+ end
+
+ context 'nested routes' do
+ include RSpec::Rails::RequestExampleGroup
+
+ let(:milestone) { create(:milestone, group: group) }
+
+ it 'redirects the nested routes' do
+ request = get("/groups/#{group_path}/milestones/#{milestone.id}/merge_requests")
+ expect(request).to redirect_to("/groups/#{group_path}/-/milestones/#{milestone.id}/merge_requests")
+ end
+ end
+
+ context 'with a query string' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?hello=world", "/groups/complex.group-namegit/-/milestones?hello=world" do
+ let(:resource) { create(:group, parent: group, path: 'milestones') }
+ end
+
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?milestones=/milestones", "/groups/complex.group-namegit/-/milestones?milestones=/milestones" do
+ let(:resource) { create(:group, parent: group, path: 'milestones') }
+ end
+ end
+ end
+
+ describe 'edit' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/edit", "/groups/complex.group-namegit/-/edit" do
+ let(:resource) do
+ pending('still rejected because of the wildcard reserved word')
+ create(:group, parent: group, path: 'edit')
+ end
+ end
+ end
+
+ describe 'issues' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/issues", "/groups/complex.group-namegit/-/issues" do
+ let(:resource) { create(:group, parent: group, path: 'issues') }
+ end
+ end
+
+ describe 'merge_requests' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/merge_requests", "/groups/complex.group-namegit/-/merge_requests" do
+ let(:resource) { create(:group, parent: group, path: 'merge_requests') }
+ end
+ end
+
+ describe 'projects' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/projects", "/groups/complex.group-namegit/-/projects" do
+ let(:resource) { create(:group, parent: group, path: 'projects') }
+ end
+ end
+
+ describe 'activity' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/activity", "/groups/complex.group-namegit/-/activity" do
+ let(:resource) { create(:group, parent: group, path: 'activity') }
+ end
+
+ it_behaves_like 'redirecting a legacy path', "/groups/activity/activity", "/groups/activity/-/activity" do
+ let!(:parent) { create(:group, path: 'activity') }
+ let(:resource) { create(:group, parent: parent, path: 'activity') }
+ end
+ end
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 39d44245c3f..fb1281a6b42 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -426,18 +426,23 @@ describe 'project routing' do
end
end
- # project_milestones GET /:project_id/milestones(.:format) milestones#index
- # POST /:project_id/milestones(.:format) milestones#create
- # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
- # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
- # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
- # PUT /:project_id/milestones/:id(.:format) milestones#update
- # DELETE /:project_id/milestones/:id(.:format) milestones#destroy
+ # project_milestones GET /:project_id/milestones(.:format) milestones#index
+ # POST /:project_id/milestones(.:format) milestones#create
+ # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
+ # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
+ # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
+ # PUT /:project_id/milestones/:id(.:format) milestones#update
+ # DELETE /:project_id/milestones/:id(.:format) milestones#destroy
+ # promote_project_milestone POST /:project_id/milestones/:id/promote milestones#promote
describe Projects::MilestonesController, 'routing' do
it_behaves_like 'RESTful project resources' do
let(:controller) { 'milestones' }
let(:actions) { [:index, :create, :new, :edit, :show, :update] }
end
+
+ it 'to #promote' do
+ expect(post('/gitlab/gitlabhq/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
+ end
end
# project_labels GET /:project_id/labels(.:format) labels#index
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 407d19c3b2a..91aefa84d0e 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -135,7 +135,6 @@ end
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update
# profile_token GET /profile/token(.:format) profile#token
-# profile_reset_private_token PUT /profile/reset_private_token(.:format) profile#reset_private_token
# profile GET /profile(.:format) profile#show
# profile_update PUT /profile/update(.:format) profile#update
describe ProfilesController, "routing" do
@@ -147,10 +146,6 @@ describe ProfilesController, "routing" do
expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
end
- it "to #reset_private_token" do
- expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token')
- end
-
it "to #reset_rss_token" do
expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
end
@@ -262,8 +257,10 @@ describe "Authentication", "routing" do
expect(post("/users/sign_in")).to route_to('sessions#create')
end
- it "DELETE /users/sign_out" do
- expect(delete("/users/sign_out")).to route_to('sessions#destroy')
+ # sign_out with GET instead of DELETE facilitates ad-hoc single-sign-out processes
+ # (https://gitlab.com/gitlab-org/gitlab-ce/issues/39708)
+ it "GET /users/sign_out" do
+ expect(get("/users/sign_out")).to route_to('sessions#destroy')
end
it "POST /users/password" do
@@ -283,36 +280,6 @@ describe "Authentication", "routing" do
end
end
-describe "Groups", "routing" do
- let(:name) { 'complex.group-namegit' }
- let!(:group) { create(:group, name: name) }
-
- it "to #show" do
- expect(get("/groups/#{name}")).to route_to('groups#show', id: name)
- end
-
- it "also supports nested groups" do
- nested_group = create(:group, parent: group)
- expect(get("/#{name}/#{nested_group.name}")).to route_to('groups#show', id: "#{name}/#{nested_group.name}")
- end
-
- it "also display group#show on the short path" do
- expect(get("/#{name}")).to route_to('groups#show', id: name)
- end
-
- it "to #activity" do
- expect(get("/groups/#{name}/activity")).to route_to('groups#activity', id: name)
- end
-
- it "to #issues" do
- expect(get("/groups/#{name}/issues")).to route_to('groups#issues', id: name)
- end
-
- it "to #members" do
- expect(get("/groups/#{name}/group_members")).to route_to('groups/group_members#index', group_id: name)
- end
-end
-
describe HealthCheckController, 'routing' do
it 'to #index' do
expect(get('/health_check')).to route_to('health_check#index')
diff --git a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb
new file mode 100644
index 00000000000..8899dc85384
--- /dev/null
+++ b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/line_break_after_guard_clauses'
+
+describe RuboCop::Cop::LineBreakAfterGuardClauses do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples 'examples with guard clause' do |title|
+ %w[if unless].each do |conditional|
+ it "flags violation for #{title} #{conditional} without line breaks" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses.size).to eq(1)
+ offense = cop.offenses.first
+
+ expect(offense.line).to eq(1)
+ expect(cop.highlights).to eq(["#{title} #{conditional} condition"])
+ expect(offense.message).to eq('Add a line break after guard clauses')
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} with line break" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} on multiple lines without line break" do
+ source = <<~RUBY
+ #{conditional} condition
+ #{title}
+ end
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by end keyword" do
+ source = <<~RUBY
+ def test
+ #{title} #{conditional} condition
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by elsif keyword" do
+ source = <<~RUBY
+ if model
+ #{title} #{conditional} condition
+ elsif
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by else keyword" do
+ source = <<~RUBY
+ if model
+ #{title} #{conditional} condition
+ else
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by when keyword" do
+ source = <<~RUBY
+ case model
+ when condition_a
+ #{title} #{conditional} condition
+ when condition_b
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by rescue keyword" do
+ source = <<~RUBY
+ begin
+ #{title} #{conditional} condition
+ rescue StandardError
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by ensure keyword" do
+ source = <<~RUBY
+ def foo
+ #{title} #{conditional} condition
+ ensure
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by another guard clause" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+ #{title} #{conditional} condition
+
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "autocorrects #{title} #{conditional} guard clauses without line break" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+ do_stuff
+ RUBY
+ autocorrected = autocorrect_source(cop, source)
+
+ expected_source = <<~RUBY
+ #{title} #{conditional} condition
+
+ do_stuff
+ RUBY
+ expect(autocorrected).to eql(expected_source)
+ end
+ end
+ end
+
+ %w[return fail raise next break throw].each do |example|
+ it_behaves_like 'examples with guard clause', example
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
deleted file mode 100644
index 07cb3fc4a2e..00000000000
--- a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/migration/add_column_with_default_to_large_table'
-
-describe RuboCop::Cop::Migration::AddColumnWithDefaultToLargeTable do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in migration' do
- before do
- allow(cop).to receive(:in_migration?).and_return(true)
- end
-
- described_class::LARGE_TABLES.each do |table|
- it "registers an offense for the #{table} table" do
- inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
- end
-
- it 'registers no offense for non-blacklisted tables' do
- inspect_source(cop, "add_column_with_default :table, :column, default: true")
-
- expect(cop.offenses).to be_empty
- end
- end
-
- context 'outside of migration' do
- it 'registers no offense' do
- table = described_class::LARGE_TABLES.sample
- inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
-
- expect(cop.offenses).to be_empty
- end
- end
-end
diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb
new file mode 100644
index 00000000000..17b19e139e4
--- /dev/null
+++ b/spec/rubocop/cop/migration/update_large_table_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/update_large_table'
+
+describe RuboCop::Cop::Migration::UpdateLargeTable do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ shared_examples 'large tables' do |update_method|
+ described_class::LARGE_TABLES.each do |table|
+ it "registers an offense for the #{table} table" do
+ inspect_source(cop, "#{update_method} :#{table}, :column, default: true")
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+ end
+ end
+
+ context 'for the add_column_with_default method' do
+ include_examples 'large tables', 'add_column_with_default'
+ end
+
+ context 'for the update_column_in_batches method' do
+ include_examples 'large tables', 'update_column_in_batches'
+ end
+
+ it 'registers no offense for non-blacklisted tables' do
+ inspect_source(cop, "add_column_with_default :table, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it 'registers no offense for non-blacklisted methods' do
+ table = described_class::LARGE_TABLES.sample
+
+ inspect_source(cop, "some_other_method :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'outside of migration' do
+ let(:table) { described_class::LARGE_TABLES.sample }
+
+ it 'registers no offense for add_column_with_default' do
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it 'registers no offense for update_column_in_batches' do
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
new file mode 100644
index 00000000000..87c7b2ad36e
--- /dev/null
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe ClusterApplicationEntity do
+ describe '#as_json' do
+ let(:application) { build(:cluster_applications_helm) }
+ subject { described_class.new(application).as_json }
+
+ it 'has name' do
+ expect(subject[:name]).to eq(application.name)
+ end
+
+ it 'has status' do
+ expect(subject[:status]).to eq(:not_installable)
+ end
+
+ it 'has no status_reason' do
+ expect(subject[:status_reason]).to be_nil
+ end
+
+ context 'when application is errored' do
+ let(:application) { build(:cluster_applications_helm, :errored) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:errored)
+ expect(subject[:status_reason]).not_to be_nil
+ expect(subject[:status_reason]).to eq(application.status_reason)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 2c7f49974f1..d6a43fd0f00 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -1,22 +1,51 @@
require 'spec_helper'
describe ClusterEntity do
- set(:cluster) { create(:gcp_cluster, :errored) }
- let(:request) { double('request') }
+ describe '#as_json' do
+ subject { described_class.new(cluster).as_json }
- let(:entity) do
- described_class.new(cluster)
- end
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
- describe '#as_json' do
- subject { entity.as_json }
+ context 'when status is creating' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:creating)
+ expect(subject[:status_reason]).to be_nil
+ end
+ end
+
+ context 'when status is errored' do
+ let(:provider) { create(:cluster_provider_gcp, :errored) }
- it 'contains status' do
- expect(subject[:status]).to eq(:errored)
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:errored)
+ expect(subject[:status_reason]).to eq(provider.status_reason)
+ end
+ end
end
- it 'contains status reason' do
- expect(subject[:status_reason]).to eq('general error')
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:created)
+ expect(subject[:status_reason]).to be_nil
+ end
+ end
+
+ context 'when no application has been installed' do
+ let(:cluster) { create(:cluster) }
+ subject { described_class.new(cluster).as_json[:applications]}
+
+ it 'contains helm as not_installable' do
+ expect(subject).not_to be_empty
+
+ helm = subject[0]
+ expect(helm[:name]).to eq('helm')
+ expect(helm[:status]).to eq(:not_installable)
+ end
end
end
end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index 1ac6784d28f..5e9f7a45891 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -1,18 +1,23 @@
require 'spec_helper'
describe ClusterSerializer do
- let(:serializer) do
- described_class.new
- end
-
describe '#represent_status' do
- subject { serializer.represent_status(resource) }
+ subject { described_class.new.represent_status(cluster) }
+
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :errored) }
+
+ it 'serializes only status' do
+ expect(subject.keys).to contain_exactly(:status, :status_reason, :applications)
+ end
+ end
- context 'when represents only status' do
- let(:resource) { create(:gcp_cluster, :errored) }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
it 'serializes only status' do
- expect(subject.keys).to contain_exactly(:status, :status_reason)
+ expect(subject.keys).to contain_exactly(:status, :status_reason, :applications)
end
end
end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
new file mode 100644
index 00000000000..caa3e41402b
--- /dev/null
+++ b/spec/serializers/issue_entity_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe IssueEntity do
+ let(:project) { create(:project) }
+ let(:resource) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user) }
+
+ subject { described_class.new(resource, request: request).as_json }
+
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+end
diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb
new file mode 100644
index 00000000000..75578816e75
--- /dev/null
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe IssueSerializer do
+ let(:resource) { create(:issue) }
+ let(:user) { create(:user) }
+ let(:json_entity) do
+ described_class.new(current_user: user)
+ .represent(resource, serializer: serializer)
+ .with_indifferent_access
+ end
+
+ context 'non-sidebar issue serialization' do
+ let(:serializer) { nil }
+
+ it 'matches issue json schema' do
+ expect(json_entity).to match_schema('entities/issue')
+ end
+ end
+
+ context 'sidebar issue serialization' do
+ let(:serializer) { 'sidebar' }
+
+ it 'matches sidebar issue json schema' do
+ expect(json_entity).to match_schema('entities/issue_sidebar')
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_basic_serializer_spec.rb b/spec/serializers/merge_request_basic_serializer_spec.rb
index 4daf5a59d0c..1fad8e6bc5d 100644
--- a/spec/serializers/merge_request_basic_serializer_spec.rb
+++ b/spec/serializers/merge_request_basic_serializer_spec.rb
@@ -4,9 +4,13 @@ describe MergeRequestBasicSerializer do
let(:resource) { create(:merge_request) }
let(:user) { create(:user) }
- subject { described_class.new.represent(resource) }
+ let(:json_entity) do
+ described_class.new(current_user: user)
+ .represent(resource, serializer: 'basic')
+ .with_indifferent_access
+ end
- it 'has important MergeRequest attributes' do
- expect(subject).to include(:merge_status)
+ it 'matches basic merge request json' do
+ expect(json_entity).to match_schema('entities/merge_request_basic')
end
end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index 87832b3dca1..f9285049c0d 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -30,8 +30,17 @@ describe MergeRequestEntity do
:assign_to_closing)
end
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+
it 'has important MergeRequest attributes' do
- expect(subject).to include(:diff_head_sha, :merge_commit_message,
+ expect(subject).to include(:state, :deleted_at, :diff_head_sha, :merge_commit_message,
:has_conflicts, :has_ci, :merge_path,
:conflict_resolution_path,
:cancel_merge_when_pipeline_succeeds_path,
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index 73fbecc153d..e3abefa6d63 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -9,11 +9,11 @@ describe MergeRequestSerializer do
end
describe '#represent' do
- let(:opts) { { basic: basic } }
- subject { serializer.represent(merge_request, basic: basic) }
+ let(:opts) { { serializer: serializer_entity } }
+ subject { serializer.represent(merge_request, serializer: serializer_entity) }
- context 'when basic param is truthy' do
- let(:basic) { true }
+ context 'when passing basic serializer param' do
+ let(:serializer_entity) { 'basic' }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
@@ -23,8 +23,8 @@ describe MergeRequestSerializer do
end
end
- context 'when basic param is falsy' do
- let(:basic) { false }
+ context 'when serializer param is falsy' do
+ let(:serializer_entity) { nil }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index f60d1843581..45e18086894 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -107,7 +107,7 @@ describe PipelineDetailsEntity do
it 'contains stages' do
expect(subject).to include(:details)
expect(subject[:details]).to include(:stages)
- expect(subject[:details][:stages].first).to include(name: 'external')
+ expect(subject[:details][:stages].first).to include(name: 'test')
end
end
diff --git a/spec/services/applications/create_service_spec.rb b/spec/services/applications/create_service_spec.rb
new file mode 100644
index 00000000000..47a2a9d6403
--- /dev/null
+++ b/spec/services/applications/create_service_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe ::Applications::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:application) }
+ let(:request) { ActionController::TestRequest.new(remote_ip: '127.0.0.1') }
+
+ subject { described_class.new(user, params) }
+
+ it 'creates an application' do
+ expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1)
+ end
+end
diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb
new file mode 100644
index 00000000000..5ec8ed0976d
--- /dev/null
+++ b/spec/services/base_count_service_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe BaseCountService, :use_clean_rails_memory_store_caching do
+ let(:service) { described_class.new }
+
+ describe '#relation_for_count' do
+ it 'raises NotImplementedError' do
+ expect { service.relation_for_count }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#count' do
+ it 'returns the number of values' do
+ expect(service)
+ .to receive(:cache_key)
+ .and_return('foo')
+
+ expect(service)
+ .to receive(:uncached_count)
+ .and_return(5)
+
+ expect(service.count).to eq(5)
+ end
+ end
+
+ describe '#uncached_count' do
+ it 'returns the uncached number of values' do
+ expect(service)
+ .to receive(:relation_for_count)
+ .and_return(double(:relation, count: 5))
+
+ expect(service.uncached_count).to eq(5)
+ end
+ end
+
+ describe '#refresh_cache' do
+ it 'refreshes the cache' do
+ allow(service)
+ .to receive(:cache_key)
+ .and_return('foo')
+
+ allow(service)
+ .to receive(:uncached_count)
+ .and_return(4)
+
+ service.refresh_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to eq(4)
+ end
+ end
+
+ describe '#delete_cache' do
+ it 'deletes the cache' do
+ allow(service)
+ .to receive(:cache_key)
+ .and_return('foo')
+
+ allow(service)
+ .to receive(:uncached_count)
+ .and_return(4)
+
+ service.refresh_cache
+ service.delete_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to be_nil
+ end
+ end
+
+ describe '#raw?' do
+ it 'returns false' do
+ expect(service.raw?).to eq(false)
+ end
+ end
+
+ describe '#cache_key' do
+ it 'raises NotImplementedError' do
+ expect { service.cache_key }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb
deleted file mode 100644
index 6e7398fbffa..00000000000
--- a/spec/services/ci/create_cluster_service_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateClusterService do
- describe '#execute' do
- let(:access_token) { 'xxx' }
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:result) { described_class.new(project, user, params).execute(access_token) }
-
- context 'when correct params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 1
- }
- end
-
- it 'creates a cluster object' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(1)
- expect(result.gcp_project_id).to eq('gcp-project')
- expect(result.gcp_cluster_name).to eq('test-cluster')
- expect(result.gcp_cluster_zone).to eq('us-central1-a')
- expect(result.gcp_cluster_size).to eq(1)
- expect(result.gcp_token).to eq(access_token)
- end
- end
-
- context 'when invalid params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 'ABC'
- }
- end
-
- it 'returns an error' do
- expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(0)
- end
- end
- end
-end
diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb
deleted file mode 100644
index 7792979c5cb..00000000000
--- a/spec/services/ci/fetch_gcp_operation_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-require 'google/apis'
-
-describe Ci::FetchGcpOperationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
-
- context 'when suceeded' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_return(operation)
- end
-
- it 'fetch the gcp operaion' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .to yield_with_args(operation)
- end
- end
-
- context 'when raises an error' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .not_to yield_with_args
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb
deleted file mode 100644
index def3709fdb4..00000000000
--- a/spec/services/ci/finalize_cluster_creation_service_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'spec_helper'
-
-describe Ci::FinalizeClusterCreationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:result) { described_class.new.execute(cluster) }
-
- context 'when suceeded to get cluster from api' do
- let(:gke_cluster) { double }
-
- before do
- allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111')
- allow(gke_cluster).to receive(:master_auth).and_return(spy)
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_return(gke_cluster)
- end
-
- context 'when suceeded to get kubernetes token' do
- let(:kubernetes_token) { 'abc' }
-
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(kubernetes_token)
- end
-
- it 'executes integration cluster' do
- expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute)
- described_class.new.execute(cluster)
- end
- end
-
- context 'when failed to get kubernetes token' do
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(nil)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when failed to get cluster from api' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb
deleted file mode 100644
index 3a79c205bd1..00000000000
--- a/spec/services/ci/integrate_cluster_service_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'spec_helper'
-
-describe Ci::IntegrateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :custom_project_namespace) }
- let(:endpoint) { '123.123.123.123' }
- let(:ca_cert) { 'ca_cert_xxx' }
- let(:token) { 'token_xxx' }
- let(:username) { 'username_xxx' }
- let(:password) { 'password_xxx' }
-
- before do
- described_class
- .new.execute(cluster, endpoint, ca_cert, token, username, password)
-
- cluster.reload
- end
-
- context 'when correct params' do
- it 'creates a cluster object' do
- expect(cluster.endpoint).to eq(endpoint)
- expect(cluster.ca_cert).to eq(ca_cert)
- expect(cluster.kubernetes_token).to eq(token)
- expect(cluster.username).to eq(username)
- expect(cluster.password).to eq(password)
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(token)
- end
- end
-
- context 'when invalid params' do
- let(:endpoint) { nil }
-
- it 'sets an error to cluster object' do
- expect(cluster).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 214adc9960f..0ce41e7c7ee 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -292,6 +292,30 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
+ context 'when there is only one manual action' do
+ before do
+ create_build('deploy', stage_idx: 0, when: 'manual', allow_failure: true)
+
+ process_pipeline
+ end
+
+ it 'skips the pipeline' do
+ expect(pipeline.reload).to be_skipped
+ end
+
+ context 'when the action was played' do
+ before do
+ play_manual_action('deploy')
+ end
+
+ it 'queues the action and pipeline' do
+ expect(all_builds_statuses).to eq(%w[pending])
+
+ expect(pipeline.reload).to be_pending
+ end
+ end
+ end
+
context 'when blocking manual actions are defined' do
before do
create_build('code:test', stage_idx: 0)
diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb
deleted file mode 100644
index 5ce5c788314..00000000000
--- a/spec/services/ci/provision_cluster_service_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ProvisionClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { spy }
-
- shared_examples 'error' do
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
-
- context 'when suceeded to request provision' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_return(operation)
- end
-
- context 'when operation status is RUNNING' do
- before do
- allow(operation).to receive(:status).and_return('RUNNING')
- end
-
- context 'when suceeded to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- context 'when cluster status is scheduled' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- it 'schedules a worker for status minitoring' do
- expect(WaitForClusterCreationWorker).to receive(:perform_in)
-
- described_class.new.execute(cluster)
- end
- end
-
- context 'when cluster status is creating' do
- before do
- cluster.make_creating!
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return(nil)
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when operation status is others' do
- before do
- allow(operation).to receive(:status).and_return('others')
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to request provision' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_raise(error)
- end
-
- it_behaves_like 'error'
- end
- end
-end
diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb
deleted file mode 100644
index a289385b88f..00000000000
--- a/spec/services/ci/update_cluster_service_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'spec_helper'
-
-describe Ci::UpdateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) }
-
- before do
- described_class.new(cluster.project, cluster.user, params).execute(cluster)
-
- cluster.reload
- end
-
- context 'when correct params' do
- context 'when enabled is true' do
- let(:params) { { 'enabled' => 'true' } }
-
- it 'enables cluster and overwrite kubernetes service' do
- expect(cluster.enabled).to be_truthy
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(cluster.ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(cluster.kubernetes_token)
- end
- end
-
- context 'when enabled is false' do
- let(:params) { { 'enabled' => 'false' } }
-
- it 'disables cluster and kubernetes service' do
- expect(cluster.enabled).to be_falsy
- expect(cluster.service.active).to be_falsy
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
new file mode 100644
index 00000000000..75fc05d36e9
--- /dev/null
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -0,0 +1,91 @@
+require 'spec_helper'
+
+describe Clusters::Applications::CheckInstallationProgressService do
+ RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
+
+ let(:application) { create(:cluster_applications_helm, :installing) }
+ let(:service) { described_class.new(application) }
+ let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
+ let(:errors) { nil }
+
+ shared_examples 'a terminated installation' do
+ it 'removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+ end
+
+ shared_examples 'a not yet terminated installation' do |a_phase|
+ let(:phase) { a_phase }
+
+ context "when phase is #{a_phase}" do
+ context 'when not timeouted' do
+ it 'reschedule a new check' do
+ expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
+ expect(service).not_to receive(:remove_installation_pod)
+
+ service.execute
+
+ expect(application).to be_installing
+ expect(application.status_reason).to be_nil
+ end
+ end
+
+ context 'when timeouted' do
+ let(:application) { create(:cluster_applications_helm, :timeouted) }
+
+ it_behaves_like 'a terminated installation'
+
+ it 'make the application errored' do
+ expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_errored
+ expect(application.status_reason).to match(/\btimeouted\b/)
+ end
+ end
+ end
+ end
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+
+ allow(service).to receive(:installation_errors).and_return(errors)
+ allow(service).to receive(:remove_installation_pod).and_return(nil)
+ end
+
+ describe '#execute' do
+ context 'when installation POD succeeded' do
+ let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
+
+ it_behaves_like 'a terminated installation'
+
+ it 'make the application installed' do
+ expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_installed
+ expect(application.status_reason).to be_nil
+ end
+ end
+
+ context 'when installation POD failed' do
+ let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
+ let(:errors) { 'test installation failed' }
+
+ it_behaves_like 'a terminated installation'
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_errored
+ expect(application.status_reason).to eq(errors)
+ end
+ end
+
+ RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
+ end
+end
diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb
new file mode 100644
index 00000000000..054a49ffedf
--- /dev/null
+++ b/spec/services/clusters/applications/install_service_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Clusters::Applications::InstallService do
+ describe '#execute' do
+ let(:application) { create(:cluster_applications_helm, :scheduled) }
+ let(:service) { described_class.new(application) }
+ let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm) }
+
+ before do
+ allow(service).to receive(:helm_api).and_return(helm_client)
+ end
+
+ context 'when there are no errors' do
+ before do
+ expect(helm_client).to receive(:install).with(application.install_command)
+ allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
+ end
+
+ it 'make the application installing' do
+ expect(application.cluster).not_to be_nil
+ service.execute
+
+ expect(application).to be_installing
+ end
+
+ it 'schedule async installation status check' do
+ expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
+
+ service.execute
+ end
+ end
+
+ context 'when k8s cluster communication fails' do
+ before do
+ error = KubeException.new(500, 'system failure', nil)
+ expect(helm_client).to receive(:install).with(application.install_command).and_raise(error)
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_errored
+ expect(application.status_reason).to match(/kubernetes error:/i)
+ end
+ end
+
+ context 'when application cannot be persisted' do
+ let(:application) { build(:cluster_applications_helm, :scheduled) }
+
+ it 'make the application errored' do
+ expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid)
+ expect(helm_client).not_to receive(:install)
+
+ service.execute
+
+ expect(application).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
new file mode 100644
index 00000000000..cf95361c935
--- /dev/null
+++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Clusters::Applications::ScheduleInstallationService do
+ def count_scheduled
+ application_class&.with_status(:scheduled)&.count || 0
+ end
+
+ shared_examples 'a failing service' do
+ it 'raise an exception' do
+ expect(ClusterInstallAppWorker).not_to receive(:perform_async)
+ count_before = count_scheduled
+
+ expect { service.execute }.to raise_error(StandardError)
+ expect(count_scheduled).to eq(count_before)
+ end
+ end
+
+ describe '#execute' do
+ let(:application_class) { Clusters::Applications::Helm }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:service) { described_class.new(project, nil, cluster: cluster, application_class: application_class) }
+
+ it 'creates a new application' do
+ expect { service.execute }.to change { application_class.count }.by(1)
+ end
+
+ it 'make the application scheduled' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application_class.application_name, kind_of(Numeric)).once
+
+ expect { service.execute }.to change { application_class.with_status(:scheduled).count }.by(1)
+ end
+
+ context 'when installation is already in progress' do
+ let(:application) { create(:cluster_applications_helm, :installing) }
+ let(:cluster) { application.cluster }
+
+ it_behaves_like 'a failing service'
+ end
+
+ context 'when application_class is nil' do
+ let(:application_class) { nil }
+
+ it_behaves_like 'a failing service'
+ end
+
+ context 'when application cannot be persisted' do
+ before do
+ expect_any_instance_of(application_class).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
+ end
+
+ it_behaves_like 'a failing service'
+ end
+ end
+end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
new file mode 100644
index 00000000000..5b6edb73beb
--- /dev/null
+++ b/spec/services/clusters/create_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Clusters::CreateService do
+ let(:access_token) { 'xxx' }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ context 'when provider is gcp' do
+ context 'when correct params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
+ }
+ end
+
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { result }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+
+ expect(result.name).to eq('test-cluster')
+ expect(result.user).to eq(user)
+ expect(result.project).to eq(project)
+ expect(result.provider.gcp_project_id).to eq('gcp-project')
+ expect(result.provider.zone).to eq('us-central1-a')
+ expect(result.provider.num_nodes).to eq(1)
+ expect(result.provider.machine_type).to eq('machine_type-a')
+ expect(result.provider.access_token).to eq(access_token)
+ expect(result.platform).to be_nil
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '!!!!!!!',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
+ }
+ end
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Clusters::Cluster.count }.by(0)
+ expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
new file mode 100644
index 00000000000..e2fa93904c5
--- /dev/null
+++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FetchOperationService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'success' do
+ it 'yields' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .to yield_with_args
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .not_to yield_with_args
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch operation' do
+ before do
+ stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
new file mode 100644
index 00000000000..0cf91307589
--- /dev/null
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FinalizeCreationService do
+ include GoogleApi::CloudPlatformHelpers
+ include KubernetesHelpers
+
+ describe '#execute' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:provider) { cluster.provider }
+ let(:platform) { cluster.platform }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:cluster_name) { cluster.name }
+
+ shared_examples 'success' do
+ it 'configures provider and kubernetes' do
+ described_class.new.execute(provider)
+
+ expect(provider).to be_created
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch gke cluster info' do
+ let(:endpoint) { '111.111.111.111' }
+ let(:api_url) { 'https://' + endpoint }
+ let(:username) { 'sample-username' }
+ let(:password) { 'sample-password' }
+
+ before do
+ stub_cloud_platform_get_zone_cluster(
+ gcp_project_id, zone, cluster_name,
+ {
+ endpoint: endpoint,
+ username: username,
+ password: password
+ }
+ )
+
+ stub_kubeclient_discover(api_url)
+ end
+
+ context 'when suceeded to fetch kuberenetes token' do
+ let(:token) { 'sample-token' }
+
+ before do
+ stub_kubeclient_get_secrets(
+ api_url,
+ {
+ token: Base64.encode64(token)
+ } )
+ end
+
+ it_behaves_like 'success'
+
+ it 'has corresponded data' do
+ described_class.new.execute(provider)
+ cluster.reload
+ provider.reload
+ platform.reload
+
+ expect(provider.endpoint).to eq(endpoint)
+ expect(platform.api_url).to eq(api_url)
+ expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
+ expect(platform.username).to eq(username)
+ expect(platform.password).to eq(password)
+ expect(platform.token).to eq(token)
+ end
+ end
+
+ context 'when default-token is not found' do
+ before do
+ stub_kubeclient_get_secrets(api_url, metadata_name: 'aaaa')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when token is empty' do
+ before do
+ stub_kubeclient_get_secrets(api_url, token: '')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to fetch kuberenetes token' do
+ before do
+ stub_kubeclient_get_secrets_error(api_url)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to fetch gke cluster info' do
+ before do
+ stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
new file mode 100644
index 00000000000..f48afdc83b2
--- /dev/null
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::ProvisionService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :scheduled) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+
+ shared_examples 'success' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_creating
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to request provision' do
+ before do
+ stub_cloud_platform_create_cluster(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when selfLink is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "selfLink": 'unexpected'
+ })
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_create_cluster_error(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
new file mode 100644
index 00000000000..2ee2fa51f63
--- /dev/null
+++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::VerifyProvisionStatusService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'continue_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'finalize_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect_any_instance_of(Clusters::Gcp::FinalizeCreationService).to receive(:execute)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when operation status is RUNNING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+
+ context 'when cluster creation time exceeds timeout' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 30.minutes.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when operation status is PENDING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'PENDING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+ end
+
+ context 'when operation status is DONE' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'DONE'
+ } )
+ end
+
+ it_behaves_like 'finalize_creation'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to get operation status' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
new file mode 100644
index 00000000000..2d91a21035d
--- /dev/null
+++ b/spec/services/clusters/update_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Clusters::UpdateService do
+ describe '#execute' do
+ subject { described_class.new(cluster.project, cluster.user, params).execute(cluster) }
+
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ context 'when correct params' do
+ context 'when enabled is true' do
+ let(:params) { { enabled: true } }
+
+ it 'enables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_truthy
+ end
+ end
+
+ context 'when enabled is false' do
+ let(:params) { { enabled: false } }
+
+ it 'disables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_falsy
+ end
+ end
+
+ context 'when namespace is specified' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: 'custom-namespace'
+ }
+ }
+ end
+
+ it 'updates namespace' do
+ is_expected.to eq(true)
+ expect(cluster.platform.namespace).to eq('custom-namespace')
+ end
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: '!!!'
+ }
+ }
+ end
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index 5a9eb359ee1..0de02576203 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -42,6 +42,14 @@ describe DeleteMergedBranchesService do
expect(project.repository.branch_names).to include('improve/awesome')
end
+ it 'ignores protected tags' do
+ create(:protected_tag, project: project, name: 'improve/*')
+
+ service.execute
+
+ expect(project.repository.branch_names).not_to include('improve/awesome')
+ end
+
context 'user without rights' do
let(:user) { create(:user) }
diff --git a/spec/services/events/render_service_spec.rb b/spec/services/events/render_service_spec.rb
new file mode 100644
index 00000000000..b4a4a44d07b
--- /dev/null
+++ b/spec/services/events/render_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Events::RenderService do
+ describe '#execute' do
+ let!(:note) { build(:note) }
+ let!(:event) { build(:event, target: note, project: note.project) }
+ let!(:user) { build(:user) }
+
+ context 'when the request format is atom' do
+ it 'renders the note inside events' do
+ expect(Banzai::ObjectRenderer).to receive(:new)
+ .with(event.project, user,
+ only_path: false,
+ xhtml: true)
+ .and_call_original
+
+ expect_any_instance_of(Banzai::ObjectRenderer)
+ .to receive(:render).with([note], :note)
+
+ described_class.new(user).execute([event], atom_request: true)
+ end
+ end
+
+ context 'when the request format is not atom' do
+ it 'renders the note inside events' do
+ expect(Banzai::ObjectRenderer).to receive(:new)
+ .with(event.project, user, {})
+ .and_call_original
+
+ expect_any_instance_of(Banzai::ObjectRenderer)
+ .to receive(:render).with([note], :note)
+
+ described_class.new(user).execute([event], atom_request: false)
+ end
+ end
+ end
+end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
new file mode 100644
index 00000000000..9f92b662be1
--- /dev/null
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Issuable::CommonSystemNotesService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issuable) { create(:issue) }
+
+ shared_examples 'system note creation' do |update_params, note_text|
+ subject { described_class.new(project, user).execute(issuable, [])}
+
+ before do
+ issuable.assign_attributes(update_params)
+ issuable.save
+ end
+
+ it 'creates 1 system note with the correct content' do
+ expect { subject }.to change { Note.count }.from(0).to(1)
+
+ note = Note.last
+ expect(note.note).to match(note_text)
+ expect(note.noteable_type).to eq('Issue')
+ end
+ end
+
+ describe '#execute' do
+ it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
+ it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
+ it_behaves_like 'system note creation', { discussion_locked: true }, 'locked this issue'
+ it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate'
+
+ context 'when new label is added' do
+ before do
+ label = create(:label, project: project)
+ issuable.labels << label
+ end
+
+ it_behaves_like 'system note creation', {}, /added ~\w+ label/
+ end
+
+ context 'when new milestone is assigned' do
+ before do
+ milestone = create(:milestone, project: project)
+ issuable.milestone_id = milestone.id
+ end
+
+ it_behaves_like 'system note creation', {}, 'changed milestone'
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ac196e92601..f86f1ac2443 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -248,6 +248,28 @@ describe MergeRequests::MergeService do
expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
+
+ context "when fast-forward merge is not allowed" do
+ before do
+ allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
+ end
+
+ %w(semi-linear ff).each do |merge_method|
+ it "logs and saves error if merge is #{merge_method} only" do
+ merge_method = 'rebase_merge' if merge_method == 'semi-linear'
+ merge_request.project.update(merge_method: merge_method)
+ error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch'
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request).to be_open
+ expect(merge_request.merge_commit_sha).to be_nil
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 98409be4236..5ce6ca70c83 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -80,7 +80,7 @@ describe MergeRequests::UpdateService, :mailer do
it 'executes hooks with update action' do
expect(service)
.to have_received(:execute_hooks)
- .with(@merge_request, 'update', old_labels: [], old_assignees: [user3])
+ .with(@merge_request, 'update', old_labels: [], old_assignees: [user3], old_total_time_spent: 0)
end
it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 5739386dd0d..af35e17bfa7 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -4,8 +4,8 @@ describe Milestones::DestroyService do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
- let(:issue) { create(:issue, project: project, milestone: milestone) }
- let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
before do
project.team << [user, :master]
diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb
new file mode 100644
index 00000000000..a0a2843b676
--- /dev/null
+++ b/spec/services/milestones/promote_service_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Milestones::PromoteService do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:user) { create(:user) }
+ let(:milestone_title) { 'project milestone' }
+ let(:milestone) { create(:milestone, project: project, title: milestone_title) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ before do
+ group.add_master(user)
+ end
+
+ context 'validations' do
+ it 'raises error if milestone does not belong to a project' do
+ allow(milestone).to receive(:project_milestone?).and_return(false)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+ end
+
+ it 'raises error if project does not belong to a group' do
+ project.update(namespace: user.namespace)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+ end
+
+ it 'does not promote milestone and update issuables if promoted milestone is not valid' do
+ issue = create(:issue, milestone: milestone, project: project)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+ allow_any_instance_of(Milestone).to receive(:valid?).and_return(false)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+
+ expect(milestone.reload).to be_persisted
+ expect(issue.reload.milestone).to eq(milestone)
+ expect(merge_request.reload.milestone).to eq(milestone)
+ end
+ end
+
+ context 'without duplicated milestone titles across projects' do
+ it 'promotes project milestone to group milestone' do
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ end
+
+ it 'does not update issuables without milestone with the new promoted milestone' do
+ issue_without_milestone = create(:issue, project: project, milestone: nil)
+ merge_request_without_milestone = create(:merge_request, milestone: nil, source_project: project)
+
+ service.execute(milestone)
+
+ expect(issue_without_milestone.reload.milestone).to be_nil
+ expect(merge_request_without_milestone.reload.milestone).to be_nil
+ end
+
+ it 'sets issuables with new promoted milestone' do
+ issue = create(:issue, milestone: milestone, project: project)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ expect(issue.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request.reload.milestone).to eq(promoted_milestone)
+ end
+ end
+
+ context 'with duplicated milestone titles across projects' do
+ let(:project_2) { create(:project, namespace: group) }
+ let!(:milestone_2) { create(:milestone, project: project_2, title: milestone_title) }
+
+ it 'deletes project milestones with the same title' do
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ expect(promoted_milestone).to be_valid
+ expect(Milestone.exists?(milestone.id)).to be_falsy
+ expect(Milestone.exists?(milestone_2.id)).to be_falsy
+ end
+
+ it 'does not update issuables without milestone with the new promoted milestone' do
+ issue_without_milestone_1 = create(:issue, project: project, milestone: nil)
+ issue_without_milestone_2 = create(:issue, project: project_2, milestone: nil)
+ merge_request_without_milestone_1 = create(:merge_request, milestone: nil, source_project: project)
+ merge_request_without_milestone_2 = create(:merge_request, milestone: nil, source_project: project_2)
+
+ service.execute(milestone)
+
+ expect(issue_without_milestone_1.reload.milestone).to be_nil
+ expect(issue_without_milestone_2.reload.milestone).to be_nil
+ expect(merge_request_without_milestone_1.reload.milestone).to be_nil
+ expect(merge_request_without_milestone_2.reload.milestone).to be_nil
+ end
+
+ it 'sets all issuables with new promoted milestone' do
+ issue = create(:issue, milestone: milestone, project: project)
+ issue_2 = create(:issue, milestone: milestone_2, project: project_2)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+ merge_request_2 = create(:merge_request, milestone: milestone_2, source_project: project_2)
+
+ promoted_milestone = service.execute(milestone)
+
+ expect(issue.reload.milestone).to eq(promoted_milestone)
+ expect(issue_2.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request_2.reload.milestone).to eq(promoted_milestone)
+ end
+ end
+ end
+end
diff --git a/spec/services/notes/render_service_spec.rb b/spec/services/notes/render_service_spec.rb
new file mode 100644
index 00000000000..faac498037f
--- /dev/null
+++ b/spec/services/notes/render_service_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Notes::RenderService do
+ describe '#execute' do
+ it 'renders a Note' do
+ note = double(:note)
+ project = double(:project)
+ wiki = double(:wiki)
+ user = double(:user)
+
+ expect(Banzai::ObjectRenderer).to receive(:new)
+ .with(project, user,
+ requested_path: 'foo',
+ project_wiki: wiki,
+ ref: 'bar',
+ only_path: nil,
+ xhtml: false)
+ .and_call_original
+
+ expect_any_instance_of(Banzai::ObjectRenderer)
+ .to receive(:render).with([note], :note)
+
+ described_class.new(user).execute([note], project,
+ requested_path: 'foo',
+ project_wiki: wiki,
+ ref: 'bar',
+ only_path: nil,
+ xhtml: false)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index b13e12e7c94..db5de572b6d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -280,6 +280,7 @@ describe NotificationService, :mailer do
next if member.id == @u_disabled.id
# Author should not be notified
next if member.id == note.author.id
+
should_email(member)
end
@@ -327,6 +328,7 @@ describe NotificationService, :mailer do
next if member.id == @u_disabled.id
# Author should not be notified
next if member.id == note.author.id
+
should_email(member)
end
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
new file mode 100644
index 00000000000..ffb270d277e
--- /dev/null
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Projects::GroupLinks::CreateService, '#execute' do
+ let(:user) { create :user }
+ let(:group) { create :group }
+ let(:project) { create :project }
+ let(:opts) do
+ {
+ link_group_access: '30',
+ expires_at: nil
+ }
+ end
+ let(:subject) { described_class.new(project, user, opts) }
+
+ it 'adds group to project' do
+ expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1)
+ end
+
+ it 'returns false if group is blank' do
+ expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ end
+end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
new file mode 100644
index 00000000000..336ee01ae50
--- /dev/null
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Projects::GroupLinks::DestroyService, '#execute' do
+ let(:group_link) { create :project_group_link }
+ let(:project) { group_link.project }
+ let(:user) { create :user }
+ let(:subject) { described_class.new(project, user) }
+
+ it 'removes group from project' do
+ expect { subject.execute(group_link) }.to change { project.project_group_links.count }.from(1).to(0)
+ end
+
+ it 'returns false if group_link is blank' do
+ expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ end
+end
diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb
index aa1988d29d6..b71b47c59b6 100644
--- a/spec/services/projects/hashed_storage_migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage_migration_service_spec.rb
@@ -23,7 +23,7 @@ describe Projects::HashedStorageMigrationService do
it 'updates project to be hashed and not read-only' do
service.execute
- expect(project.hashed_storage?).to be_truthy
+ expect(project.hashed_storage?(:repository)).to be_truthy
expect(project.repository_read_only).to be_falsey
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 034065aab00..bf7facaec99 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -6,6 +6,41 @@ describe Projects::ImportService do
subject { described_class.new(project, user) }
+ describe '#async?' do
+ it 'returns true for an asynchronous importer' do
+ importer_class = double(:importer, async?: true)
+
+ allow(subject).to receive(:has_importer?).and_return(true)
+ allow(subject).to receive(:importer_class).and_return(importer_class)
+
+ expect(subject).to be_async
+ end
+
+ it 'returns false for a regular importer' do
+ importer_class = double(:importer, async?: false)
+
+ allow(subject).to receive(:has_importer?).and_return(true)
+ allow(subject).to receive(:importer_class).and_return(importer_class)
+
+ expect(subject).not_to be_async
+ end
+
+ it 'returns false when the importer does not define #async?' do
+ importer_class = double(:importer)
+
+ allow(subject).to receive(:has_importer?).and_return(true)
+ allow(subject).to receive(:importer_class).and_return(importer_class)
+
+ expect(subject).not_to be_async
+ end
+
+ it 'returns false when the importer does not exist' do
+ allow(subject).to receive(:has_importer?).and_return(false)
+
+ expect(subject).not_to be_async
+ end
+ end
+
describe '#execute' do
context 'with unknown url' do
before do
@@ -37,21 +72,24 @@ describe Projects::ImportService do
end
context 'with a Github repository' do
- it 'succeeds if repository import is successfully' do
- expect_any_instance_of(Github::Import).to receive(:execute).and_return(true)
+ it 'succeeds if repository import was scheduled' do
+ expect_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute)
+ .and_return(true)
result = subject.execute
expect(result[:status]).to eq :success
end
- it 'fails if repository import fails' do
- expect_any_instance_of(Repository).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ it 'fails if repository import was not scheduled' do
+ expect_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute)
+ .and_return(false)
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The remote data could not be imported."
end
end
@@ -92,47 +130,22 @@ describe Projects::ImportService do
end
it 'succeeds if importer succeeds' do
- allow_any_instance_of(Github::Import).to receive(:execute).and_return(true)
+ allow_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute).and_return(true)
result = subject.execute
expect(result[:status]).to eq :success
end
- it 'flushes various caches' do
- allow_any_instance_of(Github::Import).to receive(:execute)
- .and_return(true)
-
- expect_any_instance_of(Repository).to receive(:expire_content_cache)
-
- subject.execute
- end
-
it 'fails if importer fails' do
- allow_any_instance_of(Github::Import).to receive(:execute).and_return(false)
-
- result = subject.execute
-
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - The remote data could not be imported."
- end
-
- it 'fails if importer raise an error' do
- allow_any_instance_of(Github::Import).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API'))
+ allow_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute)
+ .and_return(false)
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - Github: failed to connect API"
- end
-
- it 'expires content cache after error' do
- allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false)
-
- expect_any_instance_of(Repository).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new)
- expect_any_instance_of(Repository).to receive(:expire_content_cache)
-
- subject.execute
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 2459f371a91..2b1337bee7e 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -42,6 +42,18 @@ describe Projects::TransferService do
expect(service).to receive(:execute_system_hooks)
end
end
+
+ it 'disk path has moved' do
+ old_path = project.repository.disk_path
+ old_full_path = project.repository.full_path
+
+ transfer_project(project, user, group)
+
+ expect(project.repository.disk_path).not_to eq(old_path)
+ expect(project.repository.full_path).not_to eq(old_full_path)
+ expect(project.disk_path).not_to eq(old_path)
+ expect(project.disk_path).to start_with(group.path)
+ end
end
context 'when transfer fails' do
@@ -188,6 +200,26 @@ describe Projects::TransferService do
end
end
+ context 'when hashed storage in use' do
+ let(:hashed_project) { create(:project, :repository, :hashed, namespace: user.namespace) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'does not move the directory' do
+ old_path = hashed_project.repository.disk_path
+ old_full_path = hashed_project.repository.full_path
+
+ transfer_project(hashed_project, user, group)
+ project.reload
+
+ expect(hashed_project.repository.disk_path).to eq(old_path)
+ expect(hashed_project.repository.full_path).to eq(old_full_path)
+ expect(hashed_project.disk_path).to eq(old_path)
+ end
+ end
+
describe 'refreshing project authorizations' do
let(:group) { create(:group) }
let(:owner) { project.namespace.owner }
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 50d3a4ec982..2bba71fef4f 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -12,6 +12,9 @@ describe Projects::UnlinkForkService do
context 'with opened merge request on the source project' do
let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) }
+ let(:merge_request2) { create(:merge_request, source_project: forked_project, target_project: fork_project(project)) }
+ let(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }
+
let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) }
before do
@@ -22,9 +25,14 @@ describe Projects::UnlinkForkService do
it 'close all pending merge requests' do
expect(mr_close_service).to receive(:execute).with(merge_request)
+ expect(mr_close_service).to receive(:execute).with(merge_request2)
subject.execute
end
+
+ it 'does not close merge requests for the project being unlinked' do
+ expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)
+ end
end
it 'remove fork relation' do
@@ -53,4 +61,14 @@ describe Projects::UnlinkForkService do
expect(source.forks_count).to be_zero
end
+
+ context 'when the original project was deleted' do
+ it 'does not fail when the original project is deleted' do
+ source = forked_project.forked_from_project
+ source.destroy
+ forked_project.reload
+
+ expect { subject.execute }.not_to raise_error
+ end
+ end
end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 8f7aea533dc..46cd10cdc12 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -69,11 +69,48 @@ describe SystemHooksService do
expect(data[:project_visibility]).to eq('private')
end
+
+ context 'group_rename' do
+ it 'contains old and new path' do
+ allow(group).to receive(:path_was).and_return('old-path')
+
+ data = event_data(group, :rename)
+
+ expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path)
+ expect(data[:path]).to eq(group.path)
+ expect(data[:full_path]).to eq(group.path)
+ expect(data[:old_path]).to eq(group.path_was)
+ expect(data[:old_full_path]).to eq(group.path_was)
+ end
+
+ it 'contains old and new full_path for subgroup' do
+ subgroup = create(:group, parent: group)
+ allow(subgroup).to receive(:path_was).and_return('old-path')
+
+ data = event_data(subgroup, :rename)
+
+ expect(data[:full_path]).to eq(subgroup.full_path)
+ expect(data[:old_path]).to eq('old-path')
+ end
+ end
+
+ context 'user_rename' do
+ it 'contains old and new username' do
+ allow(user).to receive(:username_was).and_return('old-username')
+
+ data = event_data(user, :rename)
+
+ expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username)
+ expect(data[:username]).to eq(user.username)
+ expect(data[:old_username]).to eq(user.username_was)
+ end
+ end
end
context 'event names' do
it { expect(event_name(user, :create)).to eq "user_create" }
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
+ it { expect(event_name(user, :rename)).to eq 'user_rename' }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
it { expect(event_name(project, :rename)).to eq "project_rename" }
@@ -85,6 +122,7 @@ describe SystemHooksService do
it { expect(event_name(key, :destroy)).to eq 'key_destroy' }
it { expect(event_name(group, :create)).to eq 'group_create' }
it { expect(event_name(group, :destroy)).to eq 'group_destroy' }
+ it { expect(event_name(group, :rename)).to eq 'group_rename' }
it { expect(event_name(group_member, :create)).to eq 'user_add_to_group' }
it { expect(event_name(group_member, :destroy)).to eq 'user_remove_from_group' }
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index a9b34a5258a..dc2673abc73 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -248,11 +248,11 @@ describe TodoService do
end
end
- describe '#destroy_issue' do
+ describe '#destroy_issuable' do
it 'refresh the todos count cache for the user' do
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
- service.destroy_issue(issue, john_doe)
+ service.destroy_issuable(issue, john_doe)
end
end
@@ -643,14 +643,6 @@ describe TodoService do
end
end
- describe '#destroy_merge_request' do
- it 'refresh the todos count cache for the user' do
- expect(john_doe).to receive(:update_todos_count_cache).and_call_original
-
- service.destroy_merge_request(mr_assigned, john_doe)
- end
- end
-
describe '#reassigned_merge_request' do
it 'creates a pending todo for new assignee' do
mr_unassigned.update_attribute(:assignee, john_doe)
diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb
new file mode 100644
index 00000000000..a188cf86772
--- /dev/null
+++ b/spec/services/users/keys_count_service_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(user) }
+
+ describe '#count' do
+ before do
+ create(:personal_key, user: user)
+ end
+
+ it 'returns the number of SSH keys as an Integer' do
+ expect(service.count).to eq(1)
+ end
+
+ it 'caches the number of keys in Redis' do
+ service.delete_cache
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ 2.times { service.count }
+ end
+
+ expect(recorder.count).to eq(1)
+ end
+ end
+
+ describe '#refresh_cache' do
+ it 'refreshes the Redis cache' do
+ Rails.cache.write(service.cache_key, 10)
+ service.refresh_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_zero
+ end
+ end
+
+ describe '#delete_cache' do
+ it 'removes the cache' do
+ service.count
+ service.delete_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_nil
+ end
+ end
+
+ describe '#uncached_count' do
+ it 'returns the number of SSH keys' do
+ expect(service.uncached_count).to be_zero
+ end
+
+ it 'does not cache the number of keys' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ 2.times { service.uncached_count }
+ end
+
+ expect(recorder.count).to be > 0
+ end
+ end
+
+ describe '#cache_key' do
+ it 'returns the cache key' do
+ expect(service.cache_key).to eq("users/key-count-service/#{user.id}")
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 48cacba6a8a..7c8331f6c60 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -48,7 +48,11 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include CookieHelper, :js
+ config.include InputHelper, :js
+ config.include InspectRequests, :js
config.include WaitForRequests, :js
+ config.include LiveDebugger, :js
config.include StubConfiguration
config.include EmailHelpers, :mailer, type: :mailer
config.include TestEnv
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 01aca74274c..ac0c7a9b493 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -18,21 +18,23 @@ module ApiHelpers
#
# Returns the relative path to the requested API resource
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil)
- "/api/#{version}#{path}" +
+ full_path = "/api/#{version}#{path}"
- # Normalize query string
- (path.index('?') ? '' : '?') +
+ if oauth_access_token
+ query_string = "access_token=#{oauth_access_token.token}"
+ elsif personal_access_token
+ query_string = "private_token=#{personal_access_token.token}"
+ elsif user
+ personal_access_token = create(:personal_access_token, user: user)
+ query_string = "private_token=#{personal_access_token.token}"
+ end
- if personal_access_token.present?
- "&private_token=#{personal_access_token.token}"
- elsif oauth_access_token.present?
- "&access_token=#{oauth_access_token.token}"
- # Append private_token if given a User object
- elsif user.respond_to?(:private_token)
- "&private_token=#{user.private_token}"
- else
- ''
- end
+ if query_string
+ full_path << (path.index('?') ? '&' : '?')
+ full_path << query_string
+ end
+
+ full_path
end
# Temporary helper method for simplifying V3 exclusive API specs
diff --git a/spec/support/bare_repo_operations.rb b/spec/support/bare_repo_operations.rb
new file mode 100644
index 00000000000..38d11992dc2
--- /dev/null
+++ b/spec/support/bare_repo_operations.rb
@@ -0,0 +1,60 @@
+require 'zlib'
+
+class BareRepoOperations
+ # The ID of empty tree.
+ # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+ include Gitlab::Popen
+
+ def initialize(path_to_repo)
+ @path_to_repo = path_to_repo
+ end
+
+ # Based on https://stackoverflow.com/a/25556917/1856239
+ def commit_file(file, dst_path, branch = 'master')
+ head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || EMPTY_TREE_ID
+
+ execute(['read-tree', '--empty'])
+ execute(['read-tree', head_id])
+
+ blob_id = execute(['hash-object', '--stdin', '-w']) do |stdin|
+ stdin.write(file.read)
+ end
+
+ execute(['update-index', '--add', '--cacheinfo', '100644', blob_id[0], dst_path])
+
+ tree_id = execute(['write-tree'])
+
+ commit_tree_args = ['commit-tree', tree_id[0], '-m', "Add #{dst_path}"]
+ commit_tree_args += ['-p', head_id] unless head_id == EMPTY_TREE_ID
+ commit_id = execute(commit_tree_args)
+
+ execute(['update-ref', "refs/heads/#{branch}", commit_id[0]])
+ end
+
+ private
+
+ def execute(args, allow_failure: false)
+ output, status = popen(base_args + args, nil) do |stdin|
+ yield stdin if block_given?
+ end
+
+ unless status.zero?
+ if allow_failure
+ return []
+ else
+ raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
+ end
+ end
+
+ output.split("\n")
+ end
+
+ def base_args
+ [
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{@path_to_repo}"
+ ]
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index c45c4a4310d..9f672bc92fc 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -1,25 +1,25 @@
# rubocop:disable Style/GlobalVars
require 'capybara/rails'
require 'capybara/rspec'
-require 'capybara/poltergeist'
require 'capybara-screenshot/rspec'
+require 'selenium-webdriver'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :poltergeist
-Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(
- app,
- js_errors: true,
- timeout: timeout,
- window_size: [1366, 768],
- url_whitelist: %w[localhost 127.0.0.1],
- url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
- phantomjs_options: [
- '--load-images=yes'
- ]
+Capybara.javascript_driver = :chrome
+Capybara.register_driver :chrome do |app|
+ extra_args = []
+ extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ chromeOptions: {
+ 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ }
)
+
+ Capybara::Selenium::Driver
+ .new(app, browser: :chrome, desired_capabilities: capabilities)
end
Capybara.default_max_wait_time = timeout
@@ -27,6 +27,10 @@ Capybara.ignore_hidden_elements = true
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
+# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
+Capybara::Screenshot.register_driver(:chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+end
RSpec.configure do |config|
config.before(:context, :js) do
@@ -37,13 +41,23 @@ RSpec.configure do |config|
end
config.before(:example, :js) do
+ session = Capybara.current_session
+
allow(Gitlab::Application.routes).to receive(:default_url_options).and_return(
- host: Capybara.current_session.server.host,
- port: Capybara.current_session.server.port,
+ host: session.server.host,
+ port: session.server.port,
protocol: 'http')
+
+ # reset window size between tests
+ unless session.current_window.size == [1240, 1400]
+ session.current_window.resize_to(1240, 1400) rescue nil
+ end
end
config.after(:example, :js) do |example|
+ # prevent localstorage from introducing side effects based on test order
+ execute_script("localStorage.clear();")
+
# capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
# but `block_and_wait_for_requests_complete` is called before it so by
# calling it explicitely here, we prevent any new requests from being fired
diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb
index 3eb7bea3227..868233416bf 100644
--- a/spec/support/capybara_helpers.rb
+++ b/spec/support/capybara_helpers.rb
@@ -38,7 +38,7 @@ module CapybaraHelpers
# Simulate a browser restart by clearing the session cookie.
def clear_browser_session
- page.driver.remove_cookie('_gitlab_session')
+ page.driver.browser.manage.delete_cookie('_gitlab_session')
end
end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index b23d81a226a..a0839eefe6c 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -14,7 +14,7 @@ shared_examples 'a GitHub-ish import controller: POST personal_access_token' do
it "updates access token" do
token = 'asdfasdf9876'
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:user).and_return(true)
post :personal_access_token, personal_access_token: token
@@ -79,7 +79,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end
it "handles an invalid access token" do
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:repos).and_raise(Octokit::Unauthorized)
get :status
@@ -110,7 +110,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context "when the repository owner is the provider user" do
context "when the provider user and GitLab user's usernames match" do
it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -122,7 +122,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
let(:provider_username) { "someone_else" }
it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -149,7 +149,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it "takes the existing namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -161,7 +161,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
it "creates a project using user's namespace" do
create(:user, username: other_username)
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -173,14 +173,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context "when a namespace with the provider user's username doesn't exist" do
context "when current user can create namespaces" do
it "creates the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).and_return(double(execute: true))
expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1)
end
it "takes the new namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -194,14 +194,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it "doesn't create the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).and_return(double(execute: true))
expect { post :create, format: :js }.not_to change(Namespace, :count)
end
it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -219,7 +219,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -227,7 +227,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'takes the selected name and default namespace' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -245,7 +245,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -257,7 +257,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -265,7 +265,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'creates the namespaces' do
- allow(Gitlab::GithubImport::ProjectCreator)
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -274,7 +274,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'new namespace has the right parent' do
- allow(Gitlab::GithubImport::ProjectCreator)
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -289,7 +289,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -297,7 +297,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'creates the namespaces' do
- allow(Gitlab::GithubImport::ProjectCreator)
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb
new file mode 100644
index 00000000000..224619c899c
--- /dev/null
+++ b/spec/support/cookie_helper.rb
@@ -0,0 +1,17 @@
+# Helper for setting cookies in Selenium/WebDriver
+#
+module CookieHelper
+ def set_cookie(name, value, options = {})
+ # Selenium driver will not set cookies for a given domain when the browser is at `about:blank`.
+ # It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive.
+ visit options.fetch(:path, '/') unless on_a_page?
+ page.driver.browser.manage.add_cookie(name: name, value: value, **options)
+ end
+
+ private
+
+ def on_a_page?
+ current_url = Capybara.current_session.driver.browser.current_url
+ current_url && current_url != '' && current_url != 'about:blank' && current_url != 'data:,'
+ end
+end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 934b4557ba2..26fd271ce31 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -94,6 +94,7 @@ module CycleAnalyticsHelpers
ref: 'master',
tag: false,
name: 'dummy',
+ stage: 'dummy',
pipeline: dummy_pipeline,
protected: false)
end
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 7132b9cd221..c24940393f9 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -71,26 +71,28 @@ shared_examples 'discussion comments' do |resource_name|
expect(page).not_to have_selector menu_selector
find(toggle_selector).click
- find('body').trigger 'click'
+ execute_script("document.querySelector('body').click()")
expect(page).not_to have_selector menu_selector
end
it 'clicking the ul padding or divider should not change the text' do
- find(menu_selector).trigger 'click'
+ execute_script("document.querySelector('#{menu_selector}').click()")
+ # on issues page, the menu closes when clicking anywhere, on other pages it will
+ # remain open if clicking divider or menu padding, but should not change button action
if resource_name == 'issue'
expect(find(dropdown_selector)).to have_content 'Comment'
find(toggle_selector).click
- find("#{menu_selector} .divider").trigger 'click'
+ execute_script("document.querySelector('#{menu_selector} .divider').click()")
else
- find(menu_selector).trigger 'click'
+ execute_script("document.querySelector('#{menu_selector}').click()")
expect(page).to have_selector menu_selector
expect(find(dropdown_selector)).to have_content 'Comment'
- find("#{menu_selector} .divider").trigger 'click'
+ execute_script("document.querySelector('#{menu_selector} .divider').click()")
expect(page).to have_selector menu_selector
end
@@ -105,7 +107,12 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- expect(find(dropdown_selector)).to have_content 'Start discussion'
+ # on issues page, the submit input is a <button>, on other pages it is <input>
+ if resource_name == 'issue'
+ expect(find(submit_selector)).to have_content 'Start discussion'
+ else
+ expect(find(submit_selector).value).to eq 'Start discussion'
+ end
expect(page).not_to have_selector menu_selector
end
@@ -187,7 +194,12 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- expect(find(dropdown_selector)).to have_content 'Comment'
+ # on issues page, the submit input is a <button>, on other pages it is <input>
+ if resource_name == 'issue'
+ expect(find(submit_selector)).to have_content 'Comment'
+ else
+ expect(find(submit_selector).value).to eq 'Comment'
+ end
expect(page).not_to have_selector menu_selector
end
@@ -226,6 +238,7 @@ shared_examples 'discussion comments' do |resource_name|
describe "on a closed #{resource_name}" do
before do
find("#{form_selector} .js-note-target-close").click
+ wait_for_requests
find("#{form_selector} .note-textarea").send_keys('a')
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 061e0d35590..08e21ee2537 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -61,7 +61,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing commands' do
it 'creates a note without the commands and interpret the commands accordingly' do
assignee = create(:user, username: 'bob')
- write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+ write_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
@@ -82,7 +82,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing only commands' do
it 'does not create a note but interpret the commands accordingly' do
assignee = create(:user, username: 'bob')
- write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+ write_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 192a2fed0a8..836e5e7be23 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -39,7 +39,7 @@ shared_examples 'reportable note' do |type|
end
def open_dropdown(dropdown)
- dropdown.find('.more-actions-toggle').trigger('click')
+ dropdown.find('.more-actions-toggle').click
dropdown.find('.dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb
index 5515c355cea..128aaaf25fe 100644
--- a/spec/support/fixture_helpers.rb
+++ b/spec/support/fixture_helpers.rb
@@ -1,6 +1,7 @@
module FixtureHelpers
def fixture_file(filename)
return '' if filename.blank?
+
File.read(expand_fixture_path(filename))
end
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index ef3c8e7087f..4ee33f9725b 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -33,6 +33,7 @@ end
def capture!(cmd, dir)
output = IO.popen(cmd, 'r', chdir: dir) { |io| io.read }
raise "command failed with #{$?}: #{cmd.join(' ')}" unless $?.success?
+
output.chomp
end
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index 89fb362cf14..c7e8a39a617 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -1,6 +1,11 @@
RSpec.configure do |config|
config.before(:each) do |example|
- next if example.metadata[:skip_gitaly_mock]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ if example.metadata[:disable_gitaly]
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+ else
+ next if example.metadata[:skip_gitaly_mock]
+
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ end
end
end
diff --git a/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535 b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535
new file mode 100644
index 00000000000..1c47f34b9a5
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf
new file mode 100644
index 00000000000..ca13c8df66a
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16 b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16
new file mode 100644
index 00000000000..3be244dbda4
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16
@@ -0,0 +1,2 @@
+x¥ŽK
+Â0EgNIÒ|ADtè*^’ mZ qGîÄY×àð8—×ZK©ý®7"ÈFc’Ò%oH¢D²Ü9rZÛLÎs“MJ2Œ™=±ÑÒAå…CmeFg²·V¨xI9øH2†¯þXÜJ…ár»pÅ6‡Ï;NÔà8•zˆ??>ß+–ù×z¡¹WÆBÞ ÎÙf·Ç}«þßb¡N@K\SYîì •iSC \ No newline at end of file
diff --git a/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e
new file mode 100644
index 00000000000..2bf27fe5048
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f
new file mode 100644
index 00000000000..8ab8606c6be
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f
Binary files differ
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index 688175369ae..658ff5871b0 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
- "can_create_project":false,
- "private_token":"Wvjy2Krpb7y8xi93owUz",
- "access_token":"Wvjy2Krpb7y8xi93owUz"
+ "can_create_project":false
}
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index ce8dfe5ae75..658ff5871b0 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
- "can_create_project":false,
- "private_token":"Wvjy2Krpb7y8xi93owUz",
- "access_token":"Wvjy2Krpb7y8xi93owUz"
-} \ No newline at end of file
+ "can_create_project":false
+}
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
new file mode 100644
index 00000000000..dabf0db7666
--- /dev/null
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -0,0 +1,119 @@
+module GoogleApi
+ module CloudPlatformHelpers
+ def stub_google_api_validate_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.since.to_i.to_s
+ end
+
+ def stub_google_api_expired_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.ago.to_i.to_s
+ end
+
+ def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(cloud_platform_response(cloud_platform_cluster_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_cluster_error(project_id, zone, cluster_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_create_cluster(project_id, zone, **options)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_create_cluster_error(project_id, zone)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_get_zone_operation(project_id, zone, operation_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_operation_error(project_id, zone, operation_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters/#{cluster_id}"
+ end
+
+ def cloud_platform_create_cluster_url(project_id, zone)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters"
+ end
+
+ def cloud_platform_get_zone_operation_url(project_id, zone, operation_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/operations/#{operation_id}"
+ end
+
+ def cloud_platform_response(body)
+ { status: 200, headers: { 'Content-Type' => 'application/json' }, body: body.to_json }
+ end
+
+ def load_sample_cert
+ pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
+ Base64.encode64(File.read(pem_file))
+ end
+
+ ##
+ # gcloud container clusters create
+ # https://cloud.google.com/container-engine/reference/rest/v1/projects.zones.clusters/create
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def cloud_platform_cluster_body(**options)
+ {
+ "name": options[:name] || 'string',
+ "description": options[:description] || 'string',
+ "initialNodeCount": options[:initialNodeCount] || 'number',
+ "masterAuth": {
+ "username": options[:username] || 'string',
+ "password": options[:password] || 'string',
+ "clusterCaCertificate": options[:clusterCaCertificate] || load_sample_cert,
+ "clientCertificate": options[:clientCertificate] || 'string',
+ "clientKey": options[:clientKey] || 'string'
+ },
+ "loggingService": options[:loggingService] || 'string',
+ "monitoringService": options[:monitoringService] || 'string',
+ "network": options[:network] || 'string',
+ "clusterIpv4Cidr": options[:clusterIpv4Cidr] || 'string',
+ "subnetwork": options[:subnetwork] || 'string',
+ "enableKubernetesAlpha": options[:enableKubernetesAlpha] || 'boolean',
+ "labelFingerprint": options[:labelFingerprint] || 'string',
+ "selfLink": options[:selfLink] || 'string',
+ "zone": options[:zone] || 'string',
+ "endpoint": options[:endpoint] || 'string',
+ "initialClusterVersion": options[:initialClusterVersion] || 'string',
+ "currentMasterVersion": options[:currentMasterVersion] || 'string',
+ "currentNodeVersion": options[:currentNodeVersion] || 'string',
+ "createTime": options[:createTime] || 'string',
+ "status": options[:status] || 'RUNNING',
+ "statusMessage": options[:statusMessage] || 'string',
+ "nodeIpv4CidrSize": options[:nodeIpv4CidrSize] || 'number',
+ "servicesIpv4Cidr": options[:servicesIpv4Cidr] || 'string',
+ "currentNodeCount": options[:currentNodeCount] || 'number',
+ "expireTime": options[:expireTime] || 'string'
+ }
+ end
+
+ def cloud_platform_operation_body(**options)
+ {
+ "name": options[:name] || 'operation-1234567891234-1234567',
+ "zone": options[:zone] || 'us-central1-a',
+ "operationType": options[:operationType] || 'CREATE_CLUSTER',
+ "status": options[:status] || 'PENDING',
+ "detail": options[:detail] || 'detail',
+ "statusMessage": options[:statusMessage] || '',
+ "selfLink": options[:selfLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/operations/operation-1234567891234-1234567',
+ "targetLink": options[:targetLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/clusters/test-cluster',
+ "startTime": options[:startTime] || '2017-09-13T16:49:13.055601589Z',
+ "endTime": options[:endTime] || ''
+ }
+ end
+ end
+end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
index fd22e384b1b..c98aa503ed1 100644
--- a/spec/support/helpers/merge_request_diff_helpers.rb
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -2,7 +2,7 @@ module MergeRequestDiffHelpers
def click_diff_line(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
- line[:num].find('.add-diff-note').trigger('click')
+ line[:num].find('.add-diff-note', visible: false).send_keys(:return)
end
def get_line_components(line_holder, diff_side = nil)
diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb
index 86008698692..79a0aa174b1 100644
--- a/spec/support/helpers/note_interaction_helpers.rb
+++ b/spec/support/helpers/note_interaction_helpers.rb
@@ -2,7 +2,7 @@ module NoteInteractionHelpers
def open_more_actions_dropdown(note)
note_element = find("#note_#{note.id}")
- note_element.find('.more-actions-toggle').trigger('click')
+ note_element.find('.more-actions-toggle').click
note_element.find('.more-actions .dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/input_helper.rb b/spec/support/input_helper.rb
new file mode 100644
index 00000000000..acbb42274ec
--- /dev/null
+++ b/spec/support/input_helper.rb
@@ -0,0 +1,7 @@
+# see app/assets/javascripts/test_utils/simulate_input.js
+
+module InputHelper
+ def simulate_input(selector, input = '')
+ evaluate_script("window.simulateInput(#{selector.to_json}, #{input.to_json});")
+ end
+end
diff --git a/spec/support/inspect_requests.rb b/spec/support/inspect_requests.rb
new file mode 100644
index 00000000000..88ddc5c7f6c
--- /dev/null
+++ b/spec/support/inspect_requests.rb
@@ -0,0 +1,17 @@
+require_relative './wait_for_requests'
+
+module InspectRequests
+ extend self
+ include WaitForRequests
+
+ def inspect_requests(inject_headers: {})
+ Gitlab::Testing::RequestInspectorMiddleware.log_requests!(inject_headers)
+
+ yield
+
+ wait_for_all_requests
+ Gitlab::Testing::RequestInspectorMiddleware.requests
+ ensure
+ Gitlab::Testing::RequestInspectorMiddleware.stop_logging!
+ end
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index c92f78b324c..e46b61b6461 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -9,22 +9,51 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
- def stub_kubeclient_discover
- WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
+ def stub_kubeclient_discover(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
end
def stub_kubeclient_pods(response = nil)
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
+ def stub_kubeclient_get_secrets(api_url, **options)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(kube_response(kube_v1_secrets_body(options)))
+ end
+
+ def stub_kubeclient_get_secrets_error(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(status: [404, "Internal Server Error"])
+ end
+
+ def kube_v1_secrets_body(**options)
+ {
+ "kind" => "SecretList",
+ "apiVersion": "v1",
+ "items" => [
+ {
+ "metadata": {
+ "name": options[:metadata_name] || "default-token-1",
+ "namespace": "kube-system"
+ },
+ "data": {
+ "token": options[:token] || Base64.encode64('token-sample-123')
+ }
+ }
+ ]
+ }
+ end
+
def kube_v1_discovery_body
{
"kind" => "APIResourceList",
"resources" => [
- { "name" => "pods", "namespaced" => true, "kind" => "Pod" }
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
]
}
end
diff --git a/spec/support/legacy_path_redirect_shared_examples.rb b/spec/support/legacy_path_redirect_shared_examples.rb
new file mode 100644
index 00000000000..f300bdd48b1
--- /dev/null
+++ b/spec/support/legacy_path_redirect_shared_examples.rb
@@ -0,0 +1,13 @@
+shared_examples 'redirecting a legacy path' do |source, target|
+ include RSpec::Rails::RequestExampleGroup
+
+ it "redirects #{source} to #{target} when the resource does not exist" do
+ expect(get(source)).to redirect_to(target)
+ end
+
+ it "does not redirect #{source} to #{target} when the resource exists" do
+ resource
+
+ expect(get(source)).not_to redirect_to(target)
+ end
+end
diff --git a/spec/support/live_debugger.rb b/spec/support/live_debugger.rb
new file mode 100644
index 00000000000..911eb48a8ca
--- /dev/null
+++ b/spec/support/live_debugger.rb
@@ -0,0 +1,17 @@
+require 'io/console'
+
+module LiveDebugger
+ def live_debug
+ puts
+ puts "Current example is paused for live debugging."
+ puts "Opening #{current_url} in your default browser..."
+ puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user
+ puts "Press any key to resume the execution of the example!!"
+
+ `open #{current_url}`
+
+ loop until $stdin.getch
+
+ puts "Back to the example!"
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index 4aed40bf22d..50702a0ac88 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -3,6 +3,21 @@ require_relative 'devise_helpers'
module LoginHelpers
include DeviseHelpers
+ # Overriding Devise::Test::IntegrationHelpers#sign_in to store @current_user
+ # since we may need it in LiveDebugger#live_debug.
+ def sign_in(resource, scope: nil)
+ super
+
+ @current_user = resource
+ end
+
+ # Overriding Devise::Test::IntegrationHelpers#sign_out to clear @current_user.
+ def sign_out(resource_or_scope)
+ super
+
+ @current_user = nil
+ end
+
# Internal: Log in as a specific user or a new user of a specific role
#
# user_or_role - User object, or a role to create (e.g., :admin, :user)
@@ -28,7 +43,7 @@ module LoginHelpers
gitlab_sign_in_with(user, **kwargs)
- user
+ @current_user = user
end
def gitlab_sign_in_via(provider, user, uid)
@@ -41,6 +56,7 @@ module LoginHelpers
def gitlab_sign_out
find(".header-user-dropdown-toggle").click
click_link "Sign out"
+ @current_user = nil
expect(page).to have_button('Sign in')
end
diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb
index bb6b7c63ee9..cdb62a5deee 100644
--- a/spec/support/matchers/access_matchers_for_controller.rb
+++ b/spec/support/matchers/access_matchers_for_controller.rb
@@ -5,7 +5,7 @@ module AccessMatchersForController
extend RSpec::Matchers::DSL
include Warden::Test::Helpers
- EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 302].freeze
+ EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 204, 302].freeze
EXPECTED_STATUS_CODE_DENIED = [401, 404].freeze
def emulate_user(role, membership = nil)
diff --git a/spec/support/matchers/security_header_matcher.rb b/spec/support/matchers/security_header_matcher.rb
new file mode 100644
index 00000000000..f8518d13ebb
--- /dev/null
+++ b/spec/support/matchers/security_header_matcher.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :include_security_headers do |expected|
+ match do |actual|
+ expect(actual.headers).to include('X-Content-Type-Options')
+ end
+end
diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb
index 431f20a2a5c..3b9eb84e824 100644
--- a/spec/support/mobile_helpers.rb
+++ b/spec/support/mobile_helpers.rb
@@ -12,6 +12,6 @@ module MobileHelpers
end
def resize_window(width, height)
- page.driver.resize_window width, height
+ Capybara.current_session.current_window.resize_to(width, height)
end
end
diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb
index 421a51fc336..2770cdcbefc 100644
--- a/spec/support/protected_tags/access_control_ce_shared_examples.rb
+++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples "protected tags > access control > CE" do
allowed_to_create_button = find(".js-allowed-to-create")
unless allowed_to_create_button.text == access_type_name
- allowed_to_create_button.trigger('click')
+ allowed_to_create_button.click
find('.create_access_levels-container .dropdown-menu li', match: :first)
within('.create_access_levels-container .dropdown-menu') { click_on access_type_name }
end
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
index ba0b805caad..cd67b517ea0 100644
--- a/spec/support/query_recorder.rb
+++ b/spec/support/query_recorder.rb
@@ -8,7 +8,14 @@ module ActiveRecord
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
end
+ def show_backtrace(values)
+ Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
+ caller.each { |line| Rails.logger.debug(" --> #{line}") }
+ end
+
def callback(name, start, finish, message_id, values)
+ show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
+
if values[:name]&.include?("CACHE")
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb
index d2aaae7518f..361190aa352 100644
--- a/spec/support/quick_actions_helpers.rb
+++ b/spec/support/quick_actions_helpers.rb
@@ -3,7 +3,7 @@ module QuickActionsHelpers
Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: text
- find('.js-comment-submit-button').trigger('click')
+ find('.js-comment-submit-button').click
end
end
end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
index d5bc12f3bc5..5fde91512da 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
@@ -9,7 +9,7 @@ shared_examples "protected branches > access control > CE" do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.trigger('click')
+ allowed_to_push_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
@@ -34,7 +34,7 @@ shared_examples "protected branches > access control > CE" do
within('.js-allowed-to-push-container') do
expect(first("li")).to have_content("Roles")
- click_on access_type_name
+ find(:link, access_type_name).click
end
end
@@ -79,7 +79,7 @@ shared_examples "protected branches > access control > CE" do
within('.js-allowed-to-merge-container') do
expect(first("li")).to have_content("Roles")
- click_on access_type_name
+ find(:link, access_type_name).click
end
end
diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
index 6bc39f2f279..4e18804b937 100644
--- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
@@ -3,7 +3,9 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' }
describe "GET /#{attributable_name} with custom attributes filter" do
- let!(:other_attributable) { create attributable.class.name.underscore }
+ before do
+ other_attributable
+ end
context 'with an unauthorized user' do
it 'does not filter by custom attributes' do
@@ -11,6 +13,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to be 2
+ expect(json_response.map { |r| r['id'] }).to contain_exactly attributable.id, other_attributable.id
end
end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 4d448a55978..4ead78529c3 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -38,6 +38,10 @@ module StubConfiguration
allow(Gitlab.config.backup).to receive_messages(to_settings(messages))
end
+ def stub_lfs_setting(messages)
+ allow(Gitlab.config.lfs).to receive_messages(to_settings(messages))
+ end
+
def stub_storage_settings(messages)
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index a27bfdee3d2..fff120fcb88 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -182,6 +182,8 @@ module TestEnv
return unless @gitaly_pid
Process.kill('KILL', @gitaly_pid)
+ rescue Errno::ESRCH
+ # The process can already be gone if the test run was INTerrupted.
end
def setup_factory_repo
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
index 0fa74f911f6..909d4e2ee8d 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -80,6 +80,6 @@ end
def submit_time(quick_action)
fill_in 'note[note]', with: quick_action
- find('.js-comment-submit-button').trigger('click')
+ find('.js-comment-submit-button').click
wait_for_requests
end
diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb
index b5c3c0f55b8..f4130d68271 100644
--- a/spec/support/wait_for_requests.rb
+++ b/spec/support/wait_for_requests.rb
@@ -1,25 +1,47 @@
-require_relative './wait_for_requests'
-
module WaitForRequests
extend self
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
def block_and_wait_for_requests_complete
+ block_requests { wait_for_all_requests }
+ end
+
+ # Block all requests inside block with 503 response
+ def block_requests
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
- wait_for('pending requests complete') do
- Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? && finished_all_requests?
- end
+ yield
ensure
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
+ # Slow down requests inside block by injecting `sleep 0.2` before each response
+ def slow_requests
+ Gitlab::Testing::RequestBlockerMiddleware.slow_requests!
+ yield
+ ensure
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ # Wait for client-side AJAX requests
def wait_for_requests
- wait_for('JS requests') { finished_all_requests? }
+ wait_for('JS requests complete') { finished_all_js_requests? }
+ end
+
+ # Wait for active Rack requests and client-side AJAX requests
+ def wait_for_all_requests
+ wait_for('pending requests complete') do
+ finished_all_rack_reqiests? &&
+ finished_all_js_requests?
+ end
end
private
- def finished_all_requests?
+ def finished_all_rack_reqiests?
+ Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
+ end
+
+ def finished_all_js_requests?
return true unless javascript_test?
finished_all_ajax_requests? &&
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
new file mode 100644
index 00000000000..641eccfd334
--- /dev/null
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -0,0 +1,41 @@
+require 'rake_helper'
+
+describe 'gitlab:cleanup rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/cleanup'
+ end
+
+ context 'cleanup repositories' do
+ let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
+ let(:storages) do
+ {
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address }
+ }
+ end
+
+ before do
+ FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ after do
+ FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
+ end
+
+ it 'moves it to an orphaned path' do
+ FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/broken/project.git'))
+ run_rake_task('gitlab:cleanup:repos')
+ repo_list = Dir['tmp/tests/default_storage/broken/*']
+
+ expect(repo_list.first).to include('+orphaned+')
+ end
+
+ it 'ignores @hashed repos' do
+ FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))
+
+ run_rake_task('gitlab:cleanup:repos')
+
+ expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 5dd8fe8eaa5..6aba86fdc3c 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -47,7 +47,7 @@ describe 'gitlab:gitaly namespace rake task' do
stub_env('CI', false)
FileUtils.mkdir_p(clone_path)
expect(Dir).to receive(:chdir).with(clone_path).and_call_original
- allow(Bundler).to receive(:bundle_path).and_return('/fake/bundle_path')
+ allow(Rails.env).to receive(:test?).and_return(false)
end
context 'gmake is available' do
@@ -57,7 +57,7 @@ describe 'gitlab:gitaly namespace rake task' do
it 'calls gmake in the gitaly directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
- expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake BUNDLE_PATH=/fake/bundle_path]).and_return(true)
+ expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake]).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -70,18 +70,20 @@ describe 'gitlab:gitaly namespace rake task' do
end
it 'calls make in the gitaly directory' do
- expect(main_object).to receive(:run_command!).with(command_preamble + %w[make BUNDLE_PATH=/fake/bundle_path]).and_return(true)
+ expect(main_object).to receive(:run_command!).with(command_preamble + %w[make]).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
- context 'when Rails.env is not "test"' do
+ context 'when Rails.env is test' do
+ let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] }
+
before do
- allow(Rails.env).to receive(:test?).and_return(false)
+ allow(Rails.env).to receive(:test?).and_return(true)
end
- it 'calls make in the gitaly directory without BUNDLE_PATH' do
- expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true)
+ it 'calls make in the gitaly directory with --no-deployment flag for bundle' do
+ expect(main_object).to receive(:run_command!).with(command_preamble + command).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -110,6 +112,7 @@ describe 'gitlab:gitaly namespace rake task' do
expected_output = <<~TOML
# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}
# This is in TOML format suitable for use in Gitaly's config.toml file.
+ bin_dir = "tmp/tests/gitaly"
socket_path = "/path/to/my.socket"
[gitlab-shell]
dir = "#{Gitlab.config.gitlab_shell.path}"
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
deleted file mode 100644
index 972670e7f91..00000000000
--- a/spec/tasks/gitlab/users_rake_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'spec_helper'
-require 'rake'
-
-describe 'gitlab:users namespace rake task' do
- let(:enable_registry) { true }
-
- before :all do
- Rake.application.rake_require 'tasks/gitlab/helpers'
- Rake.application.rake_require 'tasks/gitlab/users'
-
- # empty task as env is already loaded
- Rake::Task.define_task :environment
- end
-
- def run_rake_task(task_name)
- Rake::Task[task_name].reenable
- Rake.application.invoke_task task_name
- end
-
- describe 'clear_all_authentication_tokens' do
- before do
- # avoid writing task output to spec progress
- allow($stdout).to receive :write
- end
-
- context 'gitlab version' do
- it 'clears the authentication token for all users' do
- create_list(:user, 2)
-
- expect(User.pluck(:authentication_token)).to all(be_present)
-
- run_rake_task('gitlab:users:clear_all_authentication_tokens')
-
- expect(User.pluck(:authentication_token)).to all(be_nil)
- end
- end
- end
-end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index b84137eb365..51f7a536cbb 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -7,12 +7,6 @@ describe 'tokens rake tasks' do
Rake.application.rake_require 'tasks/tokens'
end
- describe 'reset_all task' do
- it 'invokes create_hooks task' do
- expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token }
- end
- end
-
describe 'reset_all_email task' do
it 'invokes create_hooks task' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb
index 41de94d35c2..79a566975df 100644
--- a/spec/unicorn/unicorn_spec.rb
+++ b/spec/unicorn/unicorn_spec.rb
@@ -71,6 +71,7 @@ describe 'Unicorn' do
timeout = 5 * 60
timeout.times do
return if File.exist?(ready_file)
+
pid = Process.waitpid(master_pid, Process::WNOHANG)
raise "unicorn failed to boot: #{$?}" unless pid.nil?
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 2492d56a5cf..fd195d6f9b8 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -3,25 +3,77 @@ require 'spec_helper'
describe FileUploader do
let(:uploader) { described_class.new(build_stubbed(:project)) }
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- project = build_stubbed(:project)
- upload = double(model: project, path: 'secret/foo.jpg')
+ context 'legacy storage' do
+ let(:project) { build_stubbed(:project) }
- dynamic_segment = project.path_with_namespace
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ dynamic_segment = project.full_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
+
+ expect(uploader.store_dir).to include(project.full_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
end
end
- describe "#store_dir" do
- it "stores in the namespace path" do
- project = build_stubbed(:project)
- uploader = described_class.new(project)
+ context 'hashed storage' do
+ context 'when rolled out attachments' do
+ let(:project) { build_stubbed(:project, :hashed) }
+
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
+
+ dynamic_segment = project.disk_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
+
+ expect(uploader.store_dir).to include(project.disk_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
+ end
+ end
+
+ context 'when only repositories are rolled out' do
+ let(:project) { build_stubbed(:project, storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
+
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
+
+ dynamic_segment = project.full_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
- expect(uploader.store_dir).to include(project.path_with_namespace)
- expect(uploader.store_dir).not_to include("system")
+ expect(uploader.store_dir).to include(project.full_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
+ end
end
end
diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb
deleted file mode 100644
index 08e1c5a728a..00000000000
--- a/spec/validators/dynamic_path_validator_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-require 'spec_helper'
-
-describe DynamicPathValidator do
- let(:validator) { described_class.new(attributes: [:path]) }
-
- def expect_handles_invalid_utf8
- expect { yield('\255invalid') }.to be_falsey
- end
-
- describe '.valid_user_path' do
- it 'handles invalid utf8' do
- expect(described_class.valid_user_path?("a\0weird\255path")).to be_falsey
- end
- end
-
- describe '.valid_group_path' do
- it 'handles invalid utf8' do
- expect(described_class.valid_group_path?("a\0weird\255path")).to be_falsey
- end
- end
-
- describe '.valid_project_path' do
- it 'handles invalid utf8' do
- expect(described_class.valid_project_path?("a\0weird\255path")).to be_falsey
- end
- end
-
- describe '#path_valid_for_record?' do
- context 'for project' do
- it 'calls valid_project_path?' do
- project = build(:project, path: 'activity')
-
- expect(described_class).to receive(:valid_project_path?).with(project.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(project, 'activity')).to be_truthy
- end
- end
-
- context 'for group' do
- it 'calls valid_group_path?' do
- group = build(:group, :nested, path: 'activity')
-
- expect(described_class).to receive(:valid_group_path?).with(group.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(group, 'activity')).to be_falsey
- end
- end
-
- context 'for user' do
- it 'calls valid_user_path?' do
- user = build(:user, username: 'activity')
-
- expect(described_class).to receive(:valid_user_path?).with(user.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(user, 'activity')).to be_truthy
- end
- end
-
- context 'for user namespace' do
- it 'calls valid_user_path?' do
- user = create(:user, username: 'activity')
- namespace = user.namespace
-
- expect(described_class).to receive(:valid_user_path?).with(namespace.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(namespace, 'activity')).to be_truthy
- end
- end
- end
-
- describe '#validates_each' do
- it 'adds a message when the path is not in the correct format' do
- group = build(:group)
-
- validator.validate_each(group, :path, "Path with spaces, and comma's!")
-
- expect(group.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message)
- end
-
- it 'adds a message when the path is not in the correct format' do
- group = build(:group, path: 'users')
-
- validator.validate_each(group, :path, 'users')
-
- expect(group.errors[:path]).to include('users is a reserved name')
- end
-
- it 'updating to an invalid path is not allowed' do
- project = create(:project)
- project.path = 'update'
-
- validator.validate_each(project, :path, 'update')
-
- expect(project.errors[:path]).to include('update is a reserved name')
- end
- end
-end
diff --git a/spec/validators/namespace_path_validator_spec.rb b/spec/validators/namespace_path_validator_spec.rb
new file mode 100644
index 00000000000..61e2845f35f
--- /dev/null
+++ b/spec/validators/namespace_path_validator_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe NamespacePathValidator do
+ let(:validator) { described_class.new(attributes: [:path]) }
+
+ describe '.valid_path?' do
+ it 'handles invalid utf8' do
+ expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ group = build(:group)
+
+ validator.validate_each(group, :path, "Path with spaces, and comma's!")
+
+ expect(group.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message)
+ end
+
+ it 'adds a message when the path is reserved when creating' do
+ group = build(:group, path: 'help')
+
+ validator.validate_each(group, :path, 'help')
+
+ expect(group.errors[:path]).to include('help is a reserved name')
+ end
+
+ it 'adds a message when the path is reserved when updating' do
+ group = create(:group)
+ group.path = 'help'
+
+ validator.validate_each(group, :path, 'help')
+
+ expect(group.errors[:path]).to include('help is a reserved name')
+ end
+ end
+end
diff --git a/spec/validators/project_path_validator_spec.rb b/spec/validators/project_path_validator_spec.rb
new file mode 100644
index 00000000000..8bb5e72dc22
--- /dev/null
+++ b/spec/validators/project_path_validator_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe ProjectPathValidator do
+ let(:validator) { described_class.new(attributes: [:path]) }
+
+ describe '.valid_path?' do
+ it 'handles invalid utf8' do
+ expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ project = build(:project)
+
+ validator.validate_each(project, :path, "Path with spaces, and comma's!")
+
+ expect(project.errors[:path]).to include(Gitlab::PathRegex.project_path_format_message)
+ end
+
+ it 'adds a message when the path is reserved when creating' do
+ project = build(:project, path: 'blob')
+
+ validator.validate_each(project, :path, 'blob')
+
+ expect(project.errors[:path]).to include('blob is a reserved name')
+ end
+
+ it 'adds a message when the path is reserved when updating' do
+ project = create(:project)
+ project.path = 'blob'
+
+ validator.validate_each(project, :path, 'blob')
+
+ expect(project.errors[:path]).to include('blob is a reserved name')
+ end
+ end
+end
diff --git a/spec/validators/user_path_validator_spec.rb b/spec/validators/user_path_validator_spec.rb
new file mode 100644
index 00000000000..a46089cc24f
--- /dev/null
+++ b/spec/validators/user_path_validator_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe UserPathValidator do
+ let(:validator) { described_class.new(attributes: [:username]) }
+
+ describe '.valid_path?' do
+ it 'handles invalid utf8' do
+ expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ user = build(:user)
+
+ validator.validate_each(user, :username, "Path with spaces, and comma's!")
+
+ expect(user.errors[:username]).to include(Gitlab::PathRegex.namespace_format_message)
+ end
+
+ it 'adds a message when the path is reserved when creating' do
+ user = build(:user, username: 'help')
+
+ validator.validate_each(user, :username, 'help')
+
+ expect(user.errors[:username]).to include('help is a reserved name')
+ end
+
+ it 'adds a message when the path is reserved when updating' do
+ user = create(:user)
+ user.username = 'help'
+
+ validator.validate_each(user, :username, 'help')
+
+ expect(user.errors[:username]).to include('help is a reserved name')
+ end
+ end
+end
diff --git a/spec/views/projects/commit/branches.html.haml_spec.rb b/spec/views/projects/commit/branches.html.haml_spec.rb
new file mode 100644
index 00000000000..b9d4dc80fe0
--- /dev/null
+++ b/spec/views/projects/commit/branches.html.haml_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe 'projects/commit/branches.html.haml' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign(:project, project)
+ end
+
+ context 'when branches and tags are available' do
+ before do
+ assign(:branches, ['master', 'test-branch'])
+ assign(:branches_limit_exceeded, false)
+ assign(:tags, ['tag1'])
+ assign(:tags_limit_exceeded, false)
+
+ render
+ end
+
+ it 'shows default branch' do
+ expect(rendered).to have_link('master')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows branch and tag links' do
+ expect(rendered).to have_link('test-branch')
+ expect(rendered).to have_link('tag1')
+ end
+ end
+
+ context 'when branches are available but no tags' do
+ before do
+ assign(:branches, ['master', 'test-branch'])
+ assign(:branches_limit_exceeded, false)
+ assign(:tags, [])
+ assign(:tags_limit_exceeded, true)
+
+ render
+ end
+
+ it 'shows branches' do
+ expect(rendered).to have_link('master')
+ expect(rendered).to have_link('test-branch')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows limit exceeded message for tags' do
+ expect(rendered).to have_text('Tags unavailable')
+ end
+ end
+
+ context 'when tags are available but no branches (just default)' do
+ before do
+ assign(:branches, ['master'])
+ assign(:branches_limit_exceeded, true)
+ assign(:tags, %w(tag1 tag2))
+ assign(:tags_limit_exceeded, false)
+
+ render
+ end
+
+ it 'shows default branch' do
+ expect(rendered).to have_text('master')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows tags' do
+ expect(rendered).to have_link('tag1')
+ expect(rendered).to have_link('tag2')
+ end
+
+ it 'shows limit exceeded for branches' do
+ expect(rendered).to have_text('Branches unavailable')
+ end
+ end
+
+ context 'when branches and tags are not available' do
+ before do
+ assign(:branches, ['master'])
+ assign(:branches_limit_exceeded, true)
+ assign(:tags, [])
+ assign(:tags_limit_exceeded, true)
+
+ render
+ end
+
+ it 'shows default branch' do
+ expect(rendered).to have_text('master')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows too many to search' do
+ expect(rendered).to have_text('Branches unavailable')
+ expect(rendered).to have_text('Tags unavailable')
+ end
+ end
+end
diff --git a/spec/views/shared/issuable/_participants.html.haml.rb b/spec/views/shared/issuable/_participants.html.haml.rb
deleted file mode 100644
index 51059d4c0d7..00000000000
--- a/spec/views/shared/issuable/_participants.html.haml.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-require 'nokogiri'
-
-describe 'shared/issuable/_participants.html.haml' do
- let(:project) { create(:project) }
- let(:participants) { create_list(:user, 100) }
-
- before do
- allow(view).to receive_messages(project: project,
- participants: participants)
- end
-
- it 'renders lazy loaded avatars' do
- render 'shared/issuable/participants'
-
- html = Nokogiri::HTML(rendered)
-
- avatars = html.css('.participants-author img')
-
- avatars.each do |avatar|
- expect(avatar[:class]).to include('lazy')
- expect(avatar[:src]).to eql(LazyImageTagHelper.placeholder_image)
- expect(avatar[:"data-src"]).to match('http://www.gravatar.com/avatar/')
- end
- end
-end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index 11f208289db..8054ec11a48 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -2,11 +2,22 @@ require 'spec_helper'
describe ClusterProvisionWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :scheduled) }
it 'provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(cluster.id)
end
@@ -14,7 +25,7 @@ describe ClusterProvisionWorker do
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(123)
end
diff --git a/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb b/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb
new file mode 100644
index 00000000000..4b9aa9a7ef8
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::NotifyUponDeath do
+ let(:worker_class) do
+ Class.new do
+ include Sidekiq::Worker
+ include Gitlab::GithubImport::NotifyUponDeath
+ end
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'notifies the JobWaiter when 3 arguments are given and the last is a String' do
+ job = { 'args' => [12, {}, '123abc'], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:notify)
+ .with('123abc', '123')
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+
+ it 'does not notify the JobWaiter when only 2 arguments are given' do
+ job = { 'args' => [12, {}], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+
+ it 'does not notify the JobWaiter when only 1 argument is given' do
+ job = { 'args' => [12], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+
+ it 'does not notify the JobWaiter when the last argument is not a String' do
+ job = { 'args' => [12, {}, 40], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
new file mode 100644
index 00000000000..3ccf06f2d7d
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ObjectImporter do
+ let(:worker) do
+ Class.new do
+ include(Gitlab::GithubImport::ObjectImporter)
+
+ def counter_name
+ :dummy_counter
+ end
+
+ def counter_description
+ 'This is a counter'
+ end
+ end.new
+ end
+
+ describe '#import' do
+ it 'imports the object' do
+ representation_class = double(:representation_class)
+ importer_class = double(:importer_class)
+ importer_instance = double(:importer_instance)
+ representation = double(:representation)
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+
+ expect(worker)
+ .to receive(:representation_class)
+ .and_return(representation_class)
+
+ expect(worker)
+ .to receive(:importer_class)
+ .and_return(importer_class)
+
+ expect(representation_class)
+ .to receive(:from_json_hash)
+ .with(an_instance_of(Hash))
+ .and_return(representation)
+
+ expect(importer_class)
+ .to receive(:new)
+ .with(representation, project, client)
+ .and_return(importer_instance)
+
+ expect(importer_instance)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, { 'number' => 10 })
+ end
+ end
+
+ describe '#counter' do
+ it 'returns a Prometheus counter' do
+ expect(worker)
+ .to receive(:counter_name)
+ .and_call_original
+
+ expect(worker)
+ .to receive(:counter_description)
+ .and_call_original
+
+ worker.counter
+ end
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
new file mode 100644
index 00000000000..321ae3fe978
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Queue do
+ it 'sets the Sidekiq options for the worker' do
+ worker = Class.new do
+ include Sidekiq::Worker
+ include Gitlab::GithubImport::Queue
+ end
+
+ expect(worker.sidekiq_options['queue']).to eq('github_importer')
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
new file mode 100644
index 00000000000..8de4059c4ae
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ReschedulingMethods do
+ let(:worker) do
+ Class.new { include(Gitlab::GithubImport::ReschedulingMethods) }.new
+ end
+
+ describe '#perform' do
+ context 'with a non-existing project' do
+ it 'does not perform any work' do
+ expect(worker)
+ .not_to receive(:try_import)
+
+ worker.perform(-1, {})
+ end
+
+ it 'notifies any waiters so they do not wait forever' do
+ expect(worker)
+ .to receive(:notify_waiter)
+ .with('123')
+
+ worker.perform(-1, {}, '123')
+ end
+ end
+
+ context 'with an existing project' do
+ let(:project) { create(:project) }
+
+ it 'notifies any waiters upon successfully importing the data' do
+ expect(worker)
+ .to receive(:try_import)
+ .with(
+ an_instance_of(Project),
+ an_instance_of(Gitlab::GithubImport::Client),
+ { 'number' => 2 }
+ )
+ .and_return(true)
+
+ expect(worker)
+ .to receive(:notify_waiter).with('123')
+
+ worker.perform(project.id, { 'number' => 2 }, '123')
+ end
+
+ it 'reschedules itself if the data could not be imported' do
+ expect(worker)
+ .to receive(:try_import)
+ .with(
+ an_instance_of(Project),
+ an_instance_of(Gitlab::GithubImport::Client),
+ { 'number' => 2 }
+ )
+ .and_return(false)
+
+ expect(worker)
+ .not_to receive(:notify_waiter)
+
+ expect_any_instance_of(Gitlab::GithubImport::Client)
+ .to receive(:rate_limit_resets_in)
+ .and_return(14)
+
+ expect(worker.class)
+ .to receive(:perform_in)
+ .with(14, project.id, { 'number' => 2 }, '123')
+
+ worker.perform(project.id, { 'number' => 2 }, '123')
+ end
+ end
+ end
+
+ describe '#try_import' do
+ it 'returns true when the import succeeds' do
+ expect(worker)
+ .to receive(:import)
+ .with(10, 20)
+
+ expect(worker.try_import(10, 20)).to eq(true)
+ end
+
+ it 'returns false when the import fails due to hitting the GitHub API rate limit' do
+ expect(worker)
+ .to receive(:import)
+ .with(10, 20)
+ .and_raise(Gitlab::GithubImport::RateLimitError)
+
+ expect(worker.try_import(10, 20)).to eq(false)
+ end
+ end
+
+ describe '#notify_waiter' do
+ it 'notifies the waiter if a waiter key is specified' do
+ expect(worker)
+ .to receive(:jid)
+ .and_return('abc123')
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:notify)
+ .with('123', 'abc123')
+
+ worker.notify_waiter('123')
+ end
+
+ it 'does not notify any waiters if no waiter key is specified' do
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker.notify_waiter(nil)
+ end
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
new file mode 100644
index 00000000000..241e8a2b6d3
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::StageMethods do
+ let(:project) { create(:project) }
+ let(:worker) do
+ Class.new { include(Gitlab::GithubImport::StageMethods) }.new
+ end
+
+ describe '#perform' do
+ it 'returns if no project could be found' do
+ expect(worker).not_to receive(:try_import)
+
+ worker.perform(-1)
+ end
+
+ it 'imports the data when the project exists' do
+ allow(worker)
+ .to receive(:find_project)
+ .with(project.id)
+ .and_return(project)
+
+ expect(worker)
+ .to receive(:try_import)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Client),
+ an_instance_of(Project)
+ )
+
+ worker.perform(project.id)
+ end
+ end
+
+ describe '#try_import' do
+ it 'imports the project' do
+ client = double(:client)
+
+ expect(worker)
+ .to receive(:import)
+ .with(client, project)
+
+ worker.try_import(client, project)
+ end
+
+ it 'reschedules the worker if RateLimitError was raised' do
+ client = double(:client, rate_limit_resets_in: 10)
+
+ expect(worker)
+ .to receive(:import)
+ .with(client, project)
+ .and_raise(Gitlab::GithubImport::RateLimitError)
+
+ expect(worker.class)
+ .to receive(:perform_in)
+ .with(10, project.id)
+
+ worker.try_import(client, project)
+ end
+ end
+
+ describe '#find_project' do
+ it 'returns a Project for an existing ID' do
+ project.update_column(:import_status, 'started')
+
+ expect(worker.find_project(project.id)).to eq(project)
+ end
+
+ it 'returns nil for a project that failed importing' do
+ project.update_column(:import_status, 'failed')
+
+ expect(worker.find_project(project.id)).to be_nil
+ end
+
+ it 'returns nil for a non-existing project ID' do
+ expect(worker.find_project(-1)).to be_nil
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
new file mode 100644
index 00000000000..3be49a0dee8
--- /dev/null
+++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project, import_jid: '123') }
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ context 'when the project no longer exists' do
+ it 'does not perform any work' do
+ expect(worker).not_to receive(:wait_for_jobs)
+
+ worker.perform(-1, { '123' => 2 }, :finish)
+ end
+ end
+
+ context 'when there are remaining jobs' do
+ before do
+ allow(worker)
+ .to receive(:find_project)
+ .and_return(project)
+ end
+
+ it 'reschedules itself' do
+ expect(worker)
+ .to receive(:wait_for_jobs)
+ .with({ '123' => 2 })
+ .and_return({ '123' => 1 })
+
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(described_class::INTERVAL, project.id, { '123' => 1 }, :finish)
+
+ worker.perform(project.id, { '123' => 2 }, :finish)
+ end
+ end
+
+ context 'when there are no remaining jobs' do
+ before do
+ allow(worker)
+ .to receive(:find_project)
+ .and_return(project)
+
+ allow(worker)
+ .to receive(:wait_for_jobs)
+ .with({ '123' => 2 })
+ .and_return({})
+ end
+
+ it 'schedules the next stage' do
+ expect(project)
+ .to receive(:refresh_import_jid_expiration)
+
+ expect(Gitlab::GithubImport::Stage::FinishImportWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+
+ worker.perform(project.id, { '123' => 2 }, :finish)
+ end
+
+ it 'raises KeyError when the stage name is invalid' do
+ expect { worker.perform(project.id, { '123' => 2 }, :kittens) }
+ .to raise_error(KeyError)
+ end
+ end
+ end
+
+ describe '#wait_for_jobs' do
+ it 'waits for jobs to complete and returns a new pair of keys to wait for' do
+ waiter1 = double(:waiter1, jobs_remaining: 1, key: '123')
+ waiter2 = double(:waiter2, jobs_remaining: 0, key: '456')
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:new)
+ .ordered
+ .with(2, '123')
+ .and_return(waiter1)
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:new)
+ .ordered
+ .with(1, '456')
+ .and_return(waiter2)
+
+ expect(waiter1)
+ .to receive(:wait)
+ .with(described_class::BLOCKING_WAIT_TIME)
+
+ expect(waiter2)
+ .to receive(:wait)
+ .with(described_class::BLOCKING_WAIT_TIME)
+
+ new_waiters = worker.wait_for_jobs({ '123' => 2, '456' => 1 })
+
+ expect(new_waiters).to eq({ '123' => 1 })
+ end
+ end
+
+ describe '#find_project' do
+ it 'returns a Project' do
+ project.update_column(:import_status, 'started')
+
+ found = worker.find_project(project.id)
+
+ expect(found).to be_an_instance_of(Project)
+
+ # This test is there to make sure we only select the columns we care
+ # about.
+ expect(found.attributes).to eq({ 'id' => nil, 'import_jid' => '123' })
+ end
+
+ it 'returns nil if the project import is not running' do
+ expect(worker.find_project(project.id)).to be_nil
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
new file mode 100644
index 00000000000..7c8c665a9b3
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportDiffNoteWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports a diff note' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'noteable_id' => 42,
+ 'path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => "@@ -1 +1 @@\n-Hello\n+Hello world",
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s
+ }
+
+ expect(Gitlab::GithubImport::Importer::DiffNoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::DiffNote),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
new file mode 100644
index 00000000000..4116380ff4d
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportIssueWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports an issue' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Issue',
+ 'description' => 'This is my issue',
+ 'milestone_number' => 4,
+ 'state' => 'opened',
+ 'assignees' => [{ 'id' => 4, 'login' => 'alice' }],
+ 'label_names' => %w[bug],
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s,
+ 'pull_request' => false
+ }
+
+ expect(Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Issue),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
new file mode 100644
index 00000000000..0ca825a722b
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportNoteWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports a note' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'noteable_id' => 42,
+ 'noteable_type' => 'issues',
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s
+ }
+
+ expect(Gitlab::GithubImport::Importer::NoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Note),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
new file mode 100644
index 00000000000..d49f560af42
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportPullRequestWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports a pull request' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Pull Request',
+ 'description' => 'This is my pull request',
+ 'source_branch' => 'my-feature',
+ 'source_branch_sha' => '123abc',
+ 'target_branch' => 'master',
+ 'target_branch_sha' => '456def',
+ 'source_repository_id' => 400,
+ 'target_repository_id' => 200,
+ 'source_repository_owner' => 'alice',
+ 'state' => 'closed',
+ 'milestone_number' => 4,
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'assignee' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s,
+ 'merged_at' => Time.zone.now.to_s
+ }
+
+ expect(Gitlab::GithubImport::Importer::PullRequestImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::PullRequest),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
new file mode 100644
index 00000000000..073c6d7a2f5
--- /dev/null
+++ b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
@@ -0,0 +1,95 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::RefreshImportJidWorker do
+ let(:worker) { described_class.new }
+
+ describe '.perform_in_the_future' do
+ it 'schedules a job in the future' do
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(1.minute.to_i, 10, '123')
+
+ described_class.perform_in_the_future(10, '123')
+ end
+ end
+
+ describe '#perform' do
+ let(:project) { create(:project, import_jid: '123abc') }
+
+ context 'when the project does not exist' do
+ it 'does nothing' do
+ expect(Gitlab::SidekiqStatus)
+ .not_to receive(:running?)
+
+ worker.perform(-1, '123')
+ end
+ end
+
+ context 'when the job is running' do
+ it 'refreshes the import JID and reschedules itself' do
+ allow(worker)
+ .to receive(:find_project)
+ .with(project.id)
+ .and_return(project)
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:running?)
+ .with('123')
+ .and_return(true)
+
+ expect(project)
+ .to receive(:refresh_import_jid_expiration)
+
+ expect(worker.class)
+ .to receive(:perform_in_the_future)
+ .with(project.id, '123')
+
+ worker.perform(project.id, '123')
+ end
+ end
+
+ context 'when the job is no longer running' do
+ it 'returns' do
+ allow(worker)
+ .to receive(:find_project)
+ .with(project.id)
+ .and_return(project)
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:running?)
+ .with('123')
+ .and_return(false)
+
+ expect(project)
+ .not_to receive(:refresh_import_jid_expiration)
+
+ worker.perform(project.id, '123')
+ end
+ end
+ end
+
+ describe '#find_project' do
+ it 'returns a Project' do
+ project = create(:project, import_status: 'started')
+
+ expect(worker.find_project(project.id)).to be_an_instance_of(Project)
+ end
+
+ it 'only selects the import JID field' do
+ project = create(:project, import_status: 'started', import_jid: '123abc')
+
+ expect(worker.find_project(project.id).attributes)
+ .to eq({ 'id' => nil, 'import_jid' => '123abc' })
+ end
+
+ it 'returns nil for a project for which the import process failed' do
+ project = create(:project, import_status: 'failed')
+
+ expect(worker.find_project(project.id)).to be_nil
+ end
+
+ it 'returns nil for a non-existing project' do
+ expect(worker.find_project(-1)).to be_nil
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
new file mode 100644
index 00000000000..91e0cddb5d8
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::FinishImportWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'marks the import as finished' do
+ expect(project).to receive(:after_import)
+ expect(worker).to receive(:report_import_time).with(project)
+
+ worker.import(double(:client), project)
+ end
+ end
+
+ describe '#report_import_time' do
+ it 'reports the total import time' do
+ expect(worker.histogram)
+ .to receive(:observe)
+ .with({ project: project.path_with_namespace }, a_kind_of(Numeric))
+ .and_call_original
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .and_call_original
+
+ expect(worker.logger).to receive(:info).with(an_instance_of(String))
+
+ worker.report_import_time(project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
new file mode 100644
index 00000000000..8c80d660287
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports the base data of a project' do
+ importer = double(:importer)
+ client = double(:client)
+
+ described_class::IMPORTERS.each do |klass|
+ expect(klass)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+ end
+
+ expect(project).to receive(:refresh_import_jid_expiration)
+
+ expect(Gitlab::GithubImport::Stage::ImportPullRequestsWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
new file mode 100644
index 00000000000..ab347f5b75b
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports the issues and diff notes' do
+ client = double(:client)
+
+ described_class::IMPORTERS.each do |klass|
+ importer = double(:importer)
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(klass)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(waiter)
+ end
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :notes)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
new file mode 100644
index 00000000000..098d2d55386
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportNotesWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports all the notes' do
+ importer = double(:importer)
+ client = double(:client)
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::NotesImporter)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(waiter)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :finish)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
new file mode 100644
index 00000000000..2fc91a3e80a
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports all the pull requests' do
+ importer = double(:importer)
+ client = double(:client)
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::PullRequestsImporter)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(waiter)
+
+ expect(project)
+ .to receive(:refresh_import_jid_expiration)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :issues_and_diff_notes)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
new file mode 100644
index 00000000000..adab535ac05
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
+ let(:project) { double(:project, id: 4) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ before do
+ expect(Gitlab::GithubImport::RefreshImportJidWorker)
+ .to receive(:perform_in_the_future)
+ .with(project.id, '123')
+
+ expect(worker)
+ .to receive(:jid)
+ .and_return('123')
+ end
+
+ context 'when the import succeeds' do
+ it 'schedules the importing of the base data' do
+ client = double(:client)
+
+ expect_any_instance_of(Gitlab::GithubImport::Importer::RepositoryImporter)
+ .to receive(:execute)
+ .and_return(true)
+
+ expect(Gitlab::GithubImport::Stage::ImportBaseDataWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+
+ worker.import(client, project)
+ end
+ end
+
+ context 'when the import fails' do
+ it 'does not schedule the importing of the base data' do
+ client = double(:client)
+
+ expect_any_instance_of(Gitlab::GithubImport::Importer::RepositoryImporter)
+ .to receive(:execute)
+ .and_return(false)
+
+ expect(Gitlab::GithubImport::Stage::ImportBaseDataWorker)
+ .not_to receive(:perform_async)
+
+ worker.import(client, project)
+ end
+ end
+ end
+end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 5cff5108477..0af537647ad 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -59,5 +59,28 @@ describe RepositoryImportWorker do
expect(project.reload.import_status).to eq('failed')
end
end
+
+ context 'when using an asynchronous importer' do
+ it 'does not mark the import process as finished' do
+ service = double(:service)
+
+ allow(Projects::ImportService)
+ .to receive(:new)
+ .and_return(service)
+
+ allow(service)
+ .to receive(:execute)
+ .and_return(true)
+
+ allow(service)
+ .to receive(:async?)
+ .and_return(true)
+
+ expect_any_instance_of(Project)
+ .not_to receive(:import_finish)
+
+ subject.perform(project.id)
+ end
+ end
end
end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 558ff9109ec..0fa19ac84bb 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -23,5 +23,17 @@ describe UpdateMergeRequestsWorker do
perform
end
+
+ context 'when slow' do
+ before do
+ stub_const("UpdateMergeRequestsWorker::LOG_TIME_THRESHOLD", -1)
+ end
+
+ it 'logs debug info' do
+ expect(Rails.logger).to receive(:info).with(a_string_matching(/\AUpdateMergeRequestsWorker#perform.*project_id=#{project.id},user_id=#{user.id},oldrev=#{oldrev},newrev=#{newrev},ref=#{ref}/))
+
+ perform
+ end
+ end
end
end
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
index dcd4a3b9aec..0e92b298178 100644
--- a/spec/workers/wait_for_cluster_creation_worker_spec.rb
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -2,65 +2,32 @@ require 'spec_helper'
describe WaitForClusterCreationWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
- before do
- allow(operation).to receive(:status).and_return(status)
- allow(operation).to receive(:start_time).and_return(1.minute.ago)
- allow(operation).to receive(:status_message).and_return('error')
- allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation)
- end
-
- context 'when operation status is RUNNING' do
- let(:status) { 'RUNNING' }
-
- it 'reschedules worker' do
- expect(described_class).to receive(:perform_in)
-
- described_class.new.perform(cluster.id)
- end
-
- context 'when operation timeout' do
- before do
- allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc)
- end
-
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute)
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when operation status is DONE' do
- let(:status) { 'DONE' }
-
- it 'finalizes cluster creation' do
- expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute)
-
- described_class.new.perform(cluster.id)
- end
+ described_class.new.perform(cluster.id)
end
+ end
- context 'when operation status is others' do
- let(:status) { 'others' }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- expect(cluster.reload).to be_errored
- end
+ described_class.new.perform(cluster.id)
end
end
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- described_class.new.perform(1234)
+ described_class.new.perform(123)
end
end
end
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
deleted file mode 100644
index cfa49e72c50..00000000000
--- a/vendor/assets/javascripts/autosize.js
+++ /dev/null
@@ -1,243 +0,0 @@
-/*!
- Autosize 3.0.14
- license: MIT
- http://www.jacklmoore.com/autosize
-*/
-(function (global, factory) {
- if (typeof define === 'function' && define.amd) {
- define(['exports', 'module'], factory);
- } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
- factory(exports, module);
- } else {
- var mod = {
- exports: {}
- };
- factory(mod.exports, mod);
- global.autosize = mod.exports;
- }
-})(this, function (exports, module) {
- 'use strict';
-
- var set = typeof Set === 'function' ? new Set() : (function () {
- var list = [];
-
- return {
- has: function has(key) {
- return Boolean(list.indexOf(key) > -1);
- },
- add: function add(key) {
- list.push(key);
- },
- 'delete': function _delete(key) {
- list.splice(list.indexOf(key), 1);
- } };
- })();
-
- function assign(ta) {
- var _ref = arguments[1] === undefined ? {} : arguments[1];
-
- var _ref$setOverflowX = _ref.setOverflowX;
- var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
- var _ref$setOverflowY = _ref.setOverflowY;
- var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
-
- if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
-
- var heightOffset = null;
- var overflowY = null;
- var clientWidth = ta.clientWidth;
-
- function init() {
- var style = window.getComputedStyle(ta, null);
-
- overflowY = style.overflowY;
-
- if (style.resize === 'vertical') {
- ta.style.resize = 'none';
- } else if (style.resize === 'both') {
- ta.style.resize = 'horizontal';
- }
-
- if (style.boxSizing === 'content-box') {
- heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
- } else {
- heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
- }
- // Fix when a textarea is not on document body and heightOffset is Not a Number
- if (isNaN(heightOffset)) {
- heightOffset = 0;
- }
-
- update();
- }
-
- function changeOverflow(value) {
- {
- // Chrome/Safari-specific fix:
- // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
- // made available by removing the scrollbar. The following forces the necessary text reflow.
- var width = ta.style.width;
- ta.style.width = '0px';
- // Force reflow:
- /* jshint ignore:start */
- ta.offsetWidth;
- /* jshint ignore:end */
- ta.style.width = width;
- }
-
- overflowY = value;
-
- if (setOverflowY) {
- ta.style.overflowY = value;
- }
-
- resize();
- }
-
- function resize() {
- var htmlTop = window.pageYOffset;
- var bodyTop = document.body.scrollTop;
- var originalHeight = ta.style.height;
-
- ta.style.height = 'auto';
-
- var endHeight = ta.scrollHeight + heightOffset;
-
- if (ta.scrollHeight === 0) {
- // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
- ta.style.height = originalHeight;
- return;
- }
-
- ta.style.height = endHeight + 'px';
-
- // used to check if an update is actually necessary on window.resize
- clientWidth = ta.clientWidth;
-
- // prevents scroll-position jumping
- document.documentElement.scrollTop = htmlTop;
- document.body.scrollTop = bodyTop;
- }
-
- function update() {
- var startHeight = ta.style.height;
-
- resize();
-
- var style = window.getComputedStyle(ta, null);
-
- if (style.height !== ta.style.height) {
- if (overflowY !== 'visible') {
- changeOverflow('visible');
- }
- } else {
- if (overflowY !== 'hidden') {
- changeOverflow('hidden');
- }
- }
-
- if (startHeight !== ta.style.height) {
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:resized', true, false);
- ta.dispatchEvent(evt);
- }
- }
-
- var pageResize = function pageResize() {
- if (ta.clientWidth !== clientWidth) {
- update();
- }
- };
-
- var destroy = (function (style) {
- window.removeEventListener('resize', pageResize, false);
- ta.removeEventListener('input', update, false);
- ta.removeEventListener('keyup', update, false);
- ta.removeEventListener('autosize:destroy', destroy, false);
- ta.removeEventListener('autosize:update', update, false);
- set['delete'](ta);
-
- Object.keys(style).forEach(function (key) {
- ta.style[key] = style[key];
- });
- }).bind(ta, {
- height: ta.style.height,
- resize: ta.style.resize,
- overflowY: ta.style.overflowY,
- overflowX: ta.style.overflowX,
- wordWrap: ta.style.wordWrap });
-
- ta.addEventListener('autosize:destroy', destroy, false);
-
- // IE9 does not fire onpropertychange or oninput for deletions,
- // so binding to onkeyup to catch most of those events.
- // There is no way that I know of to detect something like 'cut' in IE9.
- if ('onpropertychange' in ta && 'oninput' in ta) {
- ta.addEventListener('keyup', update, false);
- }
-
- window.addEventListener('resize', pageResize, false);
- ta.addEventListener('input', update, false);
- ta.addEventListener('autosize:update', update, false);
- set.add(ta);
-
- if (setOverflowX) {
- ta.style.overflowX = 'hidden';
- ta.style.wordWrap = 'break-word';
- }
-
- init();
- }
-
- function destroy(ta) {
- if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:destroy', true, false);
- ta.dispatchEvent(evt);
- }
-
- function update(ta) {
- if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:update', true, false);
- ta.dispatchEvent(evt);
- }
-
- var autosize = null;
-
- // Do nothing in Node.js environment and IE8 (or lower)
- if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
- autosize = function (el) {
- return el;
- };
- autosize.destroy = function (el) {
- return el;
- };
- autosize.update = function (el) {
- return el;
- };
- } else {
- autosize = function (el, options) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], function (x) {
- return assign(x, options);
- });
- }
- return el;
- };
- autosize.destroy = function (el) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], destroy);
- }
- return el;
- };
- autosize.update = function (el) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], update);
- }
- return el;
- };
- }
-
- module.exports = autosize;
-}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.js b/vendor/assets/javascripts/fuzzaldrin-plus.js
deleted file mode 100644
index 1985e3f8f6c..00000000000
--- a/vendor/assets/javascripts/fuzzaldrin-plus.js
+++ /dev/null
@@ -1,1161 +0,0 @@
-/*!
- * fuzzaldrin-plus.js - 0.3.1
- * https://github.com/jeancroy/fuzzaldrin-plus
- *
- * Copyright 2016 - Jean Christophe Roy
- * Released under the MIT license
- * https://github.com/jeancroy/fuzzaldrin-plus/raw/master/LICENSE.md
- */
-(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-fuzzaldrinPlus = require('fuzzaldrin-plus');
-
-},{"fuzzaldrin-plus":3}],2:[function(require,module,exports){
-(function() {
- var PathSeparator, legacy_scorer, pluckCandidates, scorer, sortCandidates;
-
- scorer = require('./scorer');
-
- legacy_scorer = require('./legacy');
-
- pluckCandidates = function(a) {
- return a.candidate;
- };
-
- sortCandidates = function(a, b) {
- return b.score - a.score;
- };
-
- PathSeparator = require('path').sep;
-
- module.exports = function(candidates, query, _arg) {
- var allowErrors, bAllowErrors, bKey, candidate, coreQuery, key, legacy, maxInners, maxResults, prepQuery, queryHasSlashes, score, scoredCandidates, spotLeft, string, _i, _j, _len, _len1, _ref;
- _ref = _arg != null ? _arg : {}, key = _ref.key, maxResults = _ref.maxResults, maxInners = _ref.maxInners, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
- scoredCandidates = [];
- spotLeft = (maxInners != null) && maxInners > 0 ? maxInners : candidates.length;
- bAllowErrors = !!allowErrors;
- bKey = key != null;
- prepQuery = scorer.prepQuery(query);
- if (!legacy) {
- for (_i = 0, _len = candidates.length; _i < _len; _i++) {
- candidate = candidates[_i];
- string = bKey ? candidate[key] : candidate;
- if (!string) {
- continue;
- }
- score = scorer.score(string, query, prepQuery, bAllowErrors);
- if (score > 0) {
- scoredCandidates.push({
- candidate: candidate,
- score: score
- });
- if (!--spotLeft) {
- break;
- }
- }
- }
- } else {
- queryHasSlashes = prepQuery.depth > 0;
- coreQuery = prepQuery.core;
- for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) {
- candidate = candidates[_j];
- string = key != null ? candidate[key] : candidate;
- if (!string) {
- continue;
- }
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
- if (!queryHasSlashes) {
- score = legacy_scorer.basenameScore(string, coreQuery, score);
- }
- if (score > 0) {
- scoredCandidates.push({
- candidate: candidate,
- score: score
- });
- }
- }
- }
- scoredCandidates.sort(sortCandidates);
- candidates = scoredCandidates.map(pluckCandidates);
- if (maxResults != null) {
- candidates = candidates.slice(0, maxResults);
- }
- return candidates;
- };
-
-}).call(this);
-
-},{"./legacy":4,"./scorer":6,"path":7}],3:[function(require,module,exports){
-(function() {
- var PathSeparator, filter, legacy_scorer, matcher, prepQueryCache, scorer;
-
- scorer = require('./scorer');
-
- legacy_scorer = require('./legacy');
-
- filter = require('./filter');
-
- matcher = require('./matcher');
-
- PathSeparator = require('path').sep;
-
- prepQueryCache = null;
-
- module.exports = {
- filter: function(candidates, query, options) {
- if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) {
- return [];
- }
- return filter(candidates, query, options);
- },
- prepQuery: function(query) {
- return scorer.prepQuery(query);
- },
- score: function(string, query, prepQuery, _arg) {
- var allowErrors, coreQuery, legacy, queryHasSlashes, score, _ref;
- _ref = _arg != null ? _arg : {}, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
- if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) {
- return 0;
- }
- if (prepQuery == null) {
- prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
- }
- if (!legacy) {
- score = scorer.score(string, query, prepQuery, !!allowErrors);
- } else {
- queryHasSlashes = prepQuery.depth > 0;
- coreQuery = prepQuery.core;
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
- if (!queryHasSlashes) {
- score = legacy_scorer.basenameScore(string, coreQuery, score);
- }
- }
- return score;
- },
- match: function(string, query, prepQuery, _arg) {
- var allowErrors, baseMatches, matches, query_lw, string_lw, _i, _ref, _results;
- allowErrors = (_arg != null ? _arg : {}).allowErrors;
- if (!string) {
- return [];
- }
- if (!query) {
- return [];
- }
- if (string === query) {
- return (function() {
- _results = [];
- for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
- return _results;
- }).apply(this);
- }
- if (prepQuery == null) {
- prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
- }
- if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
- return [];
- }
- string_lw = string.toLowerCase();
- query_lw = prepQuery.query_lw;
- matches = matcher.match(string, string_lw, prepQuery);
- if (matches.length === 0) {
- return matches;
- }
- if (string.indexOf(PathSeparator) > -1) {
- baseMatches = matcher.basenameMatch(string, string_lw, prepQuery);
- matches = matcher.mergeMatches(matches, baseMatches);
- }
- return matches;
- }
- };
-
-}).call(this);
-
-},{"./filter":2,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],4:[function(require,module,exports){
-(function() {
- var PathSeparator, queryIsLastPathSegment;
-
- PathSeparator = require('path').sep;
-
- exports.basenameScore = function(string, query, score) {
- var base, depth, index, lastCharacter, segmentCount, slashCount;
- index = string.length - 1;
- while (string[index] === PathSeparator) {
- index--;
- }
- slashCount = 0;
- lastCharacter = index;
- base = null;
- while (index >= 0) {
- if (string[index] === PathSeparator) {
- slashCount++;
- if (base == null) {
- base = string.substring(index + 1, lastCharacter + 1);
- }
- } else if (index === 0) {
- if (lastCharacter < string.length - 1) {
- if (base == null) {
- base = string.substring(0, lastCharacter + 1);
- }
- } else {
- if (base == null) {
- base = string;
- }
- }
- }
- index--;
- }
- if (base === string) {
- score *= 2;
- } else if (base) {
- score += exports.score(base, query);
- }
- segmentCount = slashCount + 1;
- depth = Math.max(1, 10 - segmentCount);
- score *= depth * 0.01;
- return score;
- };
-
- exports.score = function(string, query) {
- var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref;
- if (string === query) {
- return 1;
- }
- if (queryIsLastPathSegment(string, query)) {
- return 1;
- }
- totalCharacterScore = 0;
- queryLength = query.length;
- stringLength = string.length;
- indexInQuery = 0;
- indexInString = 0;
- while (indexInQuery < queryLength) {
- character = query[indexInQuery++];
- lowerCaseIndex = string.indexOf(character.toLowerCase());
- upperCaseIndex = string.indexOf(character.toUpperCase());
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
- if (minIndex === -1) {
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
- }
- indexInString = minIndex;
- if (indexInString === -1) {
- return 0;
- }
- characterScore = 0.1;
- if (string[indexInString] === character) {
- characterScore += 0.1;
- }
- if (indexInString === 0 || string[indexInString - 1] === PathSeparator) {
- characterScore += 0.8;
- } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') {
- characterScore += 0.7;
- }
- string = string.substring(indexInString + 1, stringLength);
- totalCharacterScore += characterScore;
- }
- queryScore = totalCharacterScore / queryLength;
- return ((queryScore * (queryLength / stringLength)) + queryScore) / 2;
- };
-
- queryIsLastPathSegment = function(string, query) {
- if (string[string.length - query.length - 1] === PathSeparator) {
- return string.lastIndexOf(query) === string.length - query.length;
- }
- };
-
- exports.match = function(string, query, stringOffset) {
- var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results;
- if (stringOffset == null) {
- stringOffset = 0;
- }
- if (string === query) {
- return (function() {
- _results = [];
- for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); }
- return _results;
- }).apply(this);
- }
- queryLength = query.length;
- stringLength = string.length;
- indexInQuery = 0;
- indexInString = 0;
- matches = [];
- while (indexInQuery < queryLength) {
- character = query[indexInQuery++];
- lowerCaseIndex = string.indexOf(character.toLowerCase());
- upperCaseIndex = string.indexOf(character.toUpperCase());
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
- if (minIndex === -1) {
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
- }
- indexInString = minIndex;
- if (indexInString === -1) {
- return [];
- }
- matches.push(stringOffset + indexInString);
- stringOffset += indexInString + 1;
- string = string.substring(indexInString + 1, stringLength);
- }
- return matches;
- };
-
-}).call(this);
-
-},{"path":7}],5:[function(require,module,exports){
-(function() {
- var PathSeparator, scorer;
-
- PathSeparator = require('path').sep;
-
- scorer = require('./scorer');
-
- exports.basenameMatch = function(subject, subject_lw, prepQuery) {
- var basePos, depth, end;
- end = subject.length - 1;
- while (subject[end] === PathSeparator) {
- end--;
- }
- basePos = subject.lastIndexOf(PathSeparator, end);
- if (basePos === -1) {
- return [];
- }
- depth = prepQuery.depth;
- while (depth-- > 0) {
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
- if (basePos === -1) {
- return [];
- }
- }
- basePos++;
- end++;
- return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos);
- };
-
- exports.mergeMatches = function(a, b) {
- var ai, bj, i, j, m, n, out;
- m = a.length;
- n = b.length;
- if (n === 0) {
- return a.slice();
- }
- if (m === 0) {
- return b.slice();
- }
- i = -1;
- j = 0;
- bj = b[j];
- out = [];
- while (++i < m) {
- ai = a[i];
- while (bj <= ai && ++j < n) {
- if (bj < ai) {
- out.push(bj);
- }
- bj = b[j];
- }
- out.push(ai);
- }
- while (j < n) {
- out.push(b[j++]);
- }
- return out;
- };
-
- exports.match = function(subject, subject_lw, prepQuery, offset) {
- var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace;
- if (offset == null) {
- offset = 0;
- }
- query = prepQuery.query;
- query_lw = prepQuery.query_lw;
- m = subject.length;
- n = query.length;
- acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score;
- score_row = new Array(n);
- csc_row = new Array(n);
- STOP = 0;
- UP = 1;
- LEFT = 2;
- DIAGONAL = 3;
- trace = new Array(m * n);
- pos = -1;
- j = -1;
- while (++j < n) {
- score_row[j] = 0;
- csc_row[j] = 0;
- }
- i = -1;
- while (++i < m) {
- score = 0;
- score_up = 0;
- csc_diag = 0;
- si_lw = subject_lw[i];
- j = -1;
- while (++j < n) {
- csc_score = 0;
- align = 0;
- score_diag = score_up;
- if (query_lw[j] === si_lw) {
- start = scorer.isWordStart(i, subject, subject_lw);
- csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
- align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score);
- }
- score_up = score_row[j];
- csc_diag = csc_row[j];
- if (score > score_up) {
- move = LEFT;
- } else {
- score = score_up;
- move = UP;
- }
- if (align > score) {
- score = align;
- move = DIAGONAL;
- } else {
- csc_score = 0;
- }
- score_row[j] = score;
- csc_row[j] = csc_score;
- trace[++pos] = score > 0 ? move : STOP;
- }
- }
- i = m - 1;
- j = n - 1;
- pos = i * n + j;
- backtrack = true;
- matches = [];
- while (backtrack && i >= 0 && j >= 0) {
- switch (trace[pos]) {
- case UP:
- i--;
- pos -= n;
- break;
- case LEFT:
- j--;
- pos--;
- break;
- case DIAGONAL:
- matches.push(i + offset);
- j--;
- i--;
- pos -= n + 1;
- break;
- default:
- backtrack = false;
- }
- }
- matches.reverse();
- return matches;
- };
-
-}).call(this);
-
-},{"./scorer":6,"path":7}],6:[function(require,module,exports){
-(function() {
- var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, file_coeff, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm;
-
- PathSeparator = require('path').sep;
-
- wm = 150;
-
- pos_bonus = 20;
-
- tau_depth = 13;
-
- tau_size = 85;
-
- file_coeff = 1.2;
-
- miss_coeff = 0.75;
-
- opt_char_re = /[ _\-:\/\\]/g;
-
- exports.coreChars = coreChars = function(query) {
- return query.replace(opt_char_re, '');
- };
-
- exports.score = function(string, query, prepQuery, allowErrors) {
- var score, string_lw;
- if (prepQuery == null) {
- prepQuery = new Query(query);
- }
- if (allowErrors == null) {
- allowErrors = false;
- }
- if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
- return 0;
- }
- string_lw = string.toLowerCase();
- score = doScore(string, string_lw, prepQuery);
- return Math.ceil(basenameScore(string, string_lw, prepQuery, score));
- };
-
- Query = (function() {
- function Query(query) {
- if (!(query != null ? query.length : void 0)) {
- return null;
- }
- this.query = query;
- this.query_lw = query.toLowerCase();
- this.core = coreChars(query);
- this.core_lw = this.core.toLowerCase();
- this.core_up = truncatedUpperCase(this.core);
- this.depth = countDir(query, query.length);
- }
-
- return Query;
-
- })();
-
- exports.prepQuery = function(query) {
- return new Query(query);
- };
-
- exports.isMatch = isMatch = function(subject, query_lw, query_up) {
- var i, j, m, n, qj_lw, qj_up, si;
- m = subject.length;
- n = query_lw.length;
- if (!m || n > m) {
- return false;
- }
- i = -1;
- j = -1;
- while (++j < n) {
- qj_lw = query_lw[j];
- qj_up = query_up[j];
- while (++i < m) {
- si = subject[i];
- if (si === qj_lw || si === qj_up) {
- break;
- }
- }
- if (i === m) {
- return false;
- }
- }
- return true;
- };
-
- doScore = function(subject, subject_lw, prepQuery) {
- var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
- query = prepQuery.query;
- query_lw = prepQuery.query_lw;
- m = subject.length;
- n = query.length;
- acro = scoreAcronyms(subject, subject_lw, query, query_lw);
- acro_score = acro.score;
- if (acro.count === n) {
- return scoreExact(n, m, acro_score, acro.pos);
- }
- pos = subject_lw.indexOf(query_lw);
- if (pos > -1) {
- return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
- }
- score_row = new Array(n);
- csc_row = new Array(n);
- sz = scoreSize(n, m);
- miss_budget = Math.ceil(miss_coeff * n) + 5;
- miss_left = miss_budget;
- j = -1;
- while (++j < n) {
- score_row[j] = 0;
- csc_row[j] = 0;
- }
- i = subject_lw.indexOf(query_lw[0]);
- if (i > -1) {
- i--;
- }
- mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
- if (mm > i) {
- m = mm + 1;
- }
- while (++i < m) {
- score = 0;
- score_diag = 0;
- csc_diag = 0;
- si_lw = subject_lw[i];
- record_miss = true;
- j = -1;
- while (++j < n) {
- score_up = score_row[j];
- if (score_up > score) {
- score = score_up;
- }
- csc_score = 0;
- if (query_lw[j] === si_lw) {
- start = isWordStart(i, subject, subject_lw);
- csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
- align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
- if (align > score) {
- score = align;
- miss_left = miss_budget;
- } else {
- if (record_miss && --miss_left <= 0) {
- return score_row[n - 1] * sz;
- }
- record_miss = false;
- }
- }
- score_diag = score_up;
- csc_diag = csc_row[j];
- csc_row[j] = csc_score;
- score_row[j] = score;
- }
- }
- return score * sz;
- };
-
- exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
- var curr_s, prev_s;
- if (pos === 0) {
- return true;
- }
- curr_s = subject[pos];
- prev_s = subject[pos - 1];
- return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
- };
-
- exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
- var curr_s, next_s;
- if (pos === len - 1) {
- return true;
- }
- curr_s = subject[pos];
- next_s = subject[pos + 1];
- return isSeparator(curr_s) || isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
- };
-
- isSeparator = function(c) {
- return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
- };
-
- scorePosition = function(pos) {
- var sc;
- if (pos < pos_bonus) {
- sc = pos_bonus - pos;
- return 100 + sc * sc;
- } else {
- return Math.max(100 + pos_bonus - pos, 0);
- }
- };
-
- scoreSize = function(n, m) {
- return tau_size / (tau_size + Math.abs(m - n));
- };
-
- scoreExact = function(n, m, quality, pos) {
- return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
- };
-
- exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
- var bonus, sz;
- sz = count;
- bonus = 6;
- if (sameCase === count) {
- bonus += 2;
- }
- if (start) {
- bonus += 3;
- }
- if (end) {
- bonus += 1;
- }
- if (count === len) {
- if (start) {
- if (sameCase === len) {
- sz += 2;
- } else {
- sz += 1;
- }
- }
- if (end) {
- bonus += 1;
- }
- }
- return sameCase + sz * (sz + bonus);
- };
-
- exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
- var posBonus;
- posBonus = scorePosition(i);
- if (start) {
- return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
- }
- return posBonus + wm * csc_score;
- };
-
- exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) {
- var k, m, mi, n, nj, sameCase, startPos, sz;
- m = subject.length;
- n = query.length;
- mi = m - i;
- nj = n - j;
- k = mi < nj ? mi : nj;
- startPos = i;
- sameCase = 0;
- sz = 0;
- if (query[j] === subject[i]) {
- sameCase++;
- }
- while (++sz < k && query_lw[++j] === subject_lw[++i]) {
- if (query[j] === subject[i]) {
- sameCase++;
- }
- }
- if (sz === 1) {
- return 1 + 2 * sameCase;
- }
- return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m));
- };
-
- exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
- var end, i, pos2, sameCase, start;
- start = isWordStart(pos, subject, subject_lw);
- if (!start) {
- pos2 = subject_lw.indexOf(query_lw, pos + 1);
- if (pos2 > -1) {
- start = isWordStart(pos2, subject, subject_lw);
- if (start) {
- pos = pos2;
- }
- }
- }
- i = -1;
- sameCase = 0;
- while (++i < n) {
- if (query[pos + i] === subject[i]) {
- sameCase++;
- }
- }
- end = isWordEnd(pos + n - 1, subject, subject_lw, m);
- return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
- };
-
- AcronymResult = (function() {
- function AcronymResult(score, pos, count) {
- this.score = score;
- this.pos = pos;
- this.count = count;
- }
-
- return AcronymResult;
-
- })();
-
- emptyAcronymResult = new AcronymResult(0, 0.1, 0);
-
- exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
- var count, i, j, m, n, pos, qj_lw, sameCase, score;
- m = subject.length;
- n = query.length;
- if (!(m > 1 && n > 1)) {
- return emptyAcronymResult;
- }
- count = 0;
- pos = 0;
- sameCase = 0;
- i = -1;
- j = -1;
- while (++j < n) {
- qj_lw = query_lw[j];
- while (++i < m) {
- if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
- if (query[j] === subject[i]) {
- sameCase++;
- }
- pos += i;
- count++;
- break;
- }
- }
- if (i === m) {
- break;
- }
- }
- if (count < 2) {
- return emptyAcronymResult;
- }
- score = scorePattern(count, n, sameCase, true, false);
- return new AcronymResult(score, pos / count, count);
- };
-
- basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) {
- var alpha, basePathScore, basePos, depth, end;
- if (fullPathScore === 0) {
- return 0;
- }
- end = subject.length - 1;
- while (subject[end] === PathSeparator) {
- end--;
- }
- basePos = subject.lastIndexOf(PathSeparator, end);
- if (basePos === -1) {
- return fullPathScore;
- }
- depth = prepQuery.depth;
- while (depth-- > 0) {
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
- if (basePos === -1) {
- return fullPathScore;
- }
- }
- basePos++;
- end++;
- basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery);
- alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1));
- return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos));
- };
-
- exports.countDir = countDir = function(path, end) {
- var count, i;
- if (end < 1) {
- return 0;
- }
- count = 0;
- i = -1;
- while (++i < end && path[i] === PathSeparator) {
- continue;
- }
- while (++i < end) {
- if (path[i] === PathSeparator) {
- count++;
- while (++i < end && path[i] === PathSeparator) {
- continue;
- }
- }
- }
- return count;
- };
-
- truncatedUpperCase = function(str) {
- var char, upper, _i, _len;
- upper = "";
- for (_i = 0, _len = str.length; _i < _len; _i++) {
- char = str[_i];
- upper += char.toUpperCase()[0];
- }
- return upper;
- };
-
-}).call(this);
-
-},{"path":7}],7:[function(require,module,exports){
-(function (process){
-// Copyright Joyent, Inc. and other Node contributors.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to permit
-// persons to whom the Software is furnished to do so, subject to the
-// following conditions:
-//
-// The above copyright notice and this permission notice shall be included
-// in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
-// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
-// USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-// resolves . and .. elements in a path array with directory names there
-// must be no slashes, empty elements, or device names (c:\) in the array
-// (so also no leading and trailing slashes - it does not distinguish
-// relative and absolute paths)
-function normalizeArray(parts, allowAboveRoot) {
- // if the path tries to go above the root, `up` ends up > 0
- var up = 0;
- for (var i = parts.length - 1; i >= 0; i--) {
- var last = parts[i];
- if (last === '.') {
- parts.splice(i, 1);
- } else if (last === '..') {
- parts.splice(i, 1);
- up++;
- } else if (up) {
- parts.splice(i, 1);
- up--;
- }
- }
-
- // if the path is allowed to go above the root, restore leading ..s
- if (allowAboveRoot) {
- for (; up--; up) {
- parts.unshift('..');
- }
- }
-
- return parts;
-}
-
-// Split a filename into [root, dir, basename, ext], unix version
-// 'root' is just a slash, or nothing.
-var splitPathRe =
- /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
-var splitPath = function(filename) {
- return splitPathRe.exec(filename).slice(1);
-};
-
-// path.resolve([from ...], to)
-// posix version
-exports.resolve = function() {
- var resolvedPath = '',
- resolvedAbsolute = false;
-
- for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
- var path = (i >= 0) ? arguments[i] : process.cwd();
-
- // Skip empty and invalid entries
- if (typeof path !== 'string') {
- throw new TypeError('Arguments to path.resolve must be strings');
- } else if (!path) {
- continue;
- }
-
- resolvedPath = path + '/' + resolvedPath;
- resolvedAbsolute = path.charAt(0) === '/';
- }
-
- // At this point the path should be resolved to a full absolute path, but
- // handle relative paths to be safe (might happen when process.cwd() fails)
-
- // Normalize the path
- resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
- return !!p;
- }), !resolvedAbsolute).join('/');
-
- return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
-};
-
-// path.normalize(path)
-// posix version
-exports.normalize = function(path) {
- var isAbsolute = exports.isAbsolute(path),
- trailingSlash = substr(path, -1) === '/';
-
- // Normalize the path
- path = normalizeArray(filter(path.split('/'), function(p) {
- return !!p;
- }), !isAbsolute).join('/');
-
- if (!path && !isAbsolute) {
- path = '.';
- }
- if (path && trailingSlash) {
- path += '/';
- }
-
- return (isAbsolute ? '/' : '') + path;
-};
-
-// posix version
-exports.isAbsolute = function(path) {
- return path.charAt(0) === '/';
-};
-
-// posix version
-exports.join = function() {
- var paths = Array.prototype.slice.call(arguments, 0);
- return exports.normalize(filter(paths, function(p, index) {
- if (typeof p !== 'string') {
- throw new TypeError('Arguments to path.join must be strings');
- }
- return p;
- }).join('/'));
-};
-
-
-// path.relative(from, to)
-// posix version
-exports.relative = function(from, to) {
- from = exports.resolve(from).substr(1);
- to = exports.resolve(to).substr(1);
-
- function trim(arr) {
- var start = 0;
- for (; start < arr.length; start++) {
- if (arr[start] !== '') break;
- }
-
- var end = arr.length - 1;
- for (; end >= 0; end--) {
- if (arr[end] !== '') break;
- }
-
- if (start > end) return [];
- return arr.slice(start, end - start + 1);
- }
-
- var fromParts = trim(from.split('/'));
- var toParts = trim(to.split('/'));
-
- var length = Math.min(fromParts.length, toParts.length);
- var samePartsLength = length;
- for (var i = 0; i < length; i++) {
- if (fromParts[i] !== toParts[i]) {
- samePartsLength = i;
- break;
- }
- }
-
- var outputParts = [];
- for (var i = samePartsLength; i < fromParts.length; i++) {
- outputParts.push('..');
- }
-
- outputParts = outputParts.concat(toParts.slice(samePartsLength));
-
- return outputParts.join('/');
-};
-
-exports.sep = '/';
-exports.delimiter = ':';
-
-exports.dirname = function(path) {
- var result = splitPath(path),
- root = result[0],
- dir = result[1];
-
- if (!root && !dir) {
- // No dirname whatsoever
- return '.';
- }
-
- if (dir) {
- // It has a dirname, strip trailing slash
- dir = dir.substr(0, dir.length - 1);
- }
-
- return root + dir;
-};
-
-
-exports.basename = function(path, ext) {
- var f = splitPath(path)[2];
- // TODO: make this comparison case-insensitive on windows?
- if (ext && f.substr(-1 * ext.length) === ext) {
- f = f.substr(0, f.length - ext.length);
- }
- return f;
-};
-
-
-exports.extname = function(path) {
- return splitPath(path)[3];
-};
-
-function filter (xs, f) {
- if (xs.filter) return xs.filter(f);
- var res = [];
- for (var i = 0; i < xs.length; i++) {
- if (f(xs[i], i, xs)) res.push(xs[i]);
- }
- return res;
-}
-
-// String.prototype.substr - negative index don't work in IE8
-var substr = 'ab'.substr(-1) === 'b'
- ? function (str, start, len) { return str.substr(start, len) }
- : function (str, start, len) {
- if (start < 0) start = str.length + start;
- return str.substr(start, len);
- }
-;
-
-}).call(this,require('_process'))
-},{"_process":8}],8:[function(require,module,exports){
-// shim for using process in browser
-
-var process = module.exports = {};
-var queue = [];
-var draining = false;
-var currentQueue;
-var queueIndex = -1;
-
-function cleanUpNextTick() {
- draining = false;
- if (currentQueue.length) {
- queue = currentQueue.concat(queue);
- } else {
- queueIndex = -1;
- }
- if (queue.length) {
- drainQueue();
- }
-}
-
-function drainQueue() {
- if (draining) {
- return;
- }
- var timeout = setTimeout(cleanUpNextTick);
- draining = true;
-
- var len = queue.length;
- while(len) {
- currentQueue = queue;
- queue = [];
- while (++queueIndex < len) {
- if (currentQueue) {
- currentQueue[queueIndex].run();
- }
- }
- queueIndex = -1;
- len = queue.length;
- }
- currentQueue = null;
- draining = false;
- clearTimeout(timeout);
-}
-
-process.nextTick = function (fun) {
- var args = new Array(arguments.length - 1);
- if (arguments.length > 1) {
- for (var i = 1; i < arguments.length; i++) {
- args[i - 1] = arguments[i];
- }
- }
- queue.push(new Item(fun, args));
- if (queue.length === 1 && !draining) {
- setTimeout(drainQueue, 0);
- }
-};
-
-// v8 likes predictible objects
-function Item(fun, array) {
- this.fun = fun;
- this.array = array;
-}
-Item.prototype.run = function () {
- this.fun.apply(null, this.array);
-};
-process.title = 'browser';
-process.browser = true;
-process.env = {};
-process.argv = [];
-process.version = ''; // empty string to avoid regexp issues
-process.versions = {};
-
-function noop() {}
-
-process.on = noop;
-process.addListener = noop;
-process.once = noop;
-process.off = noop;
-process.removeListener = noop;
-process.removeAllListeners = noop;
-process.emit = noop;
-
-process.binding = function (name) {
- throw new Error('process.binding is not supported');
-};
-
-process.cwd = function () { return '/' };
-process.chdir = function (dir) {
- throw new Error('process.chdir is not supported');
-};
-process.umask = function() { return 0; };
-
-},{}]},{},[1]);
diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js
deleted file mode 100644
index da37966b28a..00000000000
--- a/vendor/assets/javascripts/latinise.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// Converting text to basic latin (aka removing accents)
-//
-// Based on: http://semplicewebsites.com/removing-accents-javascript
-//
-var Latinise = {
- map: {"Ã":"A","Ä‚":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ç":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Çž":"A","Ȧ":"A","Ç ":"A","Ạ":"A","È€":"A","À":"A","Ả":"A","È‚":"A","Ä€":"A","Ä„":"A","Ã…":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ç¢":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Æ":"B","Ḇ":"B","Ƀ":"B","Æ‚":"B","Ć":"C","ÄŒ":"C","Ç":"C","Ḉ":"C","Ĉ":"C","ÄŠ":"C","Ƈ":"C","È»":"C","ÄŽ":"D","á¸":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","ÆŠ":"D","Ḏ":"D","Dz":"D","Ç…":"D","Ä":"D","Æ‹":"D","DZ":"DZ","Ç„":"DZ","É":"E","Ä”":"E","Äš":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ä–":"E","Ẹ":"E","È„":"E","È":"E","Ẻ":"E","Ȇ":"E","Ä’":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","êª":"ET","Ḟ":"F","Æ‘":"F","Ç´":"G","Äž":"G","Ǧ":"G","Ä¢":"G","Äœ":"G","Ä ":"G","Æ“":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Èž":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Ã":"I","Ĭ":"I","Ç":"I","ÃŽ":"I","Ã":"I","Ḯ":"I","Ä°":"I","Ị":"I","Ȉ":"I","ÃŒ":"I","Ỉ":"I","ÈŠ":"I","Ī":"I","Ä®":"I","Æ—":"I","Ĩ":"I","Ḭ":"I","ê¹":"D","ê»":"F","ê½":"G","êž‚":"R","êž„":"S","Ꞇ":"T","ê¬":"IS","Ä´":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","ê‚":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","ê€":"K","ê„":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ä»":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","â± ":"L","êˆ":"L","Ḻ":"L","Ä¿":"L","â±¢":"L","Lj":"L","Å":"L","LJ":"LJ","Ḿ":"M","á¹€":"M","Ṃ":"M","â±®":"M","Ń":"N","Ň":"N","Å…":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Æ":"N","Ṉ":"N","È ":"N","Ç‹":"N","Ñ":"N","ÇŠ":"NJ","Ó":"O","ÅŽ":"O","Ç‘":"O","Ô":"O","á»":"O","Ộ":"O","á»’":"O","á»”":"O","á»–":"O","Ö":"O","Ȫ":"O","È®":"O","È°":"O","Ọ":"O","Å":"O","ÈŒ":"O","Ã’":"O","Ỏ":"O","Æ ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","á» ":"O","ÈŽ":"O","êŠ":"O","êŒ":"O","ÅŒ":"O","á¹’":"O","á¹":"O","ÆŸ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Æ¢":"OI","êŽ":"OO","Æ":"E","Ɔ":"O","È¢":"OU","á¹”":"P","á¹–":"P","ê’":"P","Ƥ":"P","ê”":"P","â±£":"P","ê":"P","ê˜":"Q","ê–":"Q","Å”":"R","Ř":"R","Å–":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","È":"R","È’":"R","Ṟ":"R","ÉŒ":"R","Ɽ":"R","Ꜿ":"C","ÆŽ":"E","Åš":"S","Ṥ":"S","Å ":"S","Ṧ":"S","Åž":"S","Åœ":"S","Ș":"S","á¹ ":"S","á¹¢":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Å¢":"T","á¹°":"T","Èš":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","á¹®":"T","Æ®":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Æœ":"M","É…":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ç“":"U","Û":"U","Ṷ":"U","Ãœ":"U","Ç—":"U","Ç™":"U","Ç›":"U","Ç•":"U","á¹²":"U","Ụ":"U","Å°":"U","È”":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","á»°":"U","Ừ":"U","Ử":"U","á»®":"U","È–":"U","Ū":"U","Ṻ":"U","Ų":"U","Å®":"U","Ũ":"U","Ṹ":"U","á¹´":"U","êž":"V","á¹¾":"V","Ʋ":"V","á¹¼":"V","ê ":"VY","Ẃ":"W","Å´":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","â±²":"W","Ẍ":"X","Ẋ":"X","Ã":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","á»´":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","ÉŽ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","áº":"Z","Ⱬ":"Z","Å»":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Å’":"OE","á´€":"A","á´":"AE","Ê™":"B","á´ƒ":"B","á´„":"C","á´…":"D","á´‡":"E","ꜰ":"F","É¢":"G","Ê›":"G","Êœ":"H","ɪ":"I","Ê":"R","á´Š":"J","á´‹":"K","ÊŸ":"L","á´Œ":"L","á´":"M","É´":"N","á´":"O","ɶ":"OE","á´":"O","á´•":"OU","á´˜":"P","Ê€":"R","á´Ž":"N","á´™":"R","ꜱ":"S","á´›":"T","â±»":"E","á´š":"R","á´œ":"U","á´ ":"V","á´¡":"W","Ê":"Y","á´¢":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ÇŽ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ÇŸ":"a","ȧ":"a","Ç¡":"a","ạ":"a","È":"a","à":"a","ả":"a","ȃ":"a","Ä":"a","Ä…":"a","á¶":"a","ẚ":"a","Ã¥":"a","Ç»":"a","á¸":"a","â±¥":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","Ç£":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","É“":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","Æ€":"b","ƃ":"b","ɵ":"o","ć":"c","Ä":"c","ç":"c","ḉ":"c","ĉ":"c","É•":"c","Ä‹":"c","ƈ":"c","ȼ":"c","Ä":"d","ḑ":"d","ḓ":"d","È¡":"d","ḋ":"d","á¸":"d","É—":"d","ᶑ":"d","á¸":"d","áµ­":"d","á¶":"d","Ä‘":"d","É–":"d","ÆŒ":"d","ı":"i","È·":"j","ÉŸ":"j","Ê„":"j","dz":"dz","dž":"dz","é":"e","Ä•":"e","Ä›":"e","È©":"e","á¸":"e","ê":"e","ế":"e","ệ":"e","á»":"e","ể":"e","á»…":"e","ḙ":"e","ë":"e","Ä—":"e","ẹ":"e","È…":"e","è":"e","ẻ":"e","ȇ":"e","Ä“":"e","ḗ":"e","ḕ":"e","ⱸ":"e","Ä™":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ê«":"et","ḟ":"f","Æ’":"f","áµ®":"f","ᶂ":"f","ǵ":"g","ÄŸ":"g","ǧ":"g","Ä£":"g","Ä":"g","Ä¡":"g","É ":"g","ḡ":"g","ᶃ":"g","Ç¥":"g","ḫ":"h","ÈŸ":"h","ḩ":"h","Ä¥":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","Æ•":"hv","í":"i","Ä­":"i","Ç":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","È‹":"i","Ä«":"i","į":"i","ᶖ":"i","ɨ":"i","Ä©":"i","ḭ":"i","êº":"d","ê¼":"f","áµ¹":"g","ꞃ":"r","êž…":"s","ꞇ":"t","ê­":"is","Ç°":"j","ĵ":"j","Ê":"j","ɉ":"j","ḱ":"k","Ç©":"k","Ä·":"k","ⱪ":"k","êƒ":"k","ḳ":"k","Æ™":"k","ḵ":"k","ᶄ":"k","ê":"k","ê…":"k","ĺ":"l","Æš":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","È´":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ê‰":"l","ḻ":"l","Å€":"l","É«":"l","ᶅ":"l","É­":"l","Å‚":"l","lj":"lj","Å¿":"s","ẜ":"s","ẛ":"s","áº":"s","ḿ":"m","á¹":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","Å„":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","á¹…":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","Æž":"n","áµ°":"n","ᶇ":"n","ɳ":"n","ñ":"n","ÇŒ":"nj","ó":"o","Å":"o","Ç’":"o","ô":"o","ố":"o","á»™":"o","ồ":"o","ổ":"o","á»—":"o","ö":"o","È«":"o","ȯ":"o","ȱ":"o","á»":"o","Å‘":"o","È":"o","ò":"o","á»":"o","Æ¡":"o","á»›":"o","ợ":"o","á»":"o","ở":"o","ỡ":"o","È":"o","ê‹":"o","ê":"o","ⱺ":"o","Å":"o","ṓ":"o","ṑ":"o","Ç«":"o","Ç­":"o","ø":"o","Ç¿":"o","õ":"o","á¹":"o","á¹":"o","È­":"o","Æ£":"oi","ê":"oo","É›":"e","ᶓ":"e","É”":"o","ᶗ":"o","È£":"ou","ṕ":"p","á¹—":"p","ê“":"p","Æ¥":"p","áµ±":"p","ᶈ":"p","ê•":"p","áµ½":"p","ê‘":"p","ê™":"q","Ê ":"q","É‹":"q","ê—":"q","Å•":"r","Å™":"r","Å—":"r","á¹™":"r","á¹›":"r","á¹":"r","È‘":"r","ɾ":"r","áµ³":"r","È“":"r","ṟ":"r","ɼ":"r","áµ²":"r","ᶉ":"r","É":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","É¿":"r","Å›":"s","á¹¥":"s","Å¡":"s","ṧ":"s","ÅŸ":"s","Å":"s","È™":"s","ṡ":"s","á¹£":"s","ṩ":"s","Ê‚":"s","áµ´":"s","ᶊ":"s","È¿":"s","É¡":"g","ß":"ss","á´‘":"o","á´“":"o","á´":"u","Å¥":"t","Å£":"t","á¹±":"t","È›":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","á¹­":"t","Æ­":"t","ṯ":"t","áµµ":"t","Æ«":"t","ʈ":"t","ŧ":"t","ᵺ":"th","É":"a","á´‚":"ae","Ç":"e","áµ·":"g","É¥":"h","Ê®":"h","ʯ":"h","á´‰":"i","Êž":"k","êž":"l","ɯ":"m","É°":"m","á´”":"oe","ɹ":"r","É»":"r","ɺ":"r","â±¹":"r","ʇ":"t","ÊŒ":"v","Ê":"w","ÊŽ":"y","ꜩ":"tz","ú":"u","Å­":"u","Ç”":"u","û":"u","á¹·":"u","ü":"u","ǘ":"u","Çš":"u","Çœ":"u","Ç–":"u","á¹³":"u","ụ":"u","ű":"u","È•":"u","ù":"u","ủ":"u","Æ°":"u","ứ":"u","á»±":"u","ừ":"u","á»­":"u","ữ":"u","È—":"u","Å«":"u","á¹»":"u","ų":"u","ᶙ":"u","ů":"u","Å©":"u","á¹¹":"u","á¹µ":"u","ᵫ":"ue","ê¸":"um","â±´":"v","êŸ":"v","ṿ":"v","Ê‹":"v","ᶌ":"v","â±±":"v","á¹½":"v","ê¡":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","áº":"w","â±³":"w","ẘ":"w","áº":"x","ẋ":"x","á¶":"x","ý":"y","Å·":"y","ÿ":"y","áº":"y","ỵ":"y","ỳ":"y","Æ´":"y","á»·":"y","ỿ":"y","ȳ":"y","ẙ":"y","É":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","Ê‘":"z","ⱬ":"z","ż":"z","ẓ":"z","È¥":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","Ê":"z","ƶ":"z","É€":"z","ff":"ff","ffi":"ffi","ffl":"ffl","ï¬":"fi","fl":"fl","ij":"ij","Å“":"oe","st":"st","â‚":"a","â‚‘":"e","áµ¢":"i","â±¼":"j","â‚’":"o","áµ£":"r","ᵤ":"u","áµ¥":"v","â‚“":"x"}
-};
-
-String.prototype.latinise = function() {
- return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
-};
diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js
index f7e77de34ff..6a341a3f0fe 100644
--- a/vendor/assets/javascripts/peek.js
+++ b/vendor/assets/javascripts/peek.js
@@ -1,5 +1,14 @@
+/*
+ * This is a modified version of https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js
+ *
+ * - Removed the dependency on jquery.tipsy
+ * - Removed the initializeTipsy and toggleBar functions
+ * - Customized updatePerformanceBar to handle SQL queries report specificities
+ * - Changed /peek/results to /-/peek/results
+ * - Removed the keypress, pjax:end, page:change, and turbolinks:load handlers
+ */
(function($) {
- var fetchRequestResults, getRequestId, peekEnabled, toggleBar, updatePerformanceBar;
+ var fetchRequestResults, getRequestId, peekEnabled, updatePerformanceBar;
getRequestId = function() {
return $('#peek').data('request-id');
};
@@ -41,22 +50,6 @@
});
return $(document).trigger('peek:render', [getRequestId(), results]);
};
- toggleBar = function(event) {
- var wrapper;
- if ($(event.target).is(':input')) {
- return;
- }
- if (event.which === 96 && !event.metaKey) {
- wrapper = $('#peek');
- if (wrapper.hasClass('disabled')) {
- wrapper.removeClass('disabled');
- return document.cookie = "peek=true; path=/";
- } else {
- wrapper.addClass('disabled');
- return document.cookie = "peek=false; path=/";
- }
- }
- };
fetchRequestResults = function() {
return $.ajax('/-/peek/results', {
data: {
@@ -68,7 +61,6 @@
error: function(xhr, textStatus, error) {}
});
};
- $(document).on('keypress', toggleBar);
$(document).on('peek:update', fetchRequestResults);
return $(function() {
if (peekEnabled()) {
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index c79ba5080a3..addf405e4f5 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -32,7 +32,7 @@ proguard/
# Android Studio captures folder
captures/
-# Intellij
+# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
diff --git a/vendor/gitignore/Composer.gitignore b/vendor/gitignore/Composer.gitignore
index c4222678424..a67d42b32f8 100644
--- a/vendor/gitignore/Composer.gitignore
+++ b/vendor/gitignore/Composer.gitignore
@@ -1,6 +1,6 @@
composer.phar
/vendor/
-# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
+# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore
index dff26a9ab70..846a1db836c 100644
--- a/vendor/gitignore/Global/Windows.gitignore
+++ b/vendor/gitignore/Global/Windows.gitignore
@@ -7,7 +7,7 @@ ehthumbs_vista.db
*.stackdump
# Folder config file
-Desktop.ini
+[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore
index 9bf1537f6ae..ecf66f84291 100644
--- a/vendor/gitignore/Perl.gitignore
+++ b/vendor/gitignore/Perl.gitignore
@@ -24,7 +24,7 @@ Build.bat
# Module::Install
inc/
-# ExtUitls::MakeMaker
+# ExtUtils::MakeMaker
/blib/
/_eumm/
/*.gz
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
index 9b5aebb1b35..1fef4ab91e1 100644
--- a/vendor/gitignore/Terraform.gitignore
+++ b/vendor/gitignore/Terraform.gitignore
@@ -1,10 +1,9 @@
-# Compiled files
-*.tfstate
-*.tfstate.*.backup
-*.tfstate.backup
+# Local .terraform directories
+**/.terraform/*
-# Module directory
-.terraform/
+# .tfstate files
+*.tfstate
+*.tfstate.*
-# Variable values for development
-terraform.tfvars
+# .tfvars files
+*.tfvars
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 0867ec5a7ee..509668db67a 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -171,11 +171,11 @@ PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
-**/packages/*
+**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
-!**/packages/build/
+!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
-#!**/packages/repositories.config
+#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index c93e6567baf..88261502d7f 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -335,7 +335,9 @@ production:
function check_kube_domain() {
if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then
- echo "In order to deploy, AUTO_DEVOPS_DOMAIN must be set as a variable at the group or project level, or manually added in .gitlab-cy.yml"
+ echo "In order to deploy or use Review Apps, AUTO_DEVOPS_DOMAIN variable must be set"
+ echo "You can do it in Auto DevOps project settings or defining a secret variable at group or project level"
+ echo "You can also manually add it in .gitlab-ci.yml"
false
else
true
diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
index 02cfab3a5b2..36386a19fdc 100644
--- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
@@ -16,7 +16,7 @@ image: "crystallang/crystal:latest"
# Cache shards in between builds
cache:
paths:
- - libs
+ - lib
# This is a basic example for a shard or script which doesn't use
# services such as redis or postgres
diff --git a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
index 636cb0a9a99..290b9997084 100644
--- a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
@@ -31,14 +31,14 @@ test2:
- oc project "$CI_PROJECT_NAME-$CI_PROJECT_ID" 2> /dev/null || oc new-project "$CI_PROJECT_NAME-$CI_PROJECT_ID"
script:
- "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
- - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
+ - "oc start-build $APP --from-dir=. --follow || sleep 3s && oc start-build $APP --from-dir=. --follow"
- "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST"
review:
<<: *deploy
stage: review
variables:
- APP: $CI_COMMIT_REF_NAME
+ APP: review-$CI_COMMIT_REF_NAME
APP_HOST: $CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
environment:
name: review/$CI_COMMIT_REF_NAME
@@ -56,7 +56,7 @@ stop-review:
- oc delete all -l "app=$APP"
when: manual
variables:
- APP: $CI_COMMIT_REF_NAME
+ APP: review-$CI_COMMIT_REF_NAME
GIT_STRATEGY: none
environment:
name: review/$CI_COMMIT_REF_NAME
diff --git a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
index 7810121c350..6573eceaa59 100644
--- a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
@@ -1,6 +1,6 @@
-# Unofficial language image. Look for the different tagged releases at:
-# https://hub.docker.com/r/scorpil/rust/tags/
-image: "scorpil/rust:stable"
+# Official language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/rust/tags/
+image: "rust:latest"
# Optional: Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
diff --git a/vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml b/vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml
new file mode 100644
index 00000000000..fc3d4ecdbba
--- /dev/null
+++ b/vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml
@@ -0,0 +1,86 @@
+# The following script will work for any project that can be built from command line by msbuild
+# It uses powershell shell executor, so you need to add the following line to your config.toml file
+# (located in gitlab-runner.exe directory):
+# shell = "powershell"
+#
+# The script is composed of 3 stages: build, test and deploy.
+#
+# The build stage restores NuGet packages and uses msbuild to build the exe and msi
+# One major issue you'll find is that you can't build msi projects from command line
+# if you use vdproj. There are workarounds building msi via devenv, but they rarely work
+# The best solution is migrating your vdproj projects to WiX, as it can be build directly
+# by msbuild.
+#
+# The test stage runs nunit from command line against Test project inside your solution
+# It also saves the resulting TestResult.xml file
+#
+# The deploy stage copies the exe and msi from build stage to a network drive
+# You need to have the network drive mapped as Local System user for gitlab-runner service to see it
+# The best way to persist the mapping is via a scheduled task (see: https://stackoverflow.com/a/7867064/1288473),
+# running the following batch command: net use P: \\x.x.x.x\Projects /u:your_user your_pass /persistent:yes
+
+
+# place project specific paths in variables to make the rest of the script more generic
+variables:
+ EXE_RELEASE_FOLDER: 'YourApp\bin\Release'
+ MSI_RELEASE_FOLDER: 'Setup\bin\Release'
+ TEST_FOLDER: 'Tests\bin\Release'
+ DEPLOY_FOLDER: 'P:\Projects\YourApp\Builds'
+
+ NUGET_PATH: 'C:\NuGet\nuget.exe'
+ MSBUILD_PATH: 'C:\Program Files (x86)\MSBuild\14.0\Bin\msbuild.exe'
+ NUNIT_PATH: 'C:\Program Files (x86)\NUnit.org\nunit-console\nunit3-console.exe'
+
+stages:
+ - build
+ - test
+ - deploy
+
+build_job:
+ stage: build
+ only:
+ - tags # the build process will only be started by git tag commits
+ script:
+ - '& "$env:NUGET_PATH" restore' # restore Nuget dependencies
+ - '& "$env:MSBUILD_PATH" /p:Configuration=Release' # build the project
+ artifacts:
+ expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
+ paths:
+ - '$env:EXE_RELEASE_FOLDER\YourApp.exe' # saving exe to copy to deploy folder
+ - '$env:MSI_RELEASE_FOLDER\YourApp Setup.msi' # saving msi to copy to deploy folder
+ - '$env:TEST_FOLDER\' # saving entire Test project so NUnit can run tests
+
+test_job:
+ stage: test
+ only:
+ - tags
+ script:
+ - '& "$env:NUNIT_PATH" ".\$env:TEST_FOLDER\Tests.dll"' # running NUnit tests
+ artifacts:
+ expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
+ paths:
+ - '.\TestResult.xml' # saving NUnit results to copy to deploy folder
+ dependencies:
+ - build_job
+
+deploy_job:
+ stage: deploy
+ only:
+ - tags
+ script:
+ # Compose a folder for each release based on commit tag.
+ # Assuming your tag is Rev1.0.0.1, and your last commit message is 'First commit'
+ # the artifact files will be copied to:
+ # P:\Projects\YourApp\Builds\Rev1.0.0.1 - First commit\
+ - '$commitSubject = git log -1 --pretty=%s'
+ - '$deployFolder = $($env:DEPLOY_FOLDER) + "\" + $($env:CI_BUILD_TAG) + " - " + $commitSubject + "\"'
+
+ # xcopy takes care of recursively creating required folders
+ - 'xcopy /y ".\$env:EXE_RELEASE_FOLDER\YourApp.exe" "$deployFolder"'
+ - 'xcopy /y ".\$env:MSI_RELEASE_FOLDER\YourApp Setup.msi" "$deployFolder"'
+ - 'xcopy /y ".\TestResult.xml" "$deployFolder"'
+
+ dependencies:
+ - build_job
+ - test_job
+ \ No newline at end of file
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 9f78059986d..6f6ca5f8b32 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -1,7 +1,11 @@
+"","","MIT,ISC,Apache 2.0,New BSD,Simplified BSD"
RedCloth,4.3.2,MIT
abbrev,1.0.9,ISC
+abbrev,1.1.0,ISC
accepts,1.3.3,MIT
ace-rails-ap,4.1.2,MIT
+acorn,3.3.0,MIT
+acorn,4.0.13,MIT
acorn,5.1.1,MIT
acorn-dynamic-import,2.0.2,MIT
acorn-jsx,3.0.1,MIT
@@ -15,7 +19,9 @@ activesupport,4.2.8,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
@@ -24,10 +30,14 @@ 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
@@ -35,6 +45,7 @@ arr-flatten,1.0.1,MIT
array-find,1.0.0,MIT
array-find-index,1.0.2,MIT
array-flatten,1.1.1,MIT
+array-flatten,2.1.1,MIT
array-slice,0.2.3,MIT
array-union,1.0.2,MIT
array-uniq,1.0.3,MIT
@@ -44,15 +55,24 @@ 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
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
@@ -130,6 +150,7 @@ babel-types,6.23.0,MIT
babosa,1.0.2,MIT
babylon,6.16.1,MIT
backo2,1.0.2,MIT
+balanced-match,0.4.2,MIT
balanced-match,1.0.0,MIT
base32,0.3.2,MIT
base64-arraybuffer,0.1.5,MIT
@@ -137,19 +158,25 @@ base64-js,1.2.0,MIT
base64id,1.0.0,MIT
batch,0.6.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
@@ -168,17 +195,23 @@ builder,3.2.3,MIT
builtin-modules,1.1.1,MIT
builtin-status-codes,3.0.0,MIT
bytes,2.4.0,MIT
+bytes,2.5.0,MIT
caller-path,0.1.0,MIT
callsite,1.0.0,unknown
callsites,0.2.0,MIT
+camelcase,1.2.1,MIT
+camelcase,2.1.1,MIT
+camelcase,3.0.0,MIT
camelcase,4.1.0,MIT
camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
caniuse-db,1.0.30000649,CC-BY-4.0
-carrierwave,1.1.0,MIT
+carrierwave,1.2.1,MIT
+caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
chalk,1.1.3,MIT
+chalk,2.3.0,MIT
charlock_holmes,0.7.5,MIT
chokidar,1.7.0,MIT
chronic,0.10.2,MIT
@@ -191,6 +224,7 @@ clap,1.1.3,MIT
cli-cursor,1.0.2,MIT
cli-width,2.1.0,ISC
clipboard,1.6.1,MIT
+cliui,2.1.0,ISC
cliui,3.2.0,ISC
clone,1.0.2,MIT
co,4.6.0,MIT
@@ -204,9 +238,11 @@ 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
@@ -220,7 +256,8 @@ 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,[Circular]
+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
@@ -230,6 +267,7 @@ convert-source-map,1.3.0,MIT
cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
copy-webpack-plugin,4.0.1,MIT
+core-js,2.3.0,MIT
core-js,2.4.1,MIT
core-util-is,1.0.2,MIT
cosmiconfig,2.1.1,MIT
@@ -240,9 +278,11 @@ 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
@@ -250,11 +290,16 @@ 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
@@ -269,7 +314,11 @@ default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
defined,1.0.0,MIT
del,2.2.2,MIT
+del,3.0.0,MIT
+delayed-stream,1.0.0,MIT
delegate,3.1.2,MIT
+delegates,1.0.0,MIT
+depd,1.1.0,MIT
depd,1.1.1,MIT
des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
@@ -285,12 +334,14 @@ 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
@@ -298,9 +349,10 @@ 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,[Circular]
+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
@@ -315,6 +367,7 @@ end-of-stream,1.4.0,MIT
engine.io,1.8.3,MIT
engine.io-client,1.8.3,MIT
engine.io-parser,1.3.2,MIT
+enhanced-resolve,0.9.1,MIT
enhanced-resolve,3.4.1,MIT
ent,2.2.0,MIT
entities,1.1.1,BSD-like
@@ -346,9 +399,12 @@ 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
@@ -365,12 +421,14 @@ 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
@@ -378,6 +436,8 @@ 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
@@ -386,17 +446,19 @@ file-loader,0.11.1,MIT
filename-regex,2.0.0,MIT
fileset,2.0.3,MIT
filesize,3.3.0,New BSD
+filesize,3.5.10,New BSD
fill-range,2.2.3,MIT
finalhandler,1.0.4,MIT
find-cache-dir,1.0.0,MIT
find-root,0.1.2,MIT
+find-up,1.1.2,MIT
find-up,2.1.0,MIT
flat-cache,1.2.2,MIT
flatten,1.0.2,MIT
flipper,0.10.2,MIT
flipper-active_record,0.10.2,MIT
flowdock,0.7.1,MIT
-fog-aliyun,0.1.0,MIT
+fog-aliyun,0.2.0,MIT
fog-aws,1.4.0,MIT
fog-core,1.44.3,MIT
fog-google,0.5.3,MIT
@@ -409,6 +471,8 @@ 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
@@ -416,8 +480,12 @@ from,0.1.7,MIT
fs-access,1.0.1,MIT
fs-extra,0.26.7,MIT
fs.realpath,1.0.0,ISC
-fsevents,,unknown
+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
@@ -426,30 +494,37 @@ 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.41.0,MIT
+gitaly-proto,0.51.0,MIT
github-linguist,4.7.6,MIT
github-markup,1.6.1,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
gitlab-grit,2.8.2,MIT
-gitlab-markup,1.6.2,MIT
+gitlab-markup,1.6.3,MIT
gitlab-svgs,1.0.4,unknown
gitlab_omniauth-ldap,2.0.4,MIT
+glob,5.0.15,ISC
glob,6.0.4,ISC
+glob,7.1.1,ISC
+glob,7.1.2,ISC
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
globalid,0.3.7,MIT
globals,9.18.0,MIT
globby,5.0.0,MIT
+globby,6.1.0,MIT
gollum-grit_adapter,1.0.1,MIT
gollum-lib,4.2.7,MIT
gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.13.6,Apache 2.0
-google-protobuf,3.4.0.2,New BSD
+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
@@ -458,25 +533,31 @@ 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.0,Apache 2.0
+grpc,1.6.6,Apache 2.0
gzip-size,3.0.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
handlebars,4.0.6,MIT
+har-schema,1.0.5,ISC
+har-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
@@ -489,10 +570,12 @@ 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
@@ -513,6 +596,7 @@ indexof,0.0.1,unknown
infinity-agent,2.0.3,MIT
inflight,1.0.6,ISC
influxdb,0.2.3,MIT
+inherits,2.0.1,ISC
inherits,2.0.3,ISC
ini,1.3.4,ISC
inquirer,0.12.0,MIT
@@ -532,12 +616,16 @@ is-builtin-module,1.0.0,MIT
is-dotfile,1.0.2,MIT
is-equal-shallow,0.1.3,MIT
is-extendable,0.1.1,MIT
+is-extglob,1.0.0,MIT
is-extglob,2.1.1,MIT
is-finite,1.0.2,MIT
+is-fullwidth-code-point,1.0.0,MIT
is-fullwidth-code-point,2.0.0,MIT
+is-glob,2.0.1,MIT
is-glob,3.1.0,MIT
is-my-json-valid,2.16.0,MIT
is-npm,1.0.0,MIT
+is-number,0.1.1,MIT
is-number,2.1.0,MIT
is-object,1.0.1,MIT
is-path-cwd,1.0.0,MIT
@@ -553,13 +641,16 @@ 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,2.0.0,ISC
+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
@@ -573,6 +664,7 @@ 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
@@ -582,18 +674,23 @@ js-beautify,1.6.12,MIT
js-cookie,2.1.3,MIT
js-tokens,3.0.1,MIT
js-yaml,3.7.0,MIT
+js-yaml,3.9.1,MIT
+jsbn,0.1.1,MIT
+jsesc,0.5.0,MIT
jsesc,1.3.0,MIT
json,1.8.6,ruby
json-jwt,1.7.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,[Circular]
+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
@@ -619,11 +716,14 @@ 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
@@ -634,11 +734,13 @@ lodash._getnative,3.9.1,MIT
lodash._isiterateecall,3.0.9,MIT
lodash._topath,3.8.1,MIT
lodash.assign,3.2.0,MIT
+lodash.camelcase,4.1.1,MIT
lodash.camelcase,4.3.0,MIT
lodash.capitalize,4.2.1,MIT
lodash.cond,4.5.2,MIT
lodash.deburr,4.1.0,MIT
lodash.defaults,3.1.2,MIT
+lodash.get,3.7.0,MIT
lodash.get,4.4.2,MIT
lodash.isarguments,3.1.0,MIT
lodash.isarray,3.0.4,MIT
@@ -658,7 +760,9 @@ loofah,2.0.3,MIT
loose-envify,1.3.1,MIT
loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
+lru-cache,2.2.4,MIT
lru-cache,3.2.0,ISC
+lru-cache,4.0.2,ISC
macaddress,0.2.8,MIT
mail,2.6.6,MIT
mail_room,0.9.1,MIT
@@ -670,6 +774,7 @@ 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
@@ -677,8 +782,10 @@ 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,[Circular]
+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
@@ -687,13 +794,17 @@ mimic-response,1.0.0,MIT
mini_portile2,2.3.0,MIT
minimalistic-assert,1.0.0,ISC
minimatch,3.0.3,ISC
+minimatch,3.0.4,ISC
minimist,0.0.8,MIT
+minimist,1.2.0,MIT
mkdirp,0.5.1,MIT
mmap2,2.2.7,ruby
moment,2.17.1,MIT
-monaco-editor,0.8.3,MIT
+monaco-editor,0.10.0,MIT
mousetrap,1.4.6,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
+ms,0.7.1,MIT
+ms,0.7.2,MIT
ms,2.0.0,MIT
multi_json,1.12.2,MIT
multi_xml,0.6.0,MIT
@@ -703,7 +814,9 @@ 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
@@ -712,21 +825,30 @@ 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
-normalize-package-data,2.3.5,Simplified BSD
+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
@@ -766,6 +888,7 @@ orm_adapter,0.5.0,MIT
os,0.9.6,MIT
os-browserify,0.2.1,MIT
os-homedir,1.0.2,MIT
+os-locale,1.4.0,MIT
os-locale,2.1.0,MIT
os-tmpdir,1.0.2,MIT
osenv,0.1.4,ISC
@@ -776,6 +899,7 @@ p-locate,2.0.0,MIT
p-map,1.1.1,MIT
p-timeout,1.2.0,MIT
package-json,1.2.0,MIT
+pako,0.2.9,MIT
pako,1.0.5,(MIT AND Zlib)
paranoia,2.3.1,MIT
parse-asn1,5.0.0,ISC
@@ -786,28 +910,34 @@ 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
+peek-mysql2,1.1.0,MIT
peek-performance_bar,1.3.0,MIT
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
-pikaday,1.5.1,"BSD,MIT"
+pify,3.0.0,MIT
+pikaday,1.6.1,MIT
pinkie,2.0.4,MIT
pinkie-promise,2.0.1,MIT
+pkg-dir,1.0.0,MIT
pkg-dir,2.0.0,MIT
pkg-up,1.0.0,MIT
pluralize,1.2.1,MIT
@@ -860,7 +990,7 @@ private,0.1.7,MIT
process,0.11.9,MIT
process-nextick-args,1.0.7,MIT
progress,1.1.8,MIT
-prometheus-client-mmap,0.7.0.beta14,Apache 2.0
+prometheus-client-mmap,0.7.0.beta18,Apache 2.0
proto-list,1.2.4,ISC
proxy-addr,1.1.5,MIT
prr,0.0.0,MIT
@@ -868,15 +998,18 @@ ps-tree,1.1.0,MIT
pseudomap,1.0.2,ISC
public-encrypt,4.0.0,MIT
public_suffix,3.0.0,MIT
+punycode,1.3.2,MIT
punycode,1.4.1,MIT
pyu-ruby-sasl,0.0.3.3,MIT
q,1.5.0,MIT
qjobs,1.1.5,MIT
+qs,6.4.0,New BSD
qs,6.5.0,New BSD
query-string,4.3.2,MIT
querystring,0.2.0,MIT
-querystring-es3,0.2.1,[Circular]
+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
@@ -908,9 +1041,14 @@ rdoc,4.2.2,ruby
re2,1.1.1,New BSD
react-dev-utils,0.5.2,New BSD
read-all-stream,3.1.0,MIT
+read-pkg,1.1.0,MIT
read-pkg,2.0.0,MIT
+read-pkg-up,1.0.1,MIT
read-pkg-up,2.0.0,MIT
+readable-stream,1.0.34,MIT
readable-stream,2.0.6,MIT
+readable-stream,2.2.9,MIT
+readable-stream,2.3.3,MIT
readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
@@ -932,21 +1070,26 @@ regenerate,1.3.2,MIT
regenerator-runtime,0.10.1,MIT
regenerator-transform,0.9.8,BSD
regex-cache,0.4.3,MIT
+regexpu-core,1.0.0,MIT
regexpu-core,2.0.0,MIT
registry-url,3.1.0,MIT
regjsgen,0.2.0,MIT
regjsparser,0.1.5,BSD
remove-trailing-separator,1.1.0,ISC
repeat-element,1.1.2,MIT
+repeat-string,0.2.2,MIT
repeat-string,1.6.1,MIT
+repeating,1.1.3,MIT
repeating,2.0.1,MIT
representable,3.0.4,MIT
+request,2.81.0,Apache 2.0
request_store,1.3.1,MIT
require-directory,2.1.1,MIT
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
@@ -972,6 +1115,7 @@ 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
@@ -985,6 +1129,7 @@ select-hose,2.0.0,MIT
select2,3.5.2-browserify,unknown
select2-rails,3.5.9.3,MIT
selfsigned,1.10.1,MIT
+semver,4.3.6,ISC
semver,5.3.0,ISC
semver-diff,2.1.0,MIT
send,0.15.4,MIT
@@ -1011,14 +1156,20 @@ 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
@@ -1031,6 +1182,7 @@ 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
state_machines,0.4.0,MIT
state_machines-activemodel,0.4.0,MIT
state_machines-activerecord,0.4.0,MIT
@@ -1042,19 +1194,29 @@ 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
@@ -1067,9 +1229,10 @@ three-stl-loader,1.0.4,MIT
through,2.3.8,MIT
thunky,0.1.0,unknown
tilt,2.0.6,MIT
-time-stamp,2.0.0,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
@@ -1079,15 +1242,20 @@ 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
u2f,0.2.1,MIT
uber,0.1.0,MIT
@@ -1095,6 +1263,8 @@ 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
@@ -1111,6 +1281,8 @@ update-notifier,0.5.0,Simplified BSD
url,0.11.0,MIT
url-loader,0.5.8,MIT
url-parse,1.0.5,MIT
+url-parse,1.1.7,MIT
+url-parse,1.1.9,MIT
url-parse-lax,1.0.0,MIT
url-to-options,1.0.1,MIT
url_safe_base64,0.2.2,MIT
@@ -1118,26 +1290,28 @@ 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,[Circular]
+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.2.6,MIT
+vue,2.5.2,MIT
vue-hot-reload-api,2.0.11,MIT
vue-loader,11.3.4,MIT
vue-resource,1.3.4,MIT
vue-style-loader,2.0.5,MIT
-vue-template-compiler,2.2.6,MIT
+vue-template-compiler,2.5.2,MIT
vue-template-es2015-compiler,1.5.1,MIT
-vuex,2.3.1,MIT
+vuex,3.0.0,MIT
warden,1.2.6,MIT
watchpack,1.4.0,MIT
wbuf,1.7.2,MIT
@@ -1151,15 +1325,20 @@ 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.3.0,ISC
+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
@@ -1168,6 +1347,9 @@ xmlhttprequest-ssl,1.5.3,MIT
xtend,4.0.1,MIT
y18n,3.2.1,ISC
yallist,2.1.2,ISC
+yargs,3.10.0,MIT
+yargs,6.6.0,MIT
yargs,8.0.2,MIT
+yargs-parser,4.2.1,ISC
yargs-parser,7.0.0,ISC
yeast,0.1.2,MIT
diff --git a/yarn.lock b/yarn.lock
index 91ffbe5d4b0..9bdf5e0f64b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,10 @@
# yarn lockfile v1
+"@gitlab-org/gitlab-svgs@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.0.2.tgz#e4d29058e2bb438ba71ac525c6397ef15ae2877b"
+
abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
@@ -248,6 +252,10 @@ autoprefixer@^6.3.1:
postcss "^5.2.16"
postcss-value-parser "^3.2.3"
+autosize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.0.tgz#7a0599b1ba84d73bd7589b0d9da3870152c69237"
+
aws-sign2@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
@@ -256,6 +264,12 @@ aws4@^1.2.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+axios-mock-adapter@^1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d"
+ dependencies:
+ deep-equal "^1.0.1"
+
axios@^0.16.2:
version "0.16.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
@@ -914,6 +928,17 @@ binary-extensions@^1.0.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
+blackst0ne-mermaid@^7.1.0-fixed:
+ version "7.1.0-fixed"
+ resolved "https://registry.yarnpkg.com/blackst0ne-mermaid/-/blackst0ne-mermaid-7.1.0-fixed.tgz#3707b3a113d78610e3068e18a588f46b4688de49"
+ dependencies:
+ d3 "3.5.17"
+ dagre-d3-renderer "^0.4.24"
+ dagre-layout "^0.8.0"
+ he "^1.1.1"
+ lodash "^4.17.4"
+ moment "^2.18.1"
+
blob@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
@@ -1630,6 +1655,10 @@ custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+d3@3.5.17:
+ version "3.5.17"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
+
d3@^3.5.11:
version "3.5.11"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
@@ -1646,6 +1675,22 @@ d@^0.1.1:
dependencies:
es5-ext "~0.10.2"
+dagre-d3-renderer@^0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.4.24.tgz#b36ce2fe4ea20de43e7698627c6ede2a9f15ec45"
+ dependencies:
+ d3 "3.5.17"
+ dagre-layout "^0.8.0"
+ graphlib "^2.1.1"
+ lodash "^4.17.4"
+
+dagre-layout@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/dagre-layout/-/dagre-layout-0.8.0.tgz#7147b6afb655602f855158dfea171db9aa98d4ff"
+ dependencies:
+ graphlib "^2.1.1"
+ lodash "^4.17.4"
+
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -2671,6 +2716,10 @@ function-bind@^1.1.1, function-bind@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+fuzzaldrin-plus@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/fuzzaldrin-plus/-/fuzzaldrin-plus-0.5.0.tgz#ef5f26f0c2fc7e9e9a16ea149a802d6cb4804b1e"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -2712,10 +2761,6 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
-"gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git":
- version "1.0.4"
- resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#46c0a49cd43639948dfcc77a0f94d59deaad1e85"
-
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -2844,6 +2889,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+graphlib@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.1.tgz#42352c52ba2f4d035cb566eb91f7395f76ebc951"
+ dependencies:
+ lodash "^4.11.1"
+
gzip-size@3.0.0, gzip-size@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
@@ -2938,7 +2989,7 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
-he@^1.1.0:
+he@^1.1.0, he@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
@@ -3934,7 +3985,7 @@ lodash@^3.8.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -4148,6 +4199,10 @@ moment@2.x:
version "2.17.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
+moment@^2.18.1:
+ version "2.19.2"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"
+
monaco-editor@0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.10.0.tgz#6604932585fe9c1f993f000a503d0d20fbe5896a"
@@ -6390,9 +6445,9 @@ vue-style-loader@^2.0.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.2.6:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.2.6.tgz#2e2928daf0cd0feca9dfc35a9729adeae173ec68"
+vue-template-compiler@^2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.2.tgz#6f198ebc677b8f804315cd33b91e849315ae7177"
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
@@ -6401,9 +6456,9 @@ vue-template-es2015-compiler@^1.2.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.5.1.tgz#0c36cc57aa3a9ec13e846342cb14a72fcac8bd93"
-vue@^2.2.6:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed"
+vue@^2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.2.tgz#fd367a87bae7535e47f9dc5c9ec3b496e5feb5a4"
vuex@^3.0.0:
version "3.0.0"